/* * Nextview acquisition control * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License Version 2 as * published by the Free Software Foundation. You find a copy of this * license in the file COPYRIGHT in the root directory of this release. * * THIS PROGRAM IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, * BUT WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * * Description: * * This module implements the main control of the acquisition process. * It contains the top-level start and stop acquisition functions * which invoke functions in other acq related modules as needed. * * Author: Tom Zoerner * * $Id: epgacqctl.c,v 1.87 2004/12/24 10:15:56 tom Exp tom $ */ #define DEBUG_SWITCH DEBUG_SWITCH_EPGCTL #define DPRINTF_OFF #include #include #include #include "epgctl/mytypes.h" #include "epgctl/debug.h" #include "epgvbi/btdrv.h" #include "epgvbi/ttxdecode.h" #include "epgdb/epgblock.h" #include "epgdb/epgdbif.h" #include "epgdb/epgstream.h" #include "epgdb/epgqueue.h" #include "epgdb/epgtscqueue.h" #include "epgdb/epgdbsav.h" #include "epgdb/epgdbmgmt.h" #include "epgdb/epgdbmerge.h" #include "epgui/uictrl.h" #include "epgui/epgmain.h" #include "epgctl/epgctxmerge.h" #include "epgctl/epgctxctl.h" #include "epgctl/epgscan.h" #include "epgctl/epgacqctl.h" #include "epgctl/epgacqsrv.h" #include "epgctl/epgacqclnt.h" // --------------------------------------------------------------------------- // Declaration of module state variables typedef struct { EPGACQ_STATE state; EPGACQ_MODE mode; EPGACQ_MODE userMode; EPGACQ_PASSIVE passiveReason; EPGACQ_PHASE cyclePhase; EPGACQ_PHASE stopPhase; uint cycleIdx; uint cniCount; uint cniTab[MAX_MERGED_DB_COUNT]; time_t dumpTime; time_t chanChangeTime; uint inputSource; uint currentSlicerType; uint currentPageNo; bool autoSlicerType; bool haveWarnedInpSrc; } EPGACQCTL_STATE; static EPGACQCTL_STATE acqCtl = {ACQSTATE_OFF, ACQMODE_FOLLOW_UI, ACQMODE_FOLLOW_UI}; static EPGDB_STATS acqStats; static bool acqStatsUpdate; static bool acqVpsPdcUpdate[VPSPDC_REQ_COUNT]; static EPGACQ_DESCR acqNetDescr; static bool acqContextIsPeek; static EPGDB_QUEUE acqDbQueue; static EPGDB_PI_TSC acqTsc; static bool acqTscEnabled = FALSE; EPGDB_CONTEXT * pAcqDbContext = NULL; static void EpgAcqCtl_InitCycle( void ); static bool EpgAcqCtl_UpdateProvider( bool changeDb ); // Interface for notifications from acquisition static void EpgAcqCtl_ChannelChange( bool changeDb ); static bool EpgAcqCtl_AiCallback( const AI_BLOCK *pNewAi ); static bool EpgAcqCtl_BiCallback( const BI_BLOCK *pBi ); static const EPGDB_ADD_CB epgQueueCb = { EpgAcqCtl_AiCallback, EpgAcqCtl_BiCallback, }; // when very many new blocks are in the queue, connection to UI must be locked #define EPG_QUEUE_OVERFLOW_LEN 250 // --------------------------------------------------------------------------- // Reset statistic values // - should be called right after start/reset acq and once after first AI // - in network acq mode it resets the cached information from the remote acq process // static void EpgAcqCtl_StatisticsReset( void ) { memset(&acqStats, 0, sizeof(acqStats)); memset(&acqNetDescr, 0, sizeof(acqNetDescr)); memset(&acqVpsPdcUpdate, FALSE, sizeof(acqVpsPdcUpdate)); acqStats.acqStartTime = time(NULL); acqStats.histIdx = STATS_HIST_WIDTH - 1; // reset acquisition repetition counters for all PI if (pAcqDbContext != NULL) { EpgDbResetAcqRepCounters(pAcqDbContext); } acqStatsUpdate = TRUE; } // --------------------------------------------------------------------------- // Maintain statistic values for AcqStat window // - should be called after each received AI block // - the history is also maintained when no stats window is open, because // once one is opened, a complete history can be displayed // static void EpgAcqCtl_StatisticsUpdate( void ) { time_t acqMinTime[2], aiDiff; time_t aiAcqTimestamp; time_t now = time(NULL); EPGDB_HIST * pHist; uint total, obsolete; uint stream, idx; acqStatsUpdate = FALSE; acqStats.nowNextMaxAcqRepCount = EpgDbGetNowCycleMaxRepCounter(pAcqDbContext); // determine min. acq time for PI blocks to count as "up to date" in the GetStat function if (acqCtl.cyclePhase == ACQMODE_PHASE_MONITOR) { // in the final phase, e.g. stream 1 should be updated after max. 60 mins acqMinTime[0] = now - EPGACQCTL_STREAM1_UPD_INTV; acqMinTime[1] = now - EPGACQCTL_STREAM2_UPD_INTV; } else { // initially disregard acq time (only the PI version is evaluated) memset(acqMinTime, 0, sizeof(acqMinTime)); } // determine block counts in current database EpgDbGetStat(pAcqDbContext, acqStats.count, acqMinTime, acqStats.nowNextMaxAcqRepCount); if (acqCtl.state == ACQSTATE_RUNNING) { // compute minimum and maximum distance between AI blocks (in seconds) aiAcqTimestamp = EpgDbGetAiUpdateTime(pAcqDbContext); if ((acqStats.ai.lastAiTime != 0) && (aiAcqTimestamp > acqStats.ai.lastAiTime)) { aiDiff = aiAcqTimestamp - acqStats.ai.lastAiTime; if ((aiDiff < acqStats.ai.minAiDistance) || (acqStats.ai.minAiDistance == 0)) acqStats.ai.minAiDistance = aiDiff; if (aiDiff > acqStats.ai.maxAiDistance) acqStats.ai.maxAiDistance = aiDiff; acqStats.ai.sumAiDistance += aiDiff; } acqStats.ai.lastAiTime = EpgDbGetAiUpdateTime(pAcqDbContext); acqStats.ai.aiCount += 1; // maintain history of block counts per stream total = acqStats.count[0].ai + acqStats.count[1].ai; obsolete = acqStats.count[0].expired + acqStats.count[0].defective + acqStats.count[1].expired + acqStats.count[1].defective; dprintf4("EpgAcqCtl-StatisticsUpdate: AI #%d, db filled %.2f%%, variance %1.2f/%1.2f\n", acqStats.ai.aiCount, (double)(acqStats.count[0].allVersions + obsolete + acqStats.count[1].allVersions) / total * 100.0, acqStats.count[0].variance, acqStats.count[1].variance); acqStats.histIdx = (acqStats.histIdx + 1) % STATS_HIST_WIDTH; pHist = &acqStats.hist[acqStats.histIdx]; if (total > 0) { pHist->expir = (uchar)((double)obsolete / total * 128.0); pHist->s1cur = (uchar)((double)(acqStats.count[0].curVersion + obsolete) / total * 128.0); pHist->s1old = (uchar)((double)(acqStats.count[0].allVersions + obsolete) / total * 128.0); pHist->s2cur = (uchar)((double)(acqStats.count[0].allVersions + obsolete + acqStats.count[1].curVersion) / total * 128.0); pHist->s2old = (uchar)((double)(acqStats.count[0].allVersions + obsolete + acqStats.count[1].allVersions) / total * 128.0); } else memset(pHist, 0, sizeof(*pHist)); for (stream=0; stream <= 1; stream++) { if (acqStats.varianceHist[stream].count < VARIANCE_HIST_COUNT) { // history buffer not yet filled -> just append the new sample idx = acqStats.varianceHist[stream].count; acqStats.varianceHist[stream].count += 1; } else { // history buffer filled -> insert into ring buffer after the last written sample acqStats.varianceHist[stream].lastIdx = (acqStats.varianceHist[stream].lastIdx + 1) % VARIANCE_HIST_COUNT; idx = acqStats.varianceHist[stream].lastIdx; } acqStats.varianceHist[stream].buf[idx] = acqStats.count[stream].variance; } } } // --------------------------------------------------------------------------- // Return database block counts: number blocks in AI, number expired, etc. // - in network acq mode the info is forwarded by the acquisition daemon // (even if the database is fully open on client-side) // const EPGDB_BLOCK_COUNT * EpgAcqCtl_GetDbStats( void ) { const EPGDB_BLOCK_COUNT * pDbStats; assert(acqStatsUpdate == FALSE); // must not be called asynchronously if (acqCtl.state != ACQSTATE_OFF) { pDbStats = acqStats.count; } else pDbStats = NULL; return pDbStats; } // --------------------------------------------------------------------------- // Return complete set of acq state and statistic values // - used by "View acq statistics" popup window // - in network acq mode it returns info about the remote acquisition process // requires the extended stats report mode to be enabled; else only the // acq state (next function) is available // const EPGDB_STATS * EpgAcqCtl_GetAcqStats( void ) { const EPGDB_STATS * pAcqStats; if (acqCtl.state != ACQSTATE_OFF) { assert(acqStatsUpdate == FALSE); // must not be called asynchronously if (acqCtl.mode != ACQMODE_NETWORK) { // retrieve additional data from TTX packet decoder TtxDecode_GetStatistics(&acqStats.ttx.ttxPkgCount, &acqStats.ttx.epgPkgCount, &acqStats.ttx.epgPagCount); EpgStreamGetStatistics(&acqStats.stream); acqStats.lastStatsUpdate = time(NULL); } pAcqStats = &acqStats; } else pAcqStats = NULL; return pAcqStats; } // --------------------------------------------------------------------------- // Return VPS CNI received on EPG input channel // - used for EPG acq stats output; esp. useful in forced-passive mode, where // it allows to see what channel is tuned in instead of the acq provider's // - in network acq mode the VPS CNI is forwarded by the remote acq server // const EPGDB_ACQ_VPS_PDC * EpgAcqCtl_GetVpsPdc( VPSPDC_REQ_ID clientId ) { EPGDB_ACQ_VPS_PDC * result = NULL; uint idx; if (clientId < VPSPDC_REQ_COUNT) { if (acqCtl.state != ACQSTATE_OFF) { // poll for new VPS/PDC data if (EpgAcqCtl_ProcessVps()) { for (idx = 0; idx < VPSPDC_REQ_COUNT; idx++) { acqVpsPdcUpdate[idx] = TRUE; } } // if there are results which have not been given yet to the client, return them if ( acqVpsPdcUpdate[clientId] || (clientId == VPSPDC_REQ_DAEMON) ) { acqVpsPdcUpdate[clientId] = FALSE; result = &acqStats.vpsPdc; } } } else debug1("EpgAcqCtl-GetVpsPdc: illegal client id %d", clientId); return result; } #ifdef USE_DAEMON // --------------------------------------------------------------------------- // Update statistics about acquisition running on a remote server // - called after the block input queue was processed // - 4 modes are supported (to minimize required bandwidth): initial, initial // with prov info, update with prov info, minimal update (prov info must be // forwarded when acq runs on a database whose blocks are not forwarded) // - depending on the mode a different structure is enclosed // static void EpgAcqCtl_NetStatsUpdate( void ) { uint idx; bool aiFollows; if (EpgAcqClient_GetNetStats(&acqStats, &acqNetDescr, &aiFollows)) { ifdebug2(acqNetDescr.dbCni != EpgDbContextGetCni(pAcqDbContext), "EpgAcqCtl-NetStatsUpdate: stats dbCni=0x%04X != acq db CNI=0x%04X", acqNetDescr.dbCni, EpgDbContextGetCni(pAcqDbContext)); for (idx = 0; idx < VPSPDC_REQ_COUNT; idx++) { acqVpsPdcUpdate[idx] = TRUE; } if (aiFollows == FALSE) { // no AI transmitted (no reception, or AI unchanged) -> trigger status update from here UiControlMsg_AcqEvent(ACQ_EVENT_STATS_UPDATE); } } } // --------------------------------------------------------------------------- // Callback for incoming VPS/PDC report // void EpgAcqCtl_AddNetVpsPdc( const EPGDB_ACQ_VPS_PDC * pVpsPdcUpd ) { uint idx; acqStats.vpsPdc = *pVpsPdcUpd; for (idx = 0; idx < VPSPDC_REQ_COUNT; idx++) { acqVpsPdcUpdate[idx] = TRUE; } UiControlMsg_AcqEvent(ACQ_EVENT_VPS_PDC); dprintf5("EpgAcqCtl-VpsPdcCallback: %02d.%02d. %02d:%02d (0x%04X)\n", (acqStats.vpsPdc.pil >> 15) & 0x1F, (acqStats.vpsPdc.pil >> 11) & 0x0F, (acqStats.vpsPdc.pil >> 6) & 0x1F, (acqStats.vpsPdc.pil ) & 0x3F, acqStats.vpsPdc.cni); } // --------------------------------------------------------------------------- // Update daemon acq provider list for follow-ui mode // - but only if in follow-ui mode // void EpgAcqCtl_UpdateNetProvList( uint cniCount, const uint * pCniList ) { if ( (acqCtl.userMode == ACQMODE_FOLLOW_UI) || (acqCtl.userMode == ACQMODE_FOLLOW_MERGED) ) { EpgAcqCtl_SelectMode(ACQMODE_FOLLOW_UI, ACQMODE_PHASE_COUNT, cniCount, pCniList); } } #endif // USE_DAEMON // --------------------------------------------------------------------------- // En-/Disable sending of extended acq statistics // - note: when disabling extended reports, the according data is marked // invalid upon reception of the next statistics message; this is simpler // and has the advantage that a statistics message arrives inbetween and // makes the data valid again // void EpgAcqCtl_EnableAcqStats( bool enable ) { #ifdef USE_DAEMON // Pass through the setting to the network client (even while acq is off or not // in network mode, because it's needed as soon as a connection is established) EpgAcqClient_SetAcqStatsMode(enable); #endif } // --------------------------------------------------------------------------- // En-/Disable sending of PI timescale information // - param enable is set to TRUE if any timescale window is open, i.e. UI or ACQ // - param allProviders is set to TRUE if the acq timescale window is open; // this flag is only used in network mode; see the epgdbsrv.c module // void EpgAcqCtl_EnableTimescales( bool enable, bool allProviders ) { if (enable == FALSE) { if (acqCtl.mode != ACQMODE_NETWORK) { EpgTscQueue_Clear(&acqTsc); } } // this flag is only use when NOT in network acq mode acqTscEnabled = enable; #ifdef USE_DAEMON // Pass through the setting to the network client (even while acq is off or not // in network mode, because it's needed as soon as a connection is established) EpgAcqClient_SetAcqTscMode(enable, allProviders); #endif } // --------------------------------------------------------------------------- // Get Pointer to PI timescale queue // - used by the GUI to retrieve info from the queue (and thereby emptying it) // - returns NULL if acq is off (queue might not be initialized yet) // EPGDB_PI_TSC * EpgAcqCtl_GetTimescaleQueue( void ) { if (acqCtl.state != ACQSTATE_OFF) return &acqTsc; else return NULL; } // --------------------------------------------------------------------------- // Reset VPS/PDC acquisition after channel change caused by connected TV app. // void EpgAcqCtl_ResetVpsPdc( void ) { if (acqCtl.state != ACQSTATE_OFF) { dprintf0("EpgAcqCtl-ResetVpsPdc: cleared CNI\n"); // clear the cache acqStats.vpsPdc.cni = 0; if (acqCtl.mode != ACQMODE_NETWORK) { // reset EPG and ttx decoder state machine EpgAcqCtl_ChannelChange(FALSE); EpgAcqCtl_StatisticsUpdate(); } else { // tell server to send the next newly received VPS/PDC CNI & PIL #ifdef USE_DAEMON EpgAcqClient_SetVpsPdcMode(TRUE, TRUE); #endif } } } // --------------------------------------------------------------------------- // Start teletext decoding and initialize the EPG stream decoder // static void EpgAcqCtl_TtxStart( EPGDB_CONTEXT *dbc, EPGDB_QUEUE * pQueue, uint pageNo, uint appId ) { uint epgPageNo; uint epgAppId; bool bWaitForBiAi; if (VALID_EPG_PAGENO(pageNo)) epgPageNo = pageNo; else if (VALID_EPG_PAGENO(dbc->pageNo)) epgPageNo = dbc->pageNo; else epgPageNo = EPG_DEFAULT_PAGENO; if (appId != EPG_ILLEGAL_APPID) epgAppId = appId; else if (dbc->appId != EPG_ILLEGAL_APPID) epgAppId = dbc->appId; else epgAppId = EPG_DEFAULT_APPID; if (dbc->pAiBlock != NULL) { // provider already known // set up a list of alphabets for string decoding EpgBlockSetAlphabets(&dbc->pAiBlock->blk.ai); // since alphabets are known PI can be collected right from the start bWaitForBiAi = FALSE; } else { // unknown provider -> wait for AI before allowing PI bWaitForBiAi = TRUE; } // initialize the state of the streams decoder EpgStreamInit(pQueue, bWaitForBiAi, epgAppId, epgPageNo); TtxDecode_StartEpgAcq(epgPageNo, FALSE); acqCtl.currentPageNo = epgPageNo; } // --------------------------------------------------------------------------- // Stop teletext decoding and clear the EPG stream and its queue // static void EpgAcqCtl_TtxStop( void ) { TtxDecode_StopAcq(); EpgStreamClear(); } // --------------------------------------------------------------------------- // Stop and Re-Start teletext acquisition // - called after change of channel or internal parameters // static void EpgAcqCtl_TtxReset( EPGDB_CONTEXT *dbc, EPGDB_QUEUE * pQueue, uint pageNo, uint appId ) { // discard remaining blocks in the scratch buffer EpgStreamClear(); EpgAcqCtl_TtxStart(dbc, pQueue, pageNo, appId); } // --------------------------------------------------------------------------- // Close the acq database // - automatically chooses the right close function for the acq database // depending on how it was opened, i.e. as peek or fully // static void EpgAcqCtl_CloseDb( void ) { if (pAcqDbContext != NULL) { if (acqContextIsPeek == FALSE) EpgContextCtl_Close(pAcqDbContext); else EpgContextCtl_ClosePeek(pAcqDbContext); pAcqDbContext = NULL; } else fatal0("EpgAcqCtl-CloseDb: db not open"); } // --------------------------------------------------------------------------- // Switch provider database to a new provider // - called by AI callback upon provider change // and called upon GUI provider list update // - in network mode it the acquisition database is only opened as "peek" // if the acq db is not in the GUI provider list; when the GUI list is // updated it may be required to up-/downgrade the db // static void EpgAcqCtl_SwitchDb( uint acqCni ) { uint oldCni; uint idx; if (acqCtl.mode != ACQMODE_NETWORK) { EpgAcqCtl_CloseDb(); acqContextIsPeek = FALSE; pAcqDbContext = EpgContextCtl_Open(acqCni, CTX_FAIL_RET_CREATE, CTX_RELOAD_ERR_ANY); } else { oldCni = EpgDbContextGetCni(pAcqDbContext); // check if current acq provider is used by the new GUI providers for (idx=0; idx < acqCtl.cniCount; idx++) if (acqCtl.cniTab[idx] == acqCni) break; if (idx < acqCtl.cniCount) { // used by GUI if (acqCni != oldCni) { // used by GUI -> open the db completely EpgAcqCtl_CloseDb(); acqContextIsPeek = FALSE; pAcqDbContext = EpgContextCtl_Open(acqCni, CTX_FAIL_RET_CREATE, CTX_RELOAD_ERR_ANY); } else if (acqContextIsPeek == TRUE) { // currently open as peek -> upgrade to full open dprintf1("EpgAcqCtl-Switchdb: upgrade acq db 0x%04X to full open\n", acqCni); EpgAcqCtl_CloseDb(); pAcqDbContext = EpgContextCtl_Open(acqCni, CTX_FAIL_RET_DUMMY, CTX_RELOAD_ERR_ANY); acqContextIsPeek = FALSE; } } else { // current acq db is not in GUI list if (acqCni != oldCni) { // just open a peek, i.e. AI and OI #0 EpgAcqCtl_CloseDb(); acqContextIsPeek = FALSE; pAcqDbContext = EpgContextCtl_Peek(acqCni, CTX_RELOAD_ERR_ANY); if (pAcqDbContext != NULL) acqContextIsPeek = TRUE; else pAcqDbContext = EpgContextCtl_Open(acqCni, CTX_FAIL_RET_CREATE, CTX_RELOAD_ERR_ANY); } else if (acqContextIsPeek) { // currently fully open -> downgrade to peek dprintf1("EpgAcqCtl-Switchdb: downgrade acq db 0x%04X to peek\n", acqCni); EpgAcqCtl_CloseDb(); acqContextIsPeek = FALSE; pAcqDbContext = EpgContextCtl_Peek(acqCni, CTX_RELOAD_ERR_ANY); if (pAcqDbContext != NULL) acqContextIsPeek = TRUE; else pAcqDbContext = EpgContextCtl_OpenDummy(); } } } } // --------------------------------------------------------------------------- // Start the acquisition // - in network acq mode it establishes a connection to the remote acq process // bool EpgAcqCtl_Start( void ) { bool result = FALSE; dprintf0("EpgAcqCtl-Start: starting acquisition\n"); assert(pAcqDbContext == NULL); if (acqCtl.state == ACQSTATE_OFF) { if (acqCtl.mode == ACQMODE_NETWORK) { #ifdef USE_DAEMON EpgAcqCtl_InitCycle(); // open the dummy database pAcqDbContext = EpgContextCtl_OpenDummy(); acqContextIsPeek = FALSE; // VPS/PDC currently always enabled EpgAcqClient_SetVpsPdcMode(TRUE, FALSE); EpgDbQueue_Init(&acqDbQueue); EpgTscQueue_Init(&acqTsc); result = EpgAcqClient_StartAcq(acqCtl.cniTab, acqCtl.cniCount, &acqDbQueue, &acqTsc); #endif } else { // initialize teletext decoder EpgDbQueue_Init(&acqDbQueue); EpgTscQueue_Init(&acqTsc); acqCtl.chanChangeTime = 0; EpgAcqCtl_InitCycle(); if (BtDriver_StartAcq()) { // set input source and tuner frequency (also detect if device is busy) EpgAcqCtl_UpdateProvider(FALSE); EpgAcqCtl_TtxStart(pAcqDbContext, &acqDbQueue, EPG_ILLEGAL_PAGENO, EPG_ILLEGAL_APPID); result = TRUE; } } if (result) { // success -> initialize state // in network mode no BI block is transmitted if (acqCtl.mode == ACQMODE_NETWORK) acqCtl.state = ACQSTATE_WAIT_AI; else acqCtl.state = ACQSTATE_WAIT_BI; acqCtl.haveWarnedInpSrc = FALSE; EpgAcqCtl_StatisticsReset(); EpgAcqCtl_StatisticsUpdate(); acqCtl.dumpTime = acqStats.acqStartTime; UiControlMsg_AcqEvent(ACQ_EVENT_PROV_CHANGE); } else if (pAcqDbContext != NULL) { // failed to start acq -> clean up EpgAcqCtl_CloseDb(); } } return result; } // --------------------------------------------------------------------------- // Stop the acquisition // - the acq-sub process is terminated and the VBI device is freed // - this allows other processes (e.g. a teletext decoder) to access VBI // - in network acq mode the connection to the remote acq process is closed; // note: neither the daemon nor acq by the daemon are stopped // void EpgAcqCtl_Stop( void ) { if (acqCtl.state != ACQSTATE_OFF) { dprintf0("EpgAcqCtl-Stop: stopping acquisition\n"); assert(pAcqDbContext != NULL); if (acqCtl.mode == ACQMODE_NETWORK) { #ifdef USE_DAEMON EpgAcqClient_StopAcq(); #endif } else { BtDriver_StopAcq(); EpgAcqCtl_TtxStop(); EpgTscQueue_Clear(&acqTsc); } EpgAcqCtl_CloseDb(); UiControlMsg_AcqEvent(ACQ_EVENT_PROV_CHANGE); acqCtl.state = ACQSTATE_OFF; } } // --------------------------------------------------------------------------- // Stop acquisition for the EPG scan but keep the device open/driver loaded // void EpgAcqCtl_Suspend( bool suspend ) { if (suspend == FALSE) { EpgAcqCtl_Start(); } else { EpgAcqCtl_CloseDb(); EpgStreamClear(); EpgTscQueue_Clear(&acqTsc); acqCtl.state = ACQSTATE_OFF; } } // --------------------------------------------------------------------------- // Query cause for the last acquisition failure // - must only be called when acq start failed // const char * EpgAcqCtl_GetLastError( void ) { if (acqCtl.mode != ACQMODE_NETWORK) return BtDriver_GetLastError(); else return NULL; } // --------------------------------------------------------------------------- // Initialize the module for daemon mode // #ifdef USE_DAEMON void EpgAcqCtl_InitDaemon( void ) { } #endif // --------------------------------------------------------------------------- // Set input source and tuner frequency for a provider // - errors are reported to the user interface // static bool EpgAcqCtl_TuneProvider( uint freq, uint cni ) { bool isTuner; bool result = FALSE; assert(acqCtl.mode != ACQMODE_PASSIVE); // reset forced-passive state; will be set upon errors below acqCtl.mode = acqCtl.userMode; acqCtl.passiveReason = ACQPASSIVE_NONE; // remember time of channel change acqCtl.chanChangeTime = time(NULL); #ifdef USE_PROXY // XXX FIXME BtDriver_SetChannelProfile( VBI_CHANNEL_PRIO_BACKGROUND, ((acqCtl.cyclePhase == ACQMODE_PHASE_MONITOR) ? VBI_CHN_SUBPRIO_INITIAL : VBI_CHN_SUBPRIO_CHECK), ((acqCtl.cyclePhase == ACQMODE_PHASE_NOWNEXT) ? 3*60 : 25*60), ((acqCtl.cyclePhase == ACQMODE_PHASE_MONITOR) ? 60 : 10) ); #else BtDriver_SetChannelProfile(VBI_CHANNEL_PRIO_BACKGROUND, 0, 0, 0); #endif // tune onto the provider's channel (before starting acq, to avoid catching false data) // always set the input source - may have been changed externally (since we don't hog the video device permanently) if ( BtDriver_TuneChannel(acqCtl.inputSource, freq, FALSE, &isTuner) ) { if (isTuner) { if (freq != 0) { result = TRUE; } else { dprintf0("EpgAcqCtl-TuneProv: no freq in db\n"); // close the device, which was kept open after setting the input source if (cni != 0) { // inform the user that acquisition will not be possible acqCtl.passiveReason = ACQPASSIVE_NO_FREQ; UiControlMsg_MissingTunerFreq(cni); } else { acqCtl.passiveReason = ACQPASSIVE_NO_DB; } acqCtl.mode = ACQMODE_FORCED_PASSIVE; } } else { dprintf0("EpgAcqCtl-TuneProv: input is no tuner -> force to passive mode\n"); acqCtl.mode = ACQMODE_FORCED_PASSIVE; acqCtl.passiveReason = ACQPASSIVE_NO_TUNER; if (freq != 0) { if ((acqCtl.mode != ACQMODE_EXTERNAL) && (acqCtl.haveWarnedInpSrc == FALSE)) { // warn the user, but only once UiControlMsg_AcqPassive(); } acqCtl.haveWarnedInpSrc = TRUE; } } if (acqCtl.autoSlicerType) { // in auto-mode fall back to default slicer for every channel change acqCtl.currentSlicerType = VBI_SLICER_TRIVIAL; BtDriver_SelectSlicer(acqCtl.currentSlicerType); } } else { dprintf0("EpgAcqCtl-TuneProv: failed to set input source -> force to passive mode\n"); acqCtl.mode = ACQMODE_FORCED_PASSIVE; acqCtl.passiveReason = ACQPASSIVE_ACCESS_DEVICE; } return result; } // --------------------------------------------------------------------------- // Chooses a provider for the acquisition // static uint EpgAcqCtl_GetProvider( void ) { uint cni; // use original user configured mode to ignore possible current error state // because the caller wants to know which TV channel ought to be tuned switch (acqCtl.userMode) { case ACQMODE_FOLLOW_UI: case ACQMODE_FOLLOW_MERGED: case ACQMODE_CYCLIC_2: case ACQMODE_CYCLIC_012: case ACQMODE_CYCLIC_02: case ACQMODE_CYCLIC_12: if (acqCtl.cycleIdx < acqCtl.cniCount) cni = acqCtl.cniTab[acqCtl.cycleIdx]; else cni = 0; break; case ACQMODE_NETWORK: case ACQMODE_EXTERNAL: case ACQMODE_PASSIVE: default: cni = 0; break; } return cni; } // --------------------------------------------------------------------------- // Switch database and TV channel to the current acq provider & handle errors // - called upon start of acquisition // - called when UI db is changed by user, or merge started // - called when acq mode is changed by the user // - called after end of a cycle phase for one db // static bool EpgAcqCtl_UpdateProvider( bool changeDb ) { EPGDB_CONTEXT * pPeek; uint freq = 0; uint cni; bool warnInputError; bool dbChanged, inputChanged, dbInitial; bool result = TRUE; dbChanged = inputChanged = dbInitial = FALSE; // in network acq mode the db is selected by the server only if (acqCtl.mode != ACQMODE_NETWORK) { // determine current CNI depending on acq mode and index cni = EpgAcqCtl_GetProvider(); // for acq start: open the requested db (or empty database for passive or network mode) if (pAcqDbContext == NULL) { dbInitial = TRUE; acqContextIsPeek = FALSE; if (cni == 0) pAcqDbContext = EpgContextCtl_OpenDummy(); else pAcqDbContext = EpgContextCtl_Open(cni, CTX_FAIL_RET_DUMMY, CTX_RELOAD_ERR_NONE); } if (cni != 0) { if ( (changeDb == FALSE) && (acqCtl.state == ACQSTATE_WAIT_BI) && (pAcqDbContext->modified == FALSE)) { changeDb = TRUE; } warnInputError = FALSE; if (cni == EpgDbContextGetCni(pAcqDbContext)) { freq = UiControlMsg_QueryProvFreq(cni); if (freq == 0) freq = pAcqDbContext->tunerFreq; warnInputError = TRUE; } else { // query the rc/ini file for a frequency for this provider freq = UiControlMsg_QueryProvFreq(cni); if ( changeDb ) { // switch to the new database EpgAcqCtl_CloseDb(); pAcqDbContext = EpgContextCtl_Open(cni, CTX_FAIL_RET_DUMMY, ((freq == 0) ? CTX_RELOAD_ERR_ACQ : CTX_RELOAD_ERR_ANY)); dbChanged = TRUE; if (EpgDbContextGetCni(pAcqDbContext) == cni) warnInputError = TRUE; if (freq == 0) freq = pAcqDbContext->tunerFreq; acqCtl.dumpTime = time(NULL); } else if (freq == 0) { // query the database file for a frequency for this provider pPeek = EpgContextCtl_Peek(cni, CTX_RELOAD_ERR_ACQ); if (pPeek != NULL) { freq = pPeek->tunerFreq; EpgContextCtl_ClosePeek(pPeek); warnInputError = TRUE; } else debug1("EpgAcqCtl-UpdateProvider: peek for 0x%04X failed", cni); } } if ( (acqCtl.mode != ACQMODE_PASSIVE) && ((acqCtl.mode != ACQMODE_FORCED_PASSIVE) || (acqCtl.passiveReason != ACQPASSIVE_NO_TUNER)) ) { result = EpgAcqCtl_TuneProvider(freq, (warnInputError ? cni: 0)); inputChanged = ((acqCtl.mode != ACQMODE_FORCED_PASSIVE) || (acqCtl.passiveReason != ACQPASSIVE_ACCESS_DEVICE)); } } else { // no provider to be tuned onto -> set at least the input source if (acqCtl.mode != ACQMODE_PASSIVE) { result = EpgAcqCtl_TuneProvider(0, 0); inputChanged = ((acqCtl.mode != ACQMODE_FORCED_PASSIVE) || (acqCtl.passiveReason != ACQPASSIVE_ACCESS_DEVICE)); } } if ((dbChanged || inputChanged) && !dbInitial) { // acq database and/or input channel/frequency have been changed // -> it's MANDATORY to reset all acq state machines or data from // a wrong provider might be added to the database if (acqCtl.state != ACQSTATE_OFF) { // reset acquisition: check provider CNI of next AI block EpgAcqCtl_ChannelChange(FALSE); EpgAcqCtl_StatisticsUpdate(); } UiControlMsg_AcqEvent(ACQ_EVENT_PROV_CHANGE); } #ifdef USE_DAEMON // inform server module about the current provider EpgAcqServer_SetProvider(EpgDbContextGetCni(pAcqDbContext)); #endif } return result; } // --------------------------------------------------------------------------- // Initialize the cyclic acquisition state machine // - has to be called during acq start or after acq mode change // - has to be called before UpdateProvider, because the cycle index must be initialized first // static void EpgAcqCtl_InitCycle( void ) { // reset forced-passive state; will be set upon errors in UpdateProv func acqCtl.mode = acqCtl.userMode; acqCtl.passiveReason = ACQPASSIVE_NONE; switch (acqCtl.mode) { case ACQMODE_CYCLIC_012: case ACQMODE_CYCLIC_02: acqCtl.cyclePhase = ACQMODE_PHASE_NOWNEXT; break; case ACQMODE_CYCLIC_12: acqCtl.cyclePhase = ACQMODE_PHASE_STREAM1; break; case ACQMODE_FOLLOW_UI: case ACQMODE_FOLLOW_MERGED: case ACQMODE_CYCLIC_2: acqCtl.cyclePhase = ACQMODE_PHASE_STREAM2; break; case ACQMODE_PASSIVE: case ACQMODE_EXTERNAL: case ACQMODE_NETWORK: case ACQMODE_FORCED_PASSIVE: default: // phase value not used in these modes; set arbritrary value acqCtl.cyclePhase = ACQMODE_PHASE_STREAM2; break; } // limit to max phase (because acq. will stop upon advance from this phase) if (acqCtl.cyclePhase > acqCtl.stopPhase) { acqCtl.cyclePhase = acqCtl.stopPhase; } // use the first provider in the list acqCtl.cycleIdx = 0; } // --------------------------------------------------------------------------- // Determine if netwop coverage variance is stable enough // static bool EpgAcqCtl_CheckVarianceStable( uint streamIdx ) { EPGDB_VAR_HIST * pHist; double min, max; uint idx; bool result = FALSE; if (streamIdx < 2) { pHist = acqStats.varianceHist + streamIdx; if (pHist->count >= VARIANCE_HIST_COUNT) { // get min and max variance from the history list min = max = pHist->buf[0]; for (idx=1; idx < VARIANCE_HIST_COUNT; idx++) { if (pHist->buf[idx] < min) min = pHist->buf[idx]; if (pHist->buf[idx] > max) max = pHist->buf[idx]; } // variance slope has to be below threshold result = ((max - min) <= MAX_CYCLE_VAR_DIFF); } } else fatal1("EpgAcqCtl-CheckVarianceStable: invalid stream index %d", streamIdx); return result; } // --------------------------------------------------------------------------- // Advance the cycle phase // static void EpgAcqCtl_AdvanceCyclePhase( bool forceAdvance ) { time_t now = time(NULL); double quote; bool advance, wrongCni; uint cni; wrongCni = FALSE; if ( !ACQMODE_IS_PASSIVE(acqCtl.mode) && (acqCtl.mode != ACQMODE_EXTERNAL) && !((acqCtl.mode == ACQMODE_FORCED_PASSIVE) && (acqCtl.passiveReason != ACQPASSIVE_ACCESS_DEVICE)) && (EpgScan_IsActive() == FALSE) ) { cni = EpgAcqCtl_GetProvider(); if ((cni != 0) && (cni != EpgDbContextGetCni(pAcqDbContext))) { // not the expected provider -> try to switch EpgAcqCtl_UpdateProvider(FALSE); wrongCni = TRUE; } } if ( ((acqCtl.state == ACQSTATE_RUNNING) || forceAdvance) && (wrongCni == FALSE) && (EpgScan_IsActive() == FALSE) && ( ACQMODE_IS_CYCLIC(acqCtl.mode) || (acqCtl.stopPhase != ACQMODE_PHASE_COUNT) || ((acqCtl.mode == ACQMODE_FORCED_PASSIVE) && ACQMODE_IS_CYCLIC(acqCtl.userMode) && (acqCtl.passiveReason == ACQPASSIVE_ACCESS_DEVICE)) ) && ( (acqCtl.cniCount > 1) || (acqCtl.stopPhase != ACQMODE_PHASE_COUNT) ) ) { // determine if acq for the current phase is complete // note: all criteria must have a decent timeout to catch abnormal cases switch (acqCtl.cyclePhase) { case ACQMODE_PHASE_NOWNEXT: advance = (acqStats.nowNextMaxAcqRepCount >= 2) || ((acqStats.nowNextMaxAcqRepCount == 0) && (acqStats.ai.aiCount >= NOWNEXT_TIMEOUT_AI_COUNT)); advance |= (now >= acqStats.acqStartTime + NOWNEXT_TIMEOUT); break; case ACQMODE_PHASE_STREAM1: quote = ((acqStats.count[0].ai > 0) ? ((double)acqStats.count[0].sinceAcq / acqStats.count[0].ai) : 100.0); advance = (quote >= MIN_CYCLE_QUOTE) && (acqStats.count[0].variance < MIN_CYCLE_VARIANCE) && (EpgAcqCtl_CheckVarianceStable(0) || (acqStats.count[0].variance == 0.0)); advance |= ((acqStats.count[0].avgAcqRepCount >= MAX_CYCLE_ACQ_REP_COUNT) || (acqStats.count[1].avgAcqRepCount >= MAX_CYCLE_ACQ_REP_COUNT)); advance |= (now >= acqStats.acqStartTime + STREAM1_TIMEOUT); break; case ACQMODE_PHASE_STREAM2: case ACQMODE_PHASE_MONITOR: quote = (((acqStats.count[0].ai + acqStats.count[1].ai) > 0) ? (((double)acqStats.count[0].sinceAcq + acqStats.count[1].sinceAcq) / ((double)acqStats.count[0].ai + acqStats.count[1].ai)) : 100.0); advance = (quote >= MIN_CYCLE_QUOTE) && (acqStats.count[0].variance < MIN_CYCLE_VARIANCE) && (acqStats.count[1].variance < MIN_CYCLE_VARIANCE) && EpgAcqCtl_CheckVarianceStable(0) && EpgAcqCtl_CheckVarianceStable(1); advance |= (acqStats.count[1].avgAcqRepCount >= MAX_CYCLE_ACQ_REP_COUNT); advance |= (now >= acqStats.acqStartTime + STREAM2_TIMEOUT); break; default: SHOULD_NOT_BE_REACHED; advance = TRUE; break; } if (advance || forceAdvance) { // determine if all databases were covered in the current phase if (acqCtl.cycleIdx + 1 < acqCtl.cniCount) { acqCtl.cycleIdx += 1; dprintf2("EpgAcqCtl: advance to CNI #%d of %d\n", acqCtl.cycleIdx, acqCtl.cniCount); } else if (acqCtl.cyclePhase >= acqCtl.stopPhase) { // max. phase reached -> stop acq. EpgAcqCtl_Stop(); } else { // phase complete for all databases -> switch to next // determine next phase switch (acqCtl.userMode) { case ACQMODE_CYCLIC_012: if (acqCtl.cyclePhase < ACQMODE_PHASE_MONITOR) acqCtl.cyclePhase += 1; break; case ACQMODE_CYCLIC_02: if (acqCtl.cyclePhase == ACQMODE_PHASE_NOWNEXT) acqCtl.cyclePhase = ACQMODE_PHASE_STREAM2; else acqCtl.cyclePhase = ACQMODE_PHASE_MONITOR; break; case ACQMODE_CYCLIC_12: if (acqCtl.cyclePhase == ACQMODE_PHASE_STREAM1) acqCtl.cyclePhase = ACQMODE_PHASE_STREAM2; else acqCtl.cyclePhase = ACQMODE_PHASE_MONITOR; break; case ACQMODE_FOLLOW_MERGED: case ACQMODE_CYCLIC_2: acqCtl.cyclePhase = ACQMODE_PHASE_MONITOR; break; case ACQMODE_FORCED_PASSIVE: default: break; } dprintf1("EpgAcqCtl: advance to phase #%d\n", acqCtl.cyclePhase); // restart with first database acqCtl.cycleIdx = 0; } if (acqCtl.state != ACQSTATE_OFF) { // note about parameter "TRUE": open the new db immediately because // it may need to be accessed, e.g. to reset rep counters EpgAcqCtl_UpdateProvider(TRUE); } } } assert((acqCtl.userMode != ACQMODE_FORCED_PASSIVE) && (acqCtl.userMode < ACQMODE_COUNT)); assert((acqCtl.mode != ACQMODE_FORCED_PASSIVE) || (acqCtl.userMode != ACQMODE_PASSIVE)); } // --------------------------------------------------------------------------- // Check the tuner device status // - tune the frequency of the current provider once again // - this is called by the GUI when start of a TV application is detected. // it's used to quickly update the forced-passive state. // bool EpgAcqCtl_CheckDeviceAccess( void ) { bool result; if ( (acqCtl.state != ACQSTATE_OFF) && (acqCtl.mode != ACQMODE_PASSIVE) && (EpgScan_IsActive() == FALSE) ) { result = EpgAcqCtl_UpdateProvider(FALSE); if (result == FALSE) UiControlMsg_AcqEvent(ACQ_EVENT_STATS_UPDATE); dprintf1("EpgAcqCtl-CheckDeviceAccess: device is now %s\n", result ? "free" : "busy"); } else result = FALSE; return result; } // --------------------------------------------------------------------------- // Select acquisition mode and provider list // bool EpgAcqCtl_SelectMode( EPGACQ_MODE newAcqMode, EPGACQ_PHASE maxPhase, uint cniCount, const uint * pCniTab ) { bool restart; bool result = FALSE; if (newAcqMode < ACQMODE_COUNT) { if ( !ACQMODE_IS_CYCLIC(newAcqMode) || ((cniCount > 0) && (pCniTab != NULL)) ) { result = TRUE; // special handling for merged database: more than one provider despite follow-ui mode if ((newAcqMode == ACQMODE_FOLLOW_UI) && (cniCount > 1)) { newAcqMode = ACQMODE_FOLLOW_MERGED; } if (cniCount > MAX_MERGED_DB_COUNT) cniCount = MAX_MERGED_DB_COUNT; if ( (newAcqMode != acqCtl.mode) && (newAcqMode != ACQMODE_NETWORK) ) { acqCtl.haveWarnedInpSrc = FALSE; } // check if the same parameters are already set -> if yes skip // note: compare actual mode instead of user mode, to reset if acq is stalled if ( (newAcqMode != acqCtl.mode) || (maxPhase != acqCtl.stopPhase) || (cniCount != acqCtl.cniCount) || (memcmp(acqCtl.cniTab, pCniTab, cniCount * sizeof(*pCniTab)) != 0) ) { // if network mode was toggled, acq must be completely restarted because different "drivers" are used restart = ( ((newAcqMode == ACQMODE_NETWORK) ^ (acqCtl.userMode == ACQMODE_NETWORK)) && (acqCtl.state != ACQSTATE_OFF) ); if (restart) EpgAcqCtl_Stop(); // lock automatic database dump for network mode (only the server may write the db) EpgContextCtl_LockDump(newAcqMode == ACQMODE_NETWORK); // save the new parameters acqCtl.cniCount = cniCount; acqCtl.userMode = newAcqMode; acqCtl.mode = newAcqMode; acqCtl.stopPhase = maxPhase; memcpy(acqCtl.cniTab, pCniTab, sizeof(acqCtl.cniTab)); // reset acquisition and start with the new parameters if (acqCtl.state != ACQSTATE_OFF) { dprintf3("EpgAcqCtl-SelectMode: reset acq with new params: mode=%d, CNI count=%d, CNI#0=%04X\n", newAcqMode, cniCount, pCniTab[0]); if (acqCtl.mode != ACQMODE_NETWORK) { EpgAcqCtl_InitCycle(); result = EpgAcqCtl_UpdateProvider(TRUE); } else { // send the provider list to the acquisition daemon // upgrade/downgrade current acq db from/to peek if now/not-anymore in GUI list uint acqCni = EpgDbContextGetCni(pAcqDbContext); if (acqCni != 0) EpgAcqCtl_SwitchDb(acqCni); #ifdef USE_DAEMON EpgAcqClient_ChangeProviders(pCniTab, cniCount); #endif } } else if (restart) { dprintf3("EpgAcqCtl-SelectMode: restart acq with new params: mode=%d, CNI count=%d, CNI#0=%04X\n", cniCount, newAcqMode, pCniTab[0]); EpgAcqCtl_Start(); } // if there was no provider change, at least notify about acq state change UiControlMsg_AcqEvent(ACQ_EVENT_STATS_UPDATE); } } else debug2("EpgAcqCtl-SelectMode: cyclic mode without provider list (count=%d,ptr=0x%lx)", cniCount, (ulong)pCniTab); } else debug1("EpgAcqCtl-SelectMode: called with illegal acq mode: %d", newAcqMode); return result; } // --------------------------------------------------------------------------- // TV card config: select input source and slicer type // - distinguishing tuner/external is important to acq control, because // + when a tuner is input source, the channel can be controlled // + else only passive acq mode is possible // note: parameter is not used in passive and network acq modes // - slicer type is passed through to driver, except for "automatic" // bool EpgAcqCtl_SetInputSource( uint inputIdx, uint slicerType ) { bool result; if (acqCtl.inputSource != inputIdx) acqCtl.haveWarnedInpSrc = FALSE; acqCtl.inputSource = inputIdx; // save slicer type; if automatic start with "trivial" if (slicerType == VBI_SLICER_AUTO) { acqCtl.autoSlicerType = TRUE; acqCtl.currentSlicerType = VBI_SLICER_TRIVIAL; } else { acqCtl.autoSlicerType = FALSE; acqCtl.currentSlicerType = slicerType; } if ( (acqCtl.state != ACQSTATE_OFF) && (acqCtl.mode != ACQMODE_PASSIVE) && (acqCtl.mode != ACQMODE_NETWORK) ) { if (acqCtl.autoSlicerType == FALSE) { BtDriver_SelectSlicer(acqCtl.currentSlicerType); } EpgAcqCtl_InitCycle(); result = EpgAcqCtl_UpdateProvider(TRUE); // if there was no provider change, at least notify about acq state change UiControlMsg_AcqEvent(ACQ_EVENT_STATS_UPDATE); } else result = TRUE; return result; } // --------------------------------------------------------------------------- // Determine state of acquisition for user information // - in network acq mode it returns info about the remove acq process // void EpgAcqCtl_DescribeAcqState( EPGACQ_DESCR * pAcqState ) { time_t lastAi, now; if (EpgScan_IsActive()) { memset(pAcqState, 0, sizeof(EPGACQ_DESCR)); pAcqState->state = ACQDESCR_SCAN; } else if (acqCtl.state == ACQSTATE_OFF) { memset(pAcqState, 0, sizeof(EPGACQ_DESCR)); pAcqState->state = ACQDESCR_DISABLED; } else { if (acqCtl.mode != ACQMODE_NETWORK) { now = time(NULL); lastAi = EpgDbGetAiUpdateTime(pAcqDbContext); if (lastAi < acqStats.acqStartTime) lastAi = acqStats.acqStartTime; // check reception if (acqCtl.state != ACQSTATE_RUNNING) { if (now - lastAi > ACQ_DESCR_STALLED_TIMEOUT) { if (acqCtl.state >= ACQSTATE_WAIT_AI) pAcqState->state = ACQDESCR_DEC_ERRORS; else pAcqState->state = ACQDESCR_NO_RECEPTION; } else pAcqState->state = ACQDESCR_STARTING; } else { if (now - lastAi > ACQ_DESCR_STALLED_TIMEOUT) pAcqState->state = ACQDESCR_STALLED; else pAcqState->state = ACQDESCR_RUNNING; } pAcqState->mode = acqCtl.mode; pAcqState->passiveReason = acqCtl.passiveReason; pAcqState->cyclePhase = acqCtl.cyclePhase; pAcqState->cniCount = acqCtl.cniCount; pAcqState->dbCni = EpgDbContextGetCni(pAcqDbContext); pAcqState->cycleCni = EpgAcqCtl_GetProvider(); pAcqState->isNetAcq = FALSE; } else { // network acq mode -> return info forwarded by acq daemon #ifdef USE_DAEMON // if state is set to INVALID (zero) caller should assume network state "loading db" // because it takes a short while after loading the db until the first stats report // is available memcpy(pAcqState, &acqNetDescr, sizeof(*pAcqState)); pAcqState->isLocalServer = EpgAcqClient_IsLocalServer(); pAcqState->isNetAcq = TRUE; #endif } } } // --------------------------------------------------------------------------- // AI callback: invoked before a new AI block is inserted into the database // static bool EpgAcqCtl_AiCallback( const AI_BLOCK *pNewAi ) { EPGACQ_STATE oldState; const AI_BLOCK *pOldAi; uint ai_cni; bool accept = FALSE; assert(EpgScan_IsActive() == FALSE); // EPG scan uses a separate callback oldState = acqCtl.state; if (AI_GET_CNI(pNewAi) == 0) { debug0("EpgAcqCtl-AiCallback: AI with illegal CNI 0 rejected"); accept = FALSE; } else if (acqCtl.state == ACQSTATE_WAIT_BI) { dprintf2("EpgCtl: AI rejected in state WAIT-BI: CNI=0x%04X db-CNI=0x%04X\n", AI_GET_CNI(pNewAi), ((pAcqDbContext->pAiBlock != NULL) ? AI_GET_CNI(&pAcqDbContext->pAiBlock->blk.ai) : 0)); accept = FALSE; } else if ((acqCtl.state == ACQSTATE_WAIT_AI) || (acqCtl.state == ACQSTATE_RUNNING)) { DBGONLY(if (acqCtl.state == ACQSTATE_WAIT_AI)) dprintf3("EpgCtl: AI found, CNI=0x%04X version %d/%d\n", AI_GET_CNI(pNewAi), pNewAi->version, pNewAi->version_swo); EpgDbLockDatabase(pAcqDbContext, TRUE); pOldAi = EpgDbGetAi(pAcqDbContext); ai_cni = AI_GET_CNI(pNewAi); if (pOldAi == NULL) { // the current db is empty EpgDbLockDatabase(pAcqDbContext, FALSE); // this is just like a provider change... EpgAcqCtl_SwitchDb(ai_cni); dprintf2("EpgAcqCtl: empty acq db, AI found: CNI 0x%04X (%s)\n", ai_cni, ((EpgDbContextGetCni(pAcqDbContext) == 0) ? "new" : "reload ok")); #ifdef USE_DAEMON // inform server module about the current provider EpgAcqServer_SetProvider(ai_cni); #endif // store the tuner frequency if none is known yet if ( (pAcqDbContext->tunerFreq == 0) && (acqCtl.mode != ACQMODE_NETWORK) ) { uint freq, chan; bool isTuner; // try to obtain the frequency from the driver (not supported by all drivers) if ( BtDriver_QueryChannel(&freq, &chan, &isTuner) && (isTuner) && (freq != 0) ) { // store the provider channel frequency in the rc/ini file debug2("EpgAcqCtl: setting current tuner frequency %.2f for CNI 0x%04X", (double)(freq & 0xffffff)/16, ai_cni); pAcqDbContext->tunerFreq = freq; UiControlMsg_NewProvFreq(ai_cni, freq); } } // new provider -> dump asap if (EpgDbContextGetCni(pAcqDbContext) == 0) acqCtl.dumpTime = 0; acqCtl.state = ACQSTATE_RUNNING; acqStatsUpdate = TRUE; // update the ui netwop list and acq stats output if neccessary UiControlMsg_AcqEvent(ACQ_EVENT_PROV_CHANGE); accept = TRUE; } else { // already an AI in the database if (AI_GET_CNI(pOldAi) == AI_GET_CNI(pNewAi)) { if ( (pOldAi->version != pNewAi->version) || (pOldAi->version_swo != pNewAi->version_swo) ) { dprintf2("EpgAcqCtl: version number has changed, was: %d/%d\n", pOldAi->version, pOldAi->version_swo); UiControlMsg_AcqEvent(ACQ_EVENT_AI_VERSION_CHANGE); #ifdef USE_DAEMON // inform server module about the current provider EpgAcqServer_SetProvider(AI_GET_CNI(pNewAi)); #endif } else { // same AI version const AI_NETWOP *pOldNets, *pNewNets; uint netwop; // check if PI range of any network has changed pOldNets = AI_GET_NETWOPS(pOldAi); pNewNets = AI_GET_NETWOPS(pNewAi); for (netwop=0; netwop < pNewAi->netwopCount; netwop++) { if ( (pOldNets[netwop].startNo != pNewNets[netwop].startNo) || (pOldNets[netwop].stopNoSwo != pNewNets[netwop].stopNoSwo) ) { // new PI added at the end -> redraw timescales to mark ranges with missing PI UiControlMsg_AcqEvent(ACQ_EVENT_AI_PI_RANGE_CHANGE); #ifdef USE_DAEMON // inform server module about the current provider EpgAcqServer_SetProvider(AI_GET_CNI(pNewAi)); #endif break; } } // if no version change, trigger status update in the stats module if (netwop >= pNewAi->netwopCount) UiControlMsg_AcqEvent(ACQ_EVENT_STATS_UPDATE); } EpgDbLockDatabase(pAcqDbContext, FALSE); acqStatsUpdate = TRUE; acqCtl.state = ACQSTATE_RUNNING; accept = TRUE; } else { // wrong provider -> switch acq database dprintf2("EpgAcqCtl: switching acq db from %04X to %04X\n", AI_GET_CNI(pOldAi), ai_cni); EpgDbLockDatabase(pAcqDbContext, FALSE); EpgAcqCtl_SwitchDb(ai_cni); if (acqCtl.mode != ACQMODE_NETWORK) { if (acqCtl.state == ACQSTATE_RUNNING) { // should normally not happen, because channel changes are detected by TTX page header supervision debug1("EpgAcqCtl: unexpected prov change to CNI %04X - resetting EPG Acq", ai_cni); EpgAcqCtl_TtxReset(pAcqDbContext, &acqDbQueue, EPG_ILLEGAL_PAGENO, EPG_ILLEGAL_APPID); // if db is new, dump asap if (EpgDbContextGetCni(pAcqDbContext) == 0) acqCtl.dumpTime = 0; else acqCtl.dumpTime = time(NULL); acqCtl.state = ACQSTATE_WAIT_BI; } else acqCtl.state = ACQSTATE_RUNNING; } #ifdef USE_DAEMON // inform server module about the current provider EpgAcqServer_SetProvider(ai_cni); #endif if (acqCtl.state != ACQSTATE_RUNNING) EpgAcqCtl_StatisticsReset(); else acqStatsUpdate = TRUE; UiControlMsg_AcqEvent(ACQ_EVENT_PROV_CHANGE); accept = TRUE; } // Store the TV tuner frequency if none is known yet // (Note: the tuner frequency in the db is never overwritten because we can // not be 100% sure that we know the current TV tuner freq here, esp. if it's // coming from a remote TV application; only the EPG scan updates the freq.) if ( (pAcqDbContext->tunerFreq == 0) && (oldState == ACQSTATE_WAIT_AI) && (accept) && (acqCtl.state == ACQSTATE_RUNNING) && (acqCtl.mode != ACQMODE_NETWORK) ) { uint freq, chan; bool isTuner; // try to obtain the frequency from the driver (not supported by all drivers) if ( BtDriver_QueryChannel(&freq, &chan, &isTuner) && (isTuner) && (freq != 0) ) { // store the provider channel frequency in the rc/ini file debug2("EpgAcqCtl: setting current tuner frequency %.2f for CNI 0x%04X", (double)(freq & 0xffffff)/16, ai_cni); pAcqDbContext->tunerFreq = freq; UiControlMsg_NewProvFreq(ai_cni, freq); } } } // update teletext page number in the database if changed // note: store is delayed until AI reception to be sure the db matches the current stream if ( (acqCtl.currentPageNo != pAcqDbContext->pageNo) && VALID_EPG_PAGENO(acqCtl.currentPageNo) ) { pAcqDbContext->pageNo = acqCtl.currentPageNo; } } else assert(acqCtl.state != ACQSTATE_OFF); assert(EpgDbIsLocked(pAcqDbContext) == FALSE); return accept; } // --------------------------------------------------------------------------- // Called by the database management when a new BI block was received // - the BI block is never inserted into the database // - only the application ID is extracted and saved in the db context // - in network acq mode the BI state is skipped -> function not used // static bool EpgAcqCtl_BiCallback( const BI_BLOCK *pNewBi ) { assert(EpgScan_IsActive() == FALSE); // EPG scan uses a separate callback assert(acqCtl.mode != ACQMODE_NETWORK); // callback not used by the dbsrv module if (pNewBi->app_id == EPG_ILLEGAL_APPID) { dprintf0("EpgCtl-BiCallback: EPG not listed in BI\n"); } else { if (pAcqDbContext->appId != EPG_ILLEGAL_APPID) { if (pAcqDbContext->appId != pNewBi->app_id) { // not the default id dprintf2("EpgCtl: app-ID changed from %d to %d\n", pAcqDbContext->appId, pAcqDbContext->appId); EpgStreamClear(); EpgStreamInit(&acqDbQueue, TRUE, pNewBi->app_id, pAcqDbContext->pageNo); } } else dprintf1("EpgAcqCtl-BiCallback: BI received, appID=%d\n", pNewBi->app_id); switch (acqCtl.state) { case ACQSTATE_WAIT_BI: dprintf1("EpgCtl: BI found, appId=%d\n", pNewBi->app_id); acqCtl.state = ACQSTATE_WAIT_AI; // app-ID can not be accepted until CNI in the following AI block is checked break; case ACQSTATE_WAIT_AI: // app-ID can not be accepted until CNI in the following AI block is checked break; case ACQSTATE_RUNNING: pAcqDbContext->appId = pNewBi->app_id; break; default: SHOULD_NOT_BE_REACHED; break; } } // the BI block is never added to the db return FALSE; } // --------------------------------------------------------------------------- // Notification from acquisition about error // static void EpgAcqCtl_Stopped( void ) { if (acqCtl.state != ACQSTATE_OFF) { if (acqCtl.mode == ACQMODE_NETWORK) { assert(EpgDbQueue_GetBlockCount(&acqDbQueue) == 0); // cleared in acq clnt if (acqCtl.haveWarnedInpSrc == FALSE) { // error during initial connect -> stop acq and inform user EpgAcqCtl_Stop(); UiControlMsg_NetAcqError(); acqCtl.haveWarnedInpSrc = TRUE; } else { // error occured while connected -> try to reconnect periodically EpgAcqCtl_StatisticsReset(); acqStatsUpdate = FALSE; acqCtl.state = ACQSTATE_WAIT_AI; acqNetDescr.state = ACQDESCR_DISABLED; UiControlMsg_AcqEvent(ACQ_EVENT_STATS_UPDATE); } } else { // stop acquisition EpgAcqCtl_Stop(); } } } // --------------------------------------------------------------------------- // Handle channel changes // - called both when the TTX decoder detects an external channel change // and when a channel is tuned internally. // - changeDb: TRUE to fall back to the default db (ui db) // static void EpgAcqCtl_ChannelChange( bool changeDb ) { uint uiCni; assert(EpgScan_IsActive() == FALSE); // EPG scan uses a separate callback assert(acqCtl.mode != ACQMODE_NETWORK); // callback not used by the dbsrv module if (acqCtl.state != ACQSTATE_OFF) { dprintf4("EpgAcqCtl-ChannelChange: reset acq for db 0x%04X (0x%lx) modified=%d changedb=%d\n", ((pAcqDbContext->pAiBlock != NULL) ? AI_GET_CNI(&pAcqDbContext->pAiBlock->blk.ai) : 0), (long)pAcqDbContext, pAcqDbContext->modified, changeDb); acqCtl.chanChangeTime = time(NULL); if ( changeDb ) { if ( ((uiCni = EpgDbContextGetCni(pUiDbContext)) != 0) && (uiCni != EpgDbContextGetCni(pAcqDbContext)) && (EpgDbContextIsMerged(pUiDbContext) == FALSE) ) { // close acq db and fall back to ui db EpgAcqCtl_CloseDb(); acqContextIsPeek = FALSE; pAcqDbContext = EpgContextCtl_Open(uiCni, CTX_FAIL_RET_DUMMY, CTX_RELOAD_ERR_ANY); #ifdef USE_DAEMON // inform server module about the current provider EpgAcqServer_SetProvider(EpgDbContextGetCni(pAcqDbContext)); #endif // notify GUI about state change UiControlMsg_AcqEvent(ACQ_EVENT_PROV_CHANGE); } else UiControlMsg_AcqEvent(ACQ_EVENT_STATS_UPDATE); } EpgAcqCtl_TtxReset(pAcqDbContext, &acqDbQueue, EPG_ILLEGAL_PAGENO, EPG_ILLEGAL_APPID); acqCtl.state = ACQSTATE_WAIT_BI; EpgAcqCtl_StatisticsReset(); } } // ---------------------------------------------------------------------------- // Poll VPS/PDC for channel changes // - invoked when local client requests VPS/PDC data // - called every 250 ms from daemon main loop // - if CNI or PIL changes, it's forwarded to connected clients (if requested) // bool EpgAcqCtl_ProcessVps( void ) { uint newCni, newPil; bool change = FALSE; bool update = FALSE; if ( (acqCtl.state != ACQSTATE_OFF) && (EpgScan_IsActive() == FALSE) && (acqCtl.mode != ACQMODE_NETWORK) ) { if (TtxDecode_GetCniAndPil(&newCni, &newPil, NULL)) { if ( (acqStats.vpsPdc.cni != newCni) || ((newPil != acqStats.vpsPdc.pil) && VPS_PIL_IS_VALID(newPil)) ) { acqStats.vpsPdc.pil = newPil; acqStats.vpsPdc.cni = newCni; dprintf5("EpgAcqCtl-PollVpsPil: %02d.%02d. %02d:%02d (0x%04X)\n", (newPil >> 15) & 0x1F, (newPil >> 11) & 0x0F, (newPil >> 6) & 0x1F, (newPil ) & 0x3F, newCni ); UiControlMsg_AcqEvent(ACQ_EVENT_VPS_PDC); change = TRUE; } #ifdef USE_DAEMON // inform server module about the current CNI and PIL (even if no change) EpgAcqServer_SetVpsPdc(change); #endif update = TRUE; } } return update; } // --------------------------------------------------------------------------- // Periodic check if the acquisition process is still alive // - called from main loop // void EpgAcqCtl_Idle( void ) { time_t now = time(NULL); if ( (acqCtl.state != ACQSTATE_OFF) && (EpgScan_IsActive() == FALSE) && (acqCtl.mode != ACQMODE_NETWORK) ) { // periodic db dump if (acqCtl.state == ACQSTATE_RUNNING) { if (pAcqDbContext->modified) { if (now >= acqCtl.dumpTime + EPGACQCTL_DUMP_INTV) { EpgDbLockDatabase(pAcqDbContext, TRUE); if ( EpgDbDump(pAcqDbContext) ) { dprintf1("EpgAcqCtl-Idle: dumped db %04X to file\n", EpgDbContextGetCni(pAcqDbContext)); acqCtl.dumpTime = now; } EpgDbLockDatabase(pAcqDbContext, FALSE); } } } // if acq running, but no reception -> wait for timeout, then reset acq // but make sure the channel isn't changed too often (need to wait at least 10 secs for AI) if ( (now >= acqCtl.dumpTime + EPGACQCTL_MODIF_INTV) && (now >= acqCtl.chanChangeTime + EPGACQCTL_MODIF_INTV) ) { dprintf1("EpgAcqCtl-Idle: no reception from provider 0x%04X - reset acq\n", EpgDbContextGetCni(pAcqDbContext)); // use user-configured mode, because the actual mode might be forced-passive if (ACQMODE_IS_CYCLIC(acqCtl.userMode)) { // no reception -> advance cycle EpgAcqCtl_AdvanceCyclePhase(TRUE); } else if (acqCtl.userMode == ACQMODE_FOLLOW_UI) { EpgAcqCtl_UpdateProvider(FALSE); } else { EpgAcqCtl_ChannelChange(TRUE); EpgAcqCtl_StatisticsUpdate(); } } assert(acqStatsUpdate == FALSE); } } // --------------------------------------------------------------------------- // Process TTX packets in the VBI ringbuffer // - called about once a second (most providers send one TTX page with EPG data // per second); assembled EPG blocks are queued // - in network acq mode incoming blocks are read asynchronously (select(2) on // network socket); this func only checks if blocks are already in the queue // - also handles error indications: acq stop, channel change, EPG param update // bool EpgAcqCtl_ProcessPackets( void ) { uint pageNo; bool stopped; time_t now; bool result = FALSE; if (pAcqDbContext != NULL) { if (acqCtl.mode != ACQMODE_NETWORK) { // query if VBI device has been freed by higher-prio users if (BtDriver_QueryChannelToken()) EpgAcqCtl_CheckDeviceAccess(); // check if new data is available in the VBI ringbuffer if (TtxDecode_CheckForPackets(&stopped)) { // assemble VBI lines, i.e. TTX packets to EPG blocks and put them in the queue if (EpgStreamProcessPackets() == FALSE) { // channel change detected -> restart acq for the current provider dprintf0("EpgAcqCtl: uncontrolled channel change detected\n"); EpgAcqCtl_ChannelChange(TRUE); EpgAcqCtl_StatisticsUpdate(); } // check if the current slicer type is adequate if ( (acqCtl.autoSlicerType) && (acqCtl.currentSlicerType + 1 < VBI_SLICER_COUNT) ) { now = time(NULL); if ( (now >= acqCtl.chanChangeTime + (acqCtl.currentSlicerType - VBI_SLICER_TRIVIAL + 1) * EPGACQCTL_SLICER_INTV) && (EpgStreamCheckSlicerQuality() == FALSE) ) { debug4("EpgAcqCtl: upgrading slicer type to #%d after %d secs (provider #%d of %d)", acqCtl.currentSlicerType + 1, (int)(now - acqCtl.chanChangeTime), acqCtl.cycleIdx, acqCtl.cniCount); acqCtl.currentSlicerType += 1; BtDriver_SelectSlicer(acqCtl.currentSlicerType); } } result = TRUE; } if (stopped == FALSE) { // check the MIP if EPG is transmitted on a different page pageNo = TtxDecode_GetMipPageNo(); if ((pageNo != EPG_ILLEGAL_PAGENO) && (pageNo != pAcqDbContext->pageNo)) { // found a different page number in MIP dprintf2("EpgAcqCtl: non-default MIP page no for EPG: %03X (was %03X) -> restart acq\n", pageNo, pAcqDbContext->pageNo); EpgAcqCtl_TtxReset(pAcqDbContext, &acqDbQueue, pageNo, EPG_ILLEGAL_APPID); } } else { // acquisition was stopped, e.g. due to death of the acq slave EpgAcqCtl_Stopped(); } } else { // network acq mode -> handle timeouts; check for EPG blocks in input queue #ifdef USE_DAEMON stopped = EpgAcqClient_CheckTimeouts(); if (stopped) { // an error has occurred in the connection to the server EpgAcqCtl_Stopped(); } // block queue processing is triggered by the socket handler in the main module // so we never trigger it here and always return FALSE result = FALSE; #endif } } // return TRUE if blocks are in queue -> caller must schedule block insertion return result; } // --------------------------------------------------------------------------- // Insert newly acquired blocks into the EPG db // - to be called when ProcessPackets returns TRUE, i.e. EPG blocks in input queue // - database MUST NOT be locked by GUI // void EpgAcqCtl_ProcessBlocks( void ) { const EPGDB_BLOCK * pBlock; bool overflow; if (pAcqDbContext != NULL) { assert(EpgDbIsLocked(pAcqDbContext) == FALSE); assert(acqStatsUpdate == FALSE); if (acqCtl.mode != ACQMODE_NETWORK) { if ( (acqCtl.state == ACQSTATE_WAIT_BI) || (acqCtl.state == ACQSTATE_WAIT_AI) ) { // still waiting for the initial BI block EpgDbProcessQueueByType(&pAcqDbContext, &acqDbQueue, BLOCK_TYPE_BI, &epgQueueCb); } if (acqCtl.state == ACQSTATE_WAIT_AI) { // BI received, but still waiting for the initial AI block EpgDbProcessQueueByType(&pAcqDbContext, &acqDbQueue, BLOCK_TYPE_AI, &epgQueueCb); } if (acqCtl.state == ACQSTATE_RUNNING) { // add all queued blocks to the db overflow = (EpgDbQueue_GetBlockCount(&acqDbQueue) >= EPG_QUEUE_OVERFLOW_LEN); if (overflow) overflow = UiControlMsg_AcqQueueOverflow(TRUE); if (acqTscEnabled) { // also add PI timescale info to the queue EpgDbProcessQueue(&pAcqDbContext, &acqDbQueue, &acqTsc, &epgQueueCb); if (EpgTscQueue_HasElems(&acqTsc)) UiControlMsg_AcqEvent(ACQ_EVENT_PI_ADDED); } else EpgDbProcessQueue(&pAcqDbContext, &acqDbQueue, NULL, &epgQueueCb); if (overflow) UiControlMsg_AcqQueueOverflow(FALSE); } // if new AI received, update statistics (after AI was inserted into db) if (acqStatsUpdate) { EpgAcqCtl_StatisticsUpdate(); EpgAcqCtl_AdvanceCyclePhase(FALSE); } else if (acqCtl.dumpTime == 0) { // immediate dump EpgDbLockDatabase(pAcqDbContext, TRUE); if ( EpgDbDump(pAcqDbContext) ) { dprintf1("EpgAcqCtl: dumped db %04X to file\n", EpgDbContextGetCni(pAcqDbContext)); acqCtl.dumpTime = time(NULL); } EpgDbLockDatabase(pAcqDbContext, FALSE); } } else { // network acq mode overflow = (EpgDbQueue_GetBlockCount(&acqDbQueue) >= EPG_QUEUE_OVERFLOW_LEN); if (overflow) overflow = UiControlMsg_AcqQueueOverflow(TRUE); if (acqCtl.state == ACQSTATE_WAIT_AI) { // fail-safe: check if the first block is a AI block while ( ((pBlock = EpgDbQueue_Peek(&acqDbQueue)) != NULL) && (pBlock->type != BLOCK_TYPE_AI) ) { debug1("EpgAcqCtl: illegal block type %d in queue while waiting for AI", pBlock->type); xfree((void *) EpgDbQueue_Get(&acqDbQueue)); } } EpgDbProcessQueue(&pAcqDbContext, &acqDbQueue, NULL, &epgQueueCb); if (overflow) UiControlMsg_AcqQueueOverflow(FALSE); #ifdef USE_DAEMON EpgAcqCtl_NetStatsUpdate(); #endif // statistics are sent automatically by the server, so no update required acqStatsUpdate = FALSE; // once connection is established, network errors are no longer reported by popups acqCtl.haveWarnedInpSrc = TRUE; if (EpgTscQueue_HasElems(&acqTsc)) { // PI timescale info has been received -> unlock new buffers and trigger GUI EpgTscQueue_UnlockBuffers(&acqTsc); UiControlMsg_AcqEvent(ACQ_EVENT_PI_ADDED); } } dprintf5("EpgAcqCtl: state=%d, phase=%d, CNI-idx=%d, cyCni=0x%04X, dbCni=0x%04X\n", acqCtl.state, acqCtl.cyclePhase, acqCtl.cycleIdx, acqCtl.cniTab[acqCtl.cycleIdx], EpgDbContextGetCni(pAcqDbContext)); } }