FreeDATA/freedata_server/schedule_manager.py

215 lines
9.2 KiB
Python

import sched
import time
import threading
import command_message_send
#from freedata_server.context import AppContext
#from message_system_db_manager import DatabaseManager
from message_system_db_messages import DatabaseManagerMessages
from message_system_db_beacon import DatabaseManagerBeacon
import explorer
import command_beacon
import structlog
from arq_session_irs import IRS_State
from arq_session_iss import ISS_State
class ScheduleManager:
"""Manages scheduled tasks for the FreeDATA modem.
This class schedules and executes various tasks related to the FreeDATA
modem, including checking for queued messages, publishing to the
explorer, transmitting beacons, cleaning up old beacons, and updating
transmission states. It uses the `sched` module for scheduling and
runs tasks in a separate thread.
"""
#def __init__(self, modem_version, config_manager, state_manger, event_manager):
def __init__(self, ctx):
"""Initializes the ScheduleManager.
Args:
modem_version (str): The version of the FreeDATA modem.
config_manager (ConfigManager): The configuration manager instance.
state_manger (StateManager): The state manager instance.
event_manager (EventManager): The event manager instance.
"""
self.log = structlog.get_logger("SCHEDULE_MANAGER")
self.ctx = ctx
self.scheduler = sched.scheduler(time.time, threading.Event().wait)
self.events = {
'check_for_queued_messages': {'function': self.check_for_queued_messages, 'interval': 15},
'explorer_publishing': {'function': self.push_to_explorer, 'interval': 60},
'transmitting_beacon': {'function': self.transmit_beacon, 'interval': 600},
'beacon_cleanup': {'function': self.delete_beacons, 'interval': 600},
'update_transmission_state': {'function': self.update_transmission_state, 'interval': 10},
}
self.running = False # Flag to control the running state
self.scheduler_thread = None # Reference to the scheduler thread
self.modem = None
def schedule_event(self, event_function, interval):
"""Schedules and executes a recurring event.
This method executes the given event function and then reschedules
it to run again after the specified interval, as long as the
ScheduleManager is still running.
Args:
event_function (function): The function to execute.
interval (int): The time interval between executions in seconds.
"""
event_function() # Execute the event function
if self.running: # Only reschedule if still running
self.scheduler.enter(interval, 1, self.schedule_event, (event_function, interval))
def start(self, modem):
"""Starts the scheduled tasks.
This method initializes and starts the scheduler in a separate
thread. It waits for a short period to allow the freedata_server to
initialize, retrieves the freedata_server instance, sets the running
flag, schedules the initial events, and starts the scheduler
thread.
Args:
modem: The FreeDATA modem instance.
"""
# wait some time for the modem to be ready
threading.Event().wait(timeout=0.1)
# get actual freedata_server instance
self.modem = modem
self.running = True # Set the running flag to True
for event_info in self.events.values():
# Schedule each event for the first time
self.scheduler.enter(0, 1, self.schedule_event, (event_info['function'], event_info['interval']))
# Run the scheduler in a separate thread
self.scheduler_thread = threading.Thread(target=self.scheduler.run, daemon=False)
self.scheduler_thread.start()
def stop(self):
"""Stops the scheduler and its associated thread.
This method stops the scheduler by setting the running flag to
False, canceling any pending scheduled events, and waiting for the
scheduler thread to finish. It logs messages indicating the
shutdown process.
"""
self.log.warning("[SHUTDOWN] stopping schedule manager....")
self.running = False # Clear the running flag to stop scheduling new events
# Clear scheduled events to break the scheduler out of its waiting state
for event in list(self.scheduler.queue):
self.scheduler.cancel(event)
# Wait for the scheduler thread to finish
if self.scheduler_thread:
self.scheduler_thread.join(1)
self.log.warning("[SHUTDOWN] schedule manager stopped")
def transmit_beacon(self):
"""Transmits a beacon signal.
This method transmits a beacon signal if beacon transmission is
enabled, the freedata_server is running, and no ARQ transmission is in
progress. It handles potential exceptions during beacon
transmission.
"""
try:
if not self.ctx.state_manager.getARQ() and self.ctx.state_manager.is_beacon_running and self.ctx.state_manager.is_modem_running:
cmd = command_beacon.BeaconCommand(self.ctx)
cmd.run()
except Exception as e:
print(e)
def delete_beacons(self):
"""Deletes old beacon records from the database.
This method periodically cleans up old beacon records from the
database that are older than two days. It handles potential
exceptions during the cleanup process.
"""
try:
DatabaseManagerBeacon(self.ctx).beacon_cleanup_older_than_days(2)
except Exception as e:
print(e)
def push_to_explorer(self):
if self.ctx.config_manager.config['STATION']['enable_explorer'] and self.ctx.state_manager.is_modem_running:
try:
explorer.Explorer(self.ctx).push()
except Exception as e:
print(e)
def check_for_queued_messages(self):
"""Checks for and sends queued messages.
This method checks for queued messages in the database and transmits
the first queued message if the freedata_server is running, not currently
transmitting other data (ARQ or Codec2), and a queued message is
available. It handles potential exceptions during message retrieval
and transmission.
"""
if not self.ctx.state_manager.getARQ() and not self.ctx.state_manager.channel_busy_event.is_set() and self.ctx.state_manager.is_modem_running:
try:
if first_queued_message := DatabaseManagerMessages(self.ctx).get_first_queued_message():
command = command_message_send.SendMessageCommand(self.ctx,first_queued_message)
command.transmit()
except Exception as e:
print(e)
return
def update_transmission_state(self):
"""Updates and cleans up ARQ session states.
This method periodically checks the state of active ARQ sessions.
It sets inactive IRS (Information Receiving Station) sessions to
RESUME, deletes successfully completed or failed/aborted sessions,
and handles potential exceptions during state updates and session
deletion.
"""
session_to_be_deleted = set()
for session_id in self.ctx.state_manager.arq_irs_sessions:
session = self.ctx.state_manager.arq_irs_sessions[session_id]
# set an IRS session to RESUME for being ready getting the data again
if session.is_IRS and session.last_state_change_timestamp + 90 < time.time():
try:
# if session state is already RESUME, don't set it again for avoiding a flooded cli
if session.state not in [session.state_enum.RESUME]:
self.log.info(f"[SCHEDULE] [ARQ={session_id}] Setting state to", old_state=session.state,
state=IRS_State.RESUME)
session.state = session.set_state(session.state_enum.RESUME)
session.state = session.state_enum.RESUME
# if session is received successfully, indiciated by ENDED state, delete it
if session.state in [session.state_enum.ENDED]:
session_to_be_deleted.add(session)
except Exception as e:
self.log.warning("[SCHEDULE] error setting ARQ state", error=e)
for session_id in self.ctx.state_manager.arq_iss_sessions:
session = self.ctx.state_manager.arq_iss_sessions[session_id]
if not session.is_IRS and session.last_state_change_timestamp + 90 < time.time() and session.state in [
ISS_State.ABORTED, ISS_State.FAILED]:
session_to_be_deleted.add(session)
# finally delete sessions
try:
for session in session_to_be_deleted:
if session.is_IRS:
self.ctx.state_manager.remove_arq_irs_session(session.id)
else:
self.ctx.state_manager.remove_arq_iss_session(session.id)
except Exception as e:
self.log.warning("[SCHEDULE] error deleting ARQ session", error=e)