138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
"""OpenFlexure System.
|
|
|
|
A module to control the underlying microscope system and to expose information about
|
|
the microscope, server, and thing states to the web API.
|
|
"""
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import time
|
|
from collections.abc import Mapping
|
|
from signal import SIGTERM
|
|
from typing import Any, Optional
|
|
from uuid import UUID, uuid4
|
|
|
|
from pydantic import BaseModel
|
|
|
|
import labthings_fastapi as lt
|
|
|
|
from openflexure_microscope_server.utilities import VersionData, robust_version_strings
|
|
|
|
SHUTDOWN_CMD = ["sudo", "shutdown", "-h", "now"]
|
|
REBOOT_CMD = ["sudo", "shutdown", "-r", "now"]
|
|
|
|
|
|
class CommandOutput(BaseModel):
|
|
"""A pydantic model passing the STDOUT and STDERR from a subprocess over HTTP."""
|
|
|
|
output: str
|
|
error: str
|
|
|
|
|
|
class OpenFlexureSystem(lt.Thing):
|
|
"""Describe and control the OpenFlexure system.
|
|
|
|
This Thing:
|
|
|
|
* Exposes information about the Microscope, Server, and Thing states to the web
|
|
API.
|
|
* Controls the underlying OS on the Raspberry Pi allowing shutdown and restarting
|
|
of the system.
|
|
"""
|
|
|
|
_microscope_id: Optional[str] = None
|
|
|
|
@lt.setting
|
|
def microscope_id(self) -> UUID:
|
|
"""A unique identifier for this microscope."""
|
|
if self._microscope_id is None:
|
|
self._microscope_id = str(uuid4())
|
|
return UUID(self._microscope_id)
|
|
|
|
@microscope_id.setter
|
|
def _set_microscope_id(self, uuid: UUID) -> None:
|
|
self._microscope_id = str(uuid)
|
|
|
|
@lt.property
|
|
def hostname(self) -> str:
|
|
"""The hostname of the microscope, as reported by its operating system."""
|
|
return socket.gethostname()
|
|
|
|
_version_data: Optional[VersionData] = None
|
|
|
|
@lt.property
|
|
def version_data(self) -> VersionData:
|
|
"""The version string and version source for the server.
|
|
|
|
The source may be a commit hash if installed from git. "TOML" if installed from
|
|
source, or "Dist" if installed from a distribution package.
|
|
|
|
If the version or its source cannot be determined an error will be Logged.
|
|
"""
|
|
# Don't save as a setting as it may change. Get the version string the first
|
|
# time the property is requested this boot.
|
|
if self._version_data is None:
|
|
self._version_data = robust_version_strings()
|
|
return self._version_data
|
|
|
|
@lt.property
|
|
def is_raspberrypi(self) -> bool:
|
|
"""Return True if running on a Raspberry Pi."""
|
|
return os.path.exists("/usr/bin/raspi-config")
|
|
|
|
@lt.action
|
|
def shutdown(self) -> CommandOutput:
|
|
"""Attempt to shutdown the device."""
|
|
if not self.is_raspberrypi:
|
|
os.kill(os.getpid(), SIGTERM)
|
|
time.sleep(0.5)
|
|
# If this line is reached then the server did not shut down!
|
|
return CommandOutput(
|
|
output="",
|
|
error="Shutdown command sent to server process, but the server has not shutdown.",
|
|
)
|
|
|
|
# On a Raspberry Pi
|
|
p = subprocess.Popen(
|
|
SHUTDOWN_CMD,
|
|
stderr=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
|
|
out, err = p.communicate()
|
|
return CommandOutput(output=out, error=err)
|
|
|
|
@lt.action
|
|
def reboot(self) -> CommandOutput:
|
|
"""Attempt to reboot the device."""
|
|
if not self.is_raspberrypi:
|
|
return CommandOutput(
|
|
output="",
|
|
error="Restart is only available on Raspberry Pi.",
|
|
)
|
|
|
|
p = subprocess.Popen(
|
|
REBOOT_CMD,
|
|
stderr=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
out, err = p.communicate()
|
|
return CommandOutput(output=out, error=err)
|
|
|
|
@lt.action
|
|
def get_things_state(self) -> Mapping:
|
|
"""Metadata summarising the current state of all Things in the server."""
|
|
return self._thing_server_interface.get_thing_states()
|
|
|
|
@property
|
|
def thing_state(self) -> Mapping[str, Any]:
|
|
"""Summary metadata describing the current state of the Thing."""
|
|
return {
|
|
"hostname": self.hostname,
|
|
"microscope-uuid": str(self.microscope_id),
|
|
"version": self.version_data.version,
|
|
"version_source": self.version_data.version_source,
|
|
}
|