Skip to content

Commit a0e190d

Browse files
committed
Extract network module to separate file
1 parent b355433 commit a0e190d

File tree

4 files changed

+244
-216
lines changed

4 files changed

+244
-216
lines changed

network.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
#############################################################################
4+
#
5+
# OnAirScreen
6+
# Copyright (c) 2012-2025 Sascha Ludwig, astrastudio.de
7+
# All rights reserved.
8+
#
9+
# network.py
10+
# This file is part of OnAirScreen
11+
#
12+
# You may use this file under the terms of the BSD license as follows:
13+
#
14+
# "Redistribution and use in source and binary forms, with or without
15+
# modification, are permitted provided that the following conditions are
16+
# met:
17+
# * Redistributions of source code must retain the above copyright
18+
# notice, this list of conditions and the following disclaimer.
19+
# * Redistributions in binary form must reproduce the above copyright
20+
# notice, this list of conditions and the following disclaimer in
21+
# the documentation and/or other materials provided with the
22+
# distribution.
23+
#
24+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
35+
#
36+
#############################################################################
37+
38+
"""
39+
Network Module for OnAirScreen
40+
41+
This module handles UDP and HTTP network communication for receiving commands.
42+
"""
43+
44+
import logging
45+
import socket
46+
from http.server import BaseHTTPRequestHandler, HTTPServer
47+
from urllib.parse import unquote_plus
48+
49+
from PyQt6.QtCore import QThread, QSettings
50+
from PyQt6.QtNetwork import QUdpSocket, QHostAddress
51+
52+
from utils import settings_group
53+
from settings_functions import versionString
54+
55+
logger = logging.getLogger(__name__)
56+
57+
HOST = '0.0.0.0'
58+
59+
60+
class UdpServer:
61+
"""
62+
UDP Server for receiving commands via multicast/unicast
63+
64+
Handles UDP socket setup, multicast group joining, and command reception.
65+
"""
66+
67+
def __init__(self, command_callback):
68+
"""
69+
Initialize UDP server
70+
71+
Args:
72+
command_callback: Callback function to handle received commands (takes bytes)
73+
"""
74+
self.command_callback = command_callback
75+
self.udpsock = QUdpSocket()
76+
self._setup_socket()
77+
78+
def _setup_socket(self) -> None:
79+
"""Setup UDP socket and join multicast group"""
80+
settings = QSettings(QSettings.Scope.UserScope, "astrastudio", "OnAirScreen")
81+
with settings_group(settings, "Network"):
82+
try:
83+
port = int(settings.value('udpport', "3310"))
84+
except ValueError:
85+
port = "3310"
86+
settings.setValue('udpport', "3310")
87+
multicast_address = settings.value('multicast_address', "239.194.0.1")
88+
if not QHostAddress(multicast_address).isMulticast():
89+
multicast_address = "239.194.0.1"
90+
settings.setValue('multicast_address', "239.194.0.1")
91+
92+
self.udpsock.bind(QHostAddress.SpecialAddress.AnyIPv4, int(port), QUdpSocket.BindFlag.ShareAddress)
93+
if QHostAddress(multicast_address).isMulticast():
94+
logger.info(f"{multicast_address} is Multicast, joining multicast group")
95+
self.udpsock.joinMulticastGroup(QHostAddress(multicast_address))
96+
self.udpsock.readyRead.connect(self._handle_udp_data)
97+
98+
def _handle_udp_data(self) -> None:
99+
"""Handle incoming UDP datagrams"""
100+
while self.udpsock.hasPendingDatagrams():
101+
data, host, port = self.udpsock.readDatagram(self.udpsock.pendingDatagramSize())
102+
lines = data.splitlines()
103+
for line in lines:
104+
self.command_callback(line)
105+
106+
107+
class HttpDaemon(QThread):
108+
"""
109+
HTTP server thread for handling HTTP-based commands
110+
111+
Runs a simple HTTP server that accepts GET requests with commands
112+
and forwards them to the UDP command handler.
113+
"""
114+
115+
def run(self):
116+
"""Start HTTP server"""
117+
settings = QSettings(QSettings.Scope.UserScope, "astrastudio", "OnAirScreen")
118+
with settings_group(settings, "Network"):
119+
try:
120+
port = int(settings.value('httpport', "8010"))
121+
except ValueError:
122+
port = 8010
123+
settings.setValue("httpport", "8010")
124+
125+
try:
126+
handler = OASHTTPRequestHandler
127+
self._server = HTTPServer((HOST, port), handler)
128+
self._server.serve_forever()
129+
except OSError as error:
130+
logger.error(f"ERROR: Starting HTTP Server on port {port}: {error}")
131+
132+
def stop(self):
133+
"""Stop HTTP server"""
134+
self._server.shutdown()
135+
self._server.socket.close()
136+
self.wait()
137+
138+
139+
class OASHTTPRequestHandler(BaseHTTPRequestHandler):
140+
"""
141+
HTTP request handler for OnAirScreen commands
142+
143+
Handles GET requests with command parameters and forwards them
144+
to the UDP command handler.
145+
"""
146+
server_version = f"OnAirScreen/{versionString}"
147+
148+
def do_HEAD(self):
149+
"""Handle HEAD request"""
150+
self.send_response(200)
151+
self.send_header("Content-type", "text/html; charset=utf-8")
152+
self.end_headers()
153+
154+
def do_GET(self):
155+
"""Handle GET request with command"""
156+
logger.debug(f"HTTP request path: {self.path}")
157+
if self.path.startswith('/?cmd'):
158+
try:
159+
# Parse the query string: /?cmd=COMMAND:VALUE
160+
# First split to get cmd=COMMAND:VALUE
161+
query_string = str(self.path)[5:] # Remove '/?cmd'
162+
if '=' in query_string:
163+
cmd, message = query_string.split("=", 1)
164+
# URL-decode the message part (value after =)
165+
# unquote_plus also decodes + signs to spaces
166+
message = unquote_plus(message)
167+
else:
168+
self.send_error(400, 'no command was given')
169+
return
170+
except ValueError:
171+
self.send_error(400, 'no command was given')
172+
return
173+
174+
if len(message) > 0:
175+
self.send_response(200)
176+
177+
# send header first
178+
self.send_header('Content-type', 'text-html; charset=utf-8')
179+
self.end_headers()
180+
181+
settings = QSettings(QSettings.Scope.UserScope, "astrastudio", "OnAirScreen")
182+
with settings_group(settings, "Network"):
183+
port = int(settings.value('udpport', "3310"))
184+
185+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
186+
sock.sendto(message.encode(), ("127.0.0.1", port))
187+
188+
# send file content to client
189+
self.wfile.write(message.encode())
190+
self.wfile.write("\n".encode())
191+
return
192+
else:
193+
self.send_error(400, 'no command was given')
194+
return
195+
196+
self.send_error(404, 'file not found')
197+

0 commit comments

Comments
 (0)