svc: Load display config at startup
The biggest issue with the HUD controller right now is how it handles, or rather does not handle, reconnecting to the display after the controller restarts. Since it is completely stateless, it does not "remember" the display configuration, and thus does not know how to connect to the display or what its monitor configuration is when it first starts. If the display is already running the the controller starts up, it will not receive the request to initialize the display until the display reboots. To resolve this, the HUD controller needs to be able to load the display configuration and initialize the display any time it starts up. As such, the `HUDService` class now attempts to load the state from a JSON file in its startup hook. If the file is available and valid, and specifies the address and port of the display host, the controller will open the Marionette connection. If the file also contains valid list of monitors, the display will be initialized. The URL list is now also read from the same configuration file, so as to avoid having to manage multiple files.master
parent
949d180edd
commit
f9eaf9f9d7
|
@ -4,3 +4,4 @@
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[co]
|
*.py[co]
|
||||||
|
/config.json
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
@ -17,16 +15,11 @@ log = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
HUDCTRL_URLS_FILE = os.environ.get('HUDCTRL_URLS_FILE')
|
|
||||||
|
|
||||||
|
|
||||||
app = fastapi.FastAPI(
|
app = fastapi.FastAPI(
|
||||||
docs_url='/api-doc/',
|
docs_url='/api-doc/',
|
||||||
)
|
)
|
||||||
|
|
||||||
svc = HUDService()
|
svc = HUDService()
|
||||||
if HUDCTRL_URLS_FILE:
|
|
||||||
svc.urls_file = Path(HUDCTRL_URLS_FILE)
|
|
||||||
|
|
||||||
|
|
||||||
class PNGImageResponse(fastapi.Response):
|
class PNGImageResponse(fastapi.Response):
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
from .xrandr import Monitor
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration(pydantic.BaseModel):
|
||||||
|
monitors: list[Monitor] = pydantic.Field(default_factory=list)
|
||||||
|
urls: dict[str, str] = pydantic.Field(default_factory=dict)
|
||||||
|
host: Optional[str] = None
|
||||||
|
port: Optional[int] = None
|
|
@ -2,18 +2,23 @@ import asyncio
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
from aiomarionette import Marionette, WindowRect
|
from aiomarionette import Marionette, WindowRect
|
||||||
|
|
||||||
|
from .config import Configuration
|
||||||
from .xrandr import MonitorConfig
|
from .xrandr import MonitorConfig
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_PATH = os.environ.get('HUDCTRL_CONFIG_PATH', 'config.json')
|
||||||
|
|
||||||
|
|
||||||
class NoMonitorConfig(Exception):
|
class NoMonitorConfig(Exception):
|
||||||
'''Raised when no monitor config has been provided yet'''
|
'''Raised when no monitor config has been provided yet'''
|
||||||
|
|
||||||
|
@ -34,7 +39,7 @@ class HUDService:
|
||||||
self.urls: Dict[str, str] = {}
|
self.urls: Dict[str, str] = {}
|
||||||
self.windows: Dict[str, str] = {}
|
self.windows: Dict[str, str] = {}
|
||||||
|
|
||||||
self.urls_file = Path('urls.json')
|
self.config_file = Path(CONFIG_PATH)
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
async def get_screen(self, name: str) -> HUDScreen:
|
async def get_screen(self, name: str) -> HUDScreen:
|
||||||
|
@ -104,29 +109,27 @@ class HUDService:
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
def load_urls(self) -> None:
|
def load_config(self) -> None:
|
||||||
try:
|
try:
|
||||||
f = self.urls_file.open(encoding='utf-8')
|
f = self.config_file.open(encoding='utf-8')
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return
|
return
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log.error('Could not load URL list: %s', e)
|
log.error('Could not load config: %s', e)
|
||||||
return
|
return
|
||||||
log.info('Loading URL list from %s', self.urls_file)
|
log.info('Loading config from %s', f.name)
|
||||||
with f:
|
with f:
|
||||||
try:
|
try:
|
||||||
value = json.load(f)
|
config = Configuration.parse_obj(json.load(f))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
log.error('Failed to load URL list: %s', e)
|
log.error('Failed to load config: %s', e)
|
||||||
return
|
return
|
||||||
if isinstance(value, dict):
|
self.urls = config.urls
|
||||||
self.urls = value
|
if config.monitors:
|
||||||
else:
|
self.monitor_config = MonitorConfig()
|
||||||
log.error(
|
self.monitor_config.monitors = config.monitors
|
||||||
'Invalid URL list: found %r, expected %r',
|
self.host = config.host
|
||||||
type(value),
|
self.port = config.port
|
||||||
dict,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def navigate(self, name: str, url: str) -> None:
|
async def navigate(self, name: str, url: str) -> None:
|
||||||
assert self.marionette
|
assert self.marionette
|
||||||
|
@ -154,10 +157,17 @@ class HUDService:
|
||||||
if self.marionette is not None:
|
if self.marionette is not None:
|
||||||
await self.marionette.close()
|
await self.marionette.close()
|
||||||
self.marionette = None
|
self.marionette = None
|
||||||
|
self.save_config()
|
||||||
|
|
||||||
async def startup(self) -> None:
|
async def startup(self) -> None:
|
||||||
loop = asyncio.get_running_loop()
|
asyncio.create_task(self._startup())
|
||||||
await loop.run_in_executor(None, self.load_urls)
|
|
||||||
|
async def _startup(self) -> None:
|
||||||
|
await asyncio.to_thread(self.load_config)
|
||||||
|
if self.host and self.port:
|
||||||
|
await self.set_display(self.host, self.port)
|
||||||
|
if self.monitor_config:
|
||||||
|
await self.initialize()
|
||||||
|
|
||||||
async def take_screenshot(self, screen: str) -> bytes:
|
async def take_screenshot(self, screen: str) -> bytes:
|
||||||
assert self.marionette
|
assert self.marionette
|
||||||
|
|
Loading…
Reference in New Issue