mirror of
https://github.com/usetrmnl/byos_fastapi.git
synced 2026-04-29 13:44:09 -07:00
269 lines
9.5 KiB
Python
269 lines
9.5 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from os import environ, getcwd
|
|
from os.path import abspath, isdir, join
|
|
from sys import stdout
|
|
|
|
# Logging Configuration
|
|
LOG_LEVEL = environ.get('LOG_LEVEL', 'DEBUG').upper()
|
|
logging.basicConfig(
|
|
level=getattr(logging, LOG_LEVEL, logging.DEBUG),
|
|
format="%(asctime)s %(levelname)s %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
stream=stdout
|
|
)
|
|
logger = logging.getLogger('trmnlServer')
|
|
|
|
# Pillow emits very noisy DEBUG logs (PNG chunk dumps). Keep them at INFO+.
|
|
logging.getLogger('PIL').setLevel(logging.INFO)
|
|
logging.getLogger('PIL.PngImagePlugin').setLevel(logging.INFO)
|
|
logging.getLogger('httpx').setLevel(logging.WARNING)
|
|
logging.getLogger('httpcore').setLevel(logging.WARNING)
|
|
|
|
logger.info('[Config] loading module')
|
|
|
|
_TRUE_VALUES = {'true', '1', 't', 'yes', 'on'}
|
|
|
|
IMAGE_PATH = 'images/screen.bmp'
|
|
REFRESH_TIME = 900
|
|
BATTERY_MAX_VOLTAGE = 4.1
|
|
BATTERY_MIN_VOLTAGE = 2.3
|
|
TIME_ZONE = 'UTC'
|
|
SERVER_PORT = 4567
|
|
ENABLE_SSL = False
|
|
SERVER_SCHEME = 'http'
|
|
SETUP_API_KEY = ''
|
|
SETUP_FRIENDLY_ID = 'trmnl-byod'
|
|
SETUP_MESSAGE = 'Configured'
|
|
DITHERING_MODE = 'none'
|
|
ASSETS_ROOT = 'web'
|
|
STATIC_ROOT = 'web'
|
|
GENERATED_ROOT = 'var/generated'
|
|
|
|
# Hard limit for generated grayscale PNG assets (bytes).
|
|
PNG_MAX_BYTES = 90000
|
|
|
|
# Photographic plugin grading
|
|
#
|
|
# When enabled, photographic plugins apply additional histogram and shadow grading
|
|
# prior to tone-curve-aware quantization/dithering.
|
|
# When disabled, photographic plugins output raw grayscale (no extra grading),
|
|
# which makes it easier to reason about the tone curve/LUT calibration.
|
|
PHOTO_GRADING_ENABLED = True
|
|
|
|
# Calibration plugin control
|
|
#
|
|
# When disabled, calibration plugins are excluded from the plugin registry and
|
|
# no calibration assets are generated.
|
|
CALIBRATION_PLUGIN_ENABLED = False
|
|
|
|
# E-ink grayscale response compensation
|
|
#
|
|
# These settings allow quantization and dithering to operate in a non-linear
|
|
# "panel space" so the resulting grays are closer to perceptually uniform on
|
|
# real e-ink panels.
|
|
#
|
|
# - EINK_TONE_POINTS: optional anchor list "in:out" in 0-255, comma-separated
|
|
# e.g. "0:0,32:40,128:160,255:255" (digital input -> observed panel output).
|
|
# - EINK_TONE_GAMMA: fallback forward gamma (digital -> panel) when points unset.
|
|
EINK_TONE_POINTS = '0:0,32:6,64:18,85:32,128:95,170:155,192:190,224:225,255:250'
|
|
EINK_TONE_GAMMA = 1.0
|
|
|
|
CONFIG_DIR = getcwd()
|
|
VAR_ROOT = join(CONFIG_DIR, 'var')
|
|
DATABASE_PATH = join(VAR_ROOT, 'db', 'trmnl.db')
|
|
LOGS_DIR = join(VAR_ROOT, 'logs')
|
|
SSL_DIR = join(VAR_ROOT, 'ssl')
|
|
WEB_ROOT_DIR = join(CONFIG_DIR, ASSETS_ROOT)
|
|
WEB_STATIC_DIR = join(CONFIG_DIR, STATIC_ROOT)
|
|
WEB_GENERATED_DIR = join(CONFIG_DIR, GENERATED_ROOT)
|
|
|
|
_ENV_OVERRIDES: set[str] = set()
|
|
|
|
|
|
def _env_str(name: str, default: str, config_key: str) -> str:
|
|
value = environ.get(name)
|
|
if value is None:
|
|
return default
|
|
_ENV_OVERRIDES.add(config_key)
|
|
return value
|
|
|
|
|
|
def _env_bool(name: str, default: bool, config_key: str) -> bool:
|
|
value = environ.get(name)
|
|
if value is None:
|
|
return default
|
|
_ENV_OVERRIDES.add(config_key)
|
|
return value.strip().lower() in _TRUE_VALUES
|
|
|
|
|
|
def _env_int(name: str, default: int, config_key: str) -> int:
|
|
value = environ.get(name)
|
|
if value is None:
|
|
return default
|
|
try:
|
|
number = int(value)
|
|
_ENV_OVERRIDES.add(config_key)
|
|
return number
|
|
except ValueError:
|
|
logger.warning('[Config] Invalid int for %s: %s', name, value)
|
|
return default
|
|
|
|
|
|
def _env_float(name: str, default: float, config_key: str) -> float:
|
|
value = environ.get(name)
|
|
if value is None:
|
|
return default
|
|
try:
|
|
number = float(value)
|
|
_ENV_OVERRIDES.add(config_key)
|
|
return number
|
|
except ValueError:
|
|
logger.warning('[Config] Invalid float for %s: %s', name, value)
|
|
return default
|
|
|
|
|
|
def _apply_environment_overrides() -> None:
|
|
global IMAGE_PATH, REFRESH_TIME
|
|
global BATTERY_MAX_VOLTAGE, BATTERY_MIN_VOLTAGE, TIME_ZONE
|
|
global SERVER_PORT, ENABLE_SSL, SERVER_SCHEME
|
|
global SETUP_API_KEY, SETUP_FRIENDLY_ID, SETUP_MESSAGE
|
|
global DITHERING_MODE, ASSETS_ROOT, STATIC_ROOT, GENERATED_ROOT
|
|
global PNG_MAX_BYTES
|
|
global EINK_TONE_POINTS, EINK_TONE_GAMMA
|
|
global PHOTO_GRADING_ENABLED, CALIBRATION_PLUGIN_ENABLED
|
|
_ENV_OVERRIDES.clear()
|
|
|
|
default_eink_tone_points = EINK_TONE_POINTS
|
|
default_eink_tone_gamma = EINK_TONE_GAMMA
|
|
|
|
IMAGE_PATH = _env_str('IMAGE_PATH', 'images/screen.bmp', 'image_path')
|
|
REFRESH_TIME = _env_int('REFRESH_TIME', 900, 'refresh_time')
|
|
BATTERY_MAX_VOLTAGE = _env_float('BATTERY_MAX_VOLTAGE', 4.1, 'battery_max_voltage')
|
|
BATTERY_MIN_VOLTAGE = _env_float('BATTERY_MIN_VOLTAGE', 2.3, 'battery_min_voltage')
|
|
TIME_ZONE = _env_str('TIME_ZONE', 'UTC', 'time_zone')
|
|
SERVER_PORT = _env_int('SERVER_PORT', 4567, 'server_port')
|
|
ENABLE_SSL = _env_bool('ENABLE_SSL', False, 'enable_ssl')
|
|
SETUP_API_KEY = _env_str('SETUP_API_KEY', '', 'setup_api_key')
|
|
SETUP_FRIENDLY_ID = _env_str('SETUP_FRIENDLY_ID', 'trmnl-byod', 'setup_friendly_id')
|
|
SETUP_MESSAGE = _env_str('SETUP_MESSAGE', 'Configured', 'setup_message')
|
|
DITHERING_MODE = _env_str('DITHERING_MODE', 'none', 'dithering_mode')
|
|
ASSETS_ROOT = _env_str('ASSETS_ROOT', 'web', 'assets_root')
|
|
STATIC_ROOT = _env_str('STATIC_ROOT', 'web', 'static_root')
|
|
GENERATED_ROOT = _env_str('GENERATED_ROOT', 'var/generated', 'generated_root')
|
|
PNG_MAX_BYTES = _env_int('PNG_MAX_BYTES', 90000, 'png_max_bytes')
|
|
EINK_TONE_POINTS = _env_str('EINK_TONE_POINTS', default_eink_tone_points, 'eink_tone_points')
|
|
EINK_TONE_GAMMA = _env_float('EINK_TONE_GAMMA', default_eink_tone_gamma, 'eink_tone_gamma')
|
|
PHOTO_GRADING_ENABLED = _env_bool('PHOTO_GRADING_ENABLED', PHOTO_GRADING_ENABLED, 'photo_grading_enabled')
|
|
CALIBRATION_PLUGIN_ENABLED = _env_bool('CALIBRATION_PLUGIN_ENABLED', CALIBRATION_PLUGIN_ENABLED, 'calibration_plugin_enabled')
|
|
_refresh_server_scheme()
|
|
|
|
|
|
def _refresh_server_scheme() -> None:
|
|
global SERVER_SCHEME
|
|
SERVER_SCHEME = 'https' if ENABLE_SSL else 'http'
|
|
|
|
|
|
def _refresh_path_constants() -> None:
|
|
global VAR_ROOT, DATABASE_PATH, LOGS_DIR, SSL_DIR
|
|
global WEB_ROOT_DIR, WEB_STATIC_DIR, WEB_GENERATED_DIR
|
|
VAR_ROOT = join(CONFIG_DIR, 'var')
|
|
DATABASE_PATH = join(VAR_ROOT, 'db', 'trmnl.db')
|
|
LOGS_DIR = join(VAR_ROOT, 'logs')
|
|
SSL_DIR = join(VAR_ROOT, 'ssl')
|
|
WEB_ROOT_DIR = join(CONFIG_DIR, ASSETS_ROOT)
|
|
WEB_STATIC_DIR = join(CONFIG_DIR, STATIC_ROOT)
|
|
WEB_GENERATED_DIR = join(CONFIG_DIR, GENERATED_ROOT)
|
|
|
|
|
|
def load_config(base_dir: str | None = None) -> None:
|
|
"""Apply environment overrides and update path constants for the provided base directory."""
|
|
global CONFIG_DIR
|
|
_apply_environment_overrides()
|
|
if base_dir:
|
|
candidate = abspath(base_dir)
|
|
if not isdir(candidate):
|
|
logger.warning('[Config] Provided base_dir %s is not a directory; using current working directory', base_dir)
|
|
candidate = getcwd()
|
|
CONFIG_DIR = candidate
|
|
else:
|
|
CONFIG_DIR = getcwd()
|
|
_refresh_path_constants()
|
|
|
|
|
|
def _coerce_bool(value) -> bool:
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, str):
|
|
return value.strip().lower() in _TRUE_VALUES
|
|
return bool(value)
|
|
|
|
|
|
def update_config(key: str, value) -> None:
|
|
"""Update an in-memory configuration value."""
|
|
global IMAGE_PATH, REFRESH_TIME
|
|
global BATTERY_MAX_VOLTAGE, BATTERY_MIN_VOLTAGE, TIME_ZONE
|
|
global SERVER_PORT, ENABLE_SSL, SETUP_API_KEY
|
|
global SETUP_FRIENDLY_ID, SETUP_MESSAGE, DITHERING_MODE
|
|
global ASSETS_ROOT, STATIC_ROOT, GENERATED_ROOT
|
|
global EINK_TONE_POINTS, EINK_TONE_GAMMA
|
|
global PHOTO_GRADING_ENABLED, CALIBRATION_PLUGIN_ENABLED
|
|
|
|
logger.info('[Config] Updating %s to %s', key, value)
|
|
|
|
if key == 'image_path':
|
|
IMAGE_PATH = str(value)
|
|
elif key == 'refresh_time':
|
|
REFRESH_TIME = int(value)
|
|
elif key == 'battery_max_voltage':
|
|
BATTERY_MAX_VOLTAGE = float(value)
|
|
elif key == 'battery_min_voltage':
|
|
BATTERY_MIN_VOLTAGE = float(value)
|
|
elif key == 'time_zone':
|
|
TIME_ZONE = str(value)
|
|
elif key == 'server_port':
|
|
SERVER_PORT = int(value)
|
|
elif key == 'enable_ssl':
|
|
ENABLE_SSL = _coerce_bool(value)
|
|
_refresh_server_scheme()
|
|
elif key == 'setup_api_key':
|
|
SETUP_API_KEY = str(value)
|
|
elif key == 'setup_friendly_id':
|
|
SETUP_FRIENDLY_ID = str(value)
|
|
elif key == 'setup_message':
|
|
SETUP_MESSAGE = str(value)
|
|
elif key == 'dithering_mode':
|
|
DITHERING_MODE = str(value)
|
|
elif key == 'assets_root':
|
|
ASSETS_ROOT = str(value)
|
|
_refresh_path_constants()
|
|
elif key == 'static_root':
|
|
STATIC_ROOT = str(value)
|
|
_refresh_path_constants()
|
|
elif key == 'generated_root':
|
|
GENERATED_ROOT = str(value)
|
|
_refresh_path_constants()
|
|
elif key == 'eink_tone_points':
|
|
EINK_TONE_POINTS = str(value)
|
|
elif key == 'eink_tone_gamma':
|
|
EINK_TONE_GAMMA = float(value)
|
|
elif key == 'photo_grading_enabled':
|
|
PHOTO_GRADING_ENABLED = _coerce_bool(value)
|
|
elif key == 'calibration_plugin_enabled':
|
|
CALIBRATION_PLUGIN_ENABLED = _coerce_bool(value)
|
|
else:
|
|
logger.warning('[Config] Unknown config key: %s', key)
|
|
|
|
|
|
def apply_persisted_config(entries: dict[str, str]) -> None:
|
|
"""Apply database-backed configuration entries unless overridden by env vars."""
|
|
for key, raw_value in entries.items():
|
|
if key in _ENV_OVERRIDES:
|
|
continue
|
|
update_config(key, raw_value)
|
|
|
|
|
|
_apply_environment_overrides()
|
|
_refresh_path_constants()
|