1
0
Fork 0

Compare commits

...

3 Commits

Author SHA1 Message Date
Dustin a14921b79e svc: Save config after navigation
dustin/hudctrl/pipeline/head There was a failure building this commit Details
When a makes a *POST /screen/{name}/navigate* HTTP request, the URL list
is updated and saved to the configuration file.  This provides a
mechanism for configuring the URL list, since it is now part of the
display configuration file and not a separate file manged externally.
2022-12-18 13:02:33 -06:00
Dustin debce63bba svc: Save display config on change
When the display host updates its monitor configuration by making the
*PUT /display/monitors* HTTP request, the configuration is now saved to
disk.  This will allow the controller to "remember" the configuration
the next time it starts up.
2022-12-18 13:02:33 -06:00
Dustin f9eaf9f9d7 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.
2022-12-18 12:52:01 -06:00
4 changed files with 63 additions and 26 deletions

1
svc/.gitignore vendored
View File

@ -4,3 +4,4 @@
.mypy_cache/
__pycache__/
*.py[co]
/config.json

View File

@ -1,7 +1,5 @@
import io
import logging
import os
from pathlib import Path
from typing import Optional
from PIL import Image
@ -17,16 +15,11 @@ log = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
HUDCTRL_URLS_FILE = os.environ.get('HUDCTRL_URLS_FILE')
app = fastapi.FastAPI(
docs_url='/api-doc/',
)
svc = HUDService()
if HUDCTRL_URLS_FILE:
svc.urls_file = Path(HUDCTRL_URLS_FILE)
class PNGImageResponse(fastapi.Response):
@ -61,7 +54,8 @@ async def get_monitor_config():
@app.put('/display/monitors')
async def put_monitor_config(
monitors: str = fastapi.Body(..., media_type='text/plain')
bgtasks: fastapi.BackgroundTasks,
monitors: str = fastapi.Body(..., media_type='text/plain'),
):
try:
svc.monitor_config = MonitorConfig.from_string(monitors)
@ -71,6 +65,7 @@ async def put_monitor_config(
fastapi.status.HTTP_400_BAD_REQUEST,
detail=f'Invalid monitor config: {e}',
)
bgtasks.add_task(svc.save_config)
return {'monitor_config': svc.monitor_config}
@ -142,8 +137,14 @@ async def get_screenshot(
'/screen/{name}/navigate',
response_class=fastapi.responses.PlainTextResponse,
)
async def navigate(name: str, url: str = fastapi.Form(...)):
async def navigate(
bgtasks: fastapi.BackgroundTasks,
name: str,
url: str = fastapi.Form(...),
):
await svc.navigate(name, url)
svc.urls[name] = url
bgtasks.add_task(svc.save_config)
@app.on_event('shutdown')

12
svc/src/hudctrl/config.py Normal file
View File

@ -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

View File

@ -2,18 +2,23 @@ import asyncio
import base64
import json
import logging
import os
from pathlib import Path
from typing import Dict, Optional
import pydantic
from aiomarionette import Marionette, WindowRect
from .config import Configuration
from .xrandr import MonitorConfig
log = logging.getLogger(__name__)
CONFIG_PATH = os.environ.get('HUDCTRL_CONFIG_PATH', 'config.json')
class NoMonitorConfig(Exception):
'''Raised when no monitor config has been provided yet'''
@ -34,7 +39,7 @@ class HUDService:
self.urls: Dict[str, str] = {}
self.windows: Dict[str, str] = {}
self.urls_file = Path('urls.json')
self.config_file = Path(CONFIG_PATH)
self.lock = asyncio.Lock()
async def get_screen(self, name: str) -> HUDScreen:
@ -104,29 +109,27 @@ class HUDService:
if tasks:
await asyncio.wait(tasks)
def load_urls(self) -> None:
def load_config(self) -> None:
try:
f = self.urls_file.open(encoding='utf-8')
f = self.config_file.open(encoding='utf-8')
except FileNotFoundError:
return
except OSError as e:
log.error('Could not load URL list: %s', e)
log.error('Could not load config: %s', e)
return
log.info('Loading URL list from %s', self.urls_file)
log.info('Loading config from %s', f.name)
with f:
try:
value = json.load(f)
config = Configuration.parse_obj(json.load(f))
except ValueError as e:
log.error('Failed to load URL list: %s', e)
log.error('Failed to load config: %s', e)
return
if isinstance(value, dict):
self.urls = value
else:
log.error(
'Invalid URL list: found %r, expected %r',
type(value),
dict,
)
self.urls = config.urls
if config.monitors:
self.monitor_config = MonitorConfig()
self.monitor_config.monitors = config.monitors
self.host = config.host
self.port = config.port
async def navigate(self, name: str, url: str) -> None:
assert self.marionette
@ -140,6 +143,19 @@ class HUDService:
await self.marionette.switch_to_window(self.windows[name])
await self.marionette.refresh()
def save_config(self) -> None:
log.info('Saving configuration to %s', self.config_file)
config = Configuration(
monitors=self.monitor_config.monitors
if self.monitor_config
else [],
urls=self.urls,
host=self.host,
port=self.port,
)
f = self.config_file.open('w', encoding='utf-8')
f.write(config.json())
async def set_display(self, host: str, port: int) -> None:
if self.marionette:
log.warning('Closing existing Marionette connection')
@ -154,10 +170,17 @@ class HUDService:
if self.marionette is not None:
await self.marionette.close()
self.marionette = None
self.save_config()
async def startup(self) -> None:
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, self.load_urls)
asyncio.create_task(self._startup())
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:
assert self.marionette