Skip to content

Commit

Permalink
installer: Added support for standalone installation
Browse files Browse the repository at this point in the history
  • Loading branch information
Clon1998 committed Nov 30, 2024
1 parent 034bc2b commit 67ef129
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 31 deletions.
104 changes: 97 additions & 7 deletions installer/Config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,12 @@ def _setup_mobileraker_conf(self, context: Context):
Logger.Blank()
Logger.Blank()
Logger.Warn(f"No printer sections found in {Config.CONFIG_FILE_NAME}, adding a default one.")
Logger.Warn(f"Please verify the config settings are correct after the install is complete in the {Config.CONFIG_FILE_NAME}.")
Logger.Info("Note: If you have multiple printers, you will need to add them manually in the same file and format.")
self._add_printer("default", context, config, write_section)
self._add_printer_with_user_input(context, config, write_section, "default")
elif not self._printer_in_config(context, config, printer_sections):
Logger.Blank()
Logger.Blank()
Logger.Warn(f"Printer section for Moonraker Instance with port {context.moonraker_port} not found in {Config.CONFIG_FILE_NAME}, adding it as new one.")
Logger.Warn(f"Please verify the config settings are correct after the install is complete in the {Config.CONFIG_FILE_NAME}.")
self._add_printer(f"moonraker_{context.moonraker_port}", context, config, write_section)
self._add_printer_with_user_input(context, config, write_section, f"moonraker_{context.moonraker_port}")
else:
Logger.Blank()
Logger.Blank()
Expand Down Expand Up @@ -148,8 +145,9 @@ def _ask_for_language(self) -> str:
return available_languages[respond_index]

def _link_mobileraker_conf(self, context: Context) -> None:
if context.platform == PlatformType.K1:
if context.platform == PlatformType.K1 or context.is_standalone:
# K1 has only a single moonraker instance, so we can just skip this.
# Standalone plugins don't need this either.
return

# Creates a link to the mobileraker config file in the moonraker config folder if it is not the master config file.
Expand Down Expand Up @@ -189,6 +187,9 @@ def _discover_mobileraker_conf_path_for_sonic_pad(self, context: Context) -> str


def _default_mobileraker_conf_path(self, context: Context) -> str:
if context.is_standalone:
return os.path.join(context.standalone_data_path, Config.CONFIG_FILE_NAME)

if len(context.printer_data_config_folder) != 0:
return os.path.join(context.printer_data_config_folder, Config.CONFIG_FILE_NAME)

Expand Down Expand Up @@ -287,4 +288,93 @@ def _add_printer(self, name: str,context: Context, config: configparser.ConfigPa
else:
config.set(sec, "snapshot_uri", "http://127.0.0.1/webcam/?action=snapshot")
config.set(sec, "snapshot_rotation", "0")
config.set(sec, "ignore_filament_sensors", "")
config.set(sec, "ignore_filament_sensors", "")

def _add_printer_with_user_input(self, context: Context, config: configparser.ConfigParser, write_section: List[str], printer_name: str):
Logger.Blank()
Logger.Blank()
Logger.Info(f"Do you want me to help you add a printer to the {Config.CONFIG_FILE_NAME} file or should I use the default settings?")
help = self._ask_input("y/n", "y")
if help.lower() == 'n':
Logger.Info("Using default settings for printer.")
self._add_printer("default", context, config, write_section)
return
Logger.Info("Okay... I will guide you through the process of adding a printer to the config file.")
Logger.Blank()
Logger.Info("Please provide the following information:")
defaults = {
"name": printer_name,
"moonraker_uri": "127.0.0.1",
"moonraker_port": str(context.moonraker_port),
"moonraker_api_key": "False",
"snapshot_rotation": "0"
}
user_input = defaults

while True:
Logger.Blank()
name = self._ask_input("Printer Name", user_input["name"])
moonraker_uri = self._ask_input("Moonraker URI (Without Port)", user_input["moonraker_uri"])
moonraker_port = self._ask_input("Moonraker Port", user_input["moonraker_port"])
api_key = self._ask_input("Moonraker API Key, False if none is used", user_input["moonraker_api_key"])
snapshot_rotation = self._ask_input("Snapshot Rotation", user_input["snapshot_rotation"])

# Prepare the configuration section
sec = f"printer {name}"
user_input = {
"name": name,
"moonraker_uri": moonraker_uri,
"moonraker_port": moonraker_port,
"moonraker_api_key": api_key,
"snapshot_rotation": snapshot_rotation
}

# Display the configuration
Logger.Blank()
Logger.Info(f"Printer Configuration for '{name}':")
for key, value in user_input.items():
Logger.Info(f" {key}: {value}")

# Confirm or modify
confirm = self._ask_input("Is this configuration correct? (y/n/edit)", "y")

if confirm.lower() == 'y':
# Add the section and values to the config
write_section.append(sec)
config.add_section(sec)
config.set(sec, "moonraker_uri", f"ws://{moonraker_uri}:{moonraker_port}/websocket")
config.set(sec, "moonraker_api_key", api_key)
config.set(sec, "snapshot_uri", f"http://{moonraker_uri}/webcam/?action=snapshot")
config.set(sec, "snapshot_rotation", snapshot_rotation)
config.set(sec, "ignore_filament_sensors", "")

# Ask about adding another printer
add_more = self._ask_input("Add another printer? (y/n)", "n")
if add_more.lower() != "y":
break
user_input = defaults

elif confirm.lower() == 'edit':
# If user wants to edit, the loop will restart and ask for inputs again
continue

else:
# If user doesn't confirm, restart the input process
continue

def _ask_input(self, hint: str, default: Optional[str]) -> str:
while True:
try:
if default is not None:
hint += f" [Default = {default}]"
response = input(f"{hint}: ")
response = response.strip()
if len(response) == 0:
if default is not None:
return default
Logger.Warn("Empty input, try again.")
continue

return response
except Exception as e:
Logger.Warn("Invalid input, try again. Logger.Error: "+str(e))
40 changes: 23 additions & 17 deletions installer/Configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ def run(self, context:Context):
"""
Logger.Header("Starting configuration...")

Logger.Debug(f"Moonraker Service File Name: {context.moonraker_service_file_name}")
if context.is_standalone is False:
Logger.Debug(f"Moonraker Service File Name: {context.moonraker_service_file_name}")

# Get printer data root and config folder

# TODO: MR does not need a observer config file, it's only for the observer.
if context.platform == PlatformType.SONIC_PAD:
if context.is_standalone:
# Do nothing for standalone plugins.
Logger.Debug("Standalone plugin, no config setup needed.")
elif context.platform == PlatformType.SONIC_PAD:
# ONLY FOR THE SONIC PAD, we know the folder setup is different.
# The user data folder will have /mnt/UDISK/printer_config<number> where the config files are and /mnt/UDISK/printer_logs<number> for logs.
# Use the normal folder for the config files.
Expand Down Expand Up @@ -78,25 +80,29 @@ def run(self, context:Context):

# There's not a great way to find the log path from the config file, since the only place it's located is in the systemd file.

# First, we will see if we can find a named folder relative to this folder.
context.printer_data_logs_folder = os.path.join(context.printer_data_folder, "logs")
if os.path.exists(context.printer_data_logs_folder) is False:
# Try an older path
context.printer_data_logs_folder = os.path.join(context.printer_data_folder, "klipper_logs")
if context.is_standalone:
context.printer_data_logs_folder = os.path.join(context.standalone_data_path, "logs")
Util.ensure_dir_exists(context.printer_data_logs_folder, context, True)
else:
# First, we will see if we can find a named folder relative to this folder.
context.printer_data_logs_folder = os.path.join(context.printer_data_folder, "logs")
if os.path.exists(context.printer_data_logs_folder) is False:
# Try the path Creality OS uses, something like /mnt/UDISK/printer_logs<number>
context.printer_data_logs_folder = os.path.join(Util.parent_dir(context.printer_data_config_folder), "printer_logs")
# Try an older path
context.printer_data_logs_folder = os.path.join(context.printer_data_folder, "klipper_logs")
if os.path.exists(context.printer_data_logs_folder) is False:
# Failed, make a folder in the printer data root.
context.printer_data_logs_folder = os.path.join(context.printer_data_folder, "mobileraker-logs")
# Create the folder and force the permissions so our service can write to it.
Util.ensure_dir_exists(context.printer_data_logs_folder, context, True)
# Try the path Creality OS uses, something like /mnt/UDISK/printer_logs<number>
context.printer_data_logs_folder = os.path.join(Util.parent_dir(context.printer_data_config_folder), "printer_logs")
if os.path.exists(context.printer_data_logs_folder) is False:
# Failed, make a folder in the printer data root.
context.printer_data_logs_folder = os.path.join(context.printer_data_folder, "mobileraker-logs")
# Create the folder and force the permissions so our service can write to it.
Util.ensure_dir_exists(context.printer_data_logs_folder, context, True)

# Setup default moonraker port
self._discover_moonraker_port(context)

# Report
Logger.Info(f'Configured. Service File Path: {context.service_file_path}, Config Dir: {context.printer_data_config_folder}, Logs: {context.printer_data_logs_folder}')
Logger.Info(f'Configured. Service File Path: {context.service_file_path}, Config Dir: {"--" if context.is_standalone else context.printer_data_config_folder}, Logs: {context.printer_data_logs_folder}')

def _discover_moonraker_port(self, context:Context):
"""
Expand All @@ -110,7 +116,7 @@ def _discover_moonraker_port(self, context:Context):
"""

# First we try to read the port from the moonraker conf
if os.path.exists(context.moonraker_config_file_path):
if context.has_moonraker_config_file_path and os.path.exists(context.moonraker_config_file_path):
config = configparser.ConfigParser()
config.read(context.moonraker_config_file_path)

Expand Down
50 changes: 48 additions & 2 deletions installer/Context.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class OperationMode(Enum):
UNINSTALL: Uninstall mode.
"""
INSTALL = 1
# INSTALL_STANDALONE = 2
UPDATE = 3
UNINSTALL = 4

Expand Down Expand Up @@ -89,6 +88,8 @@ def __init__(self) -> None:

# Parsed from the command line args, defines how this installer should run.
self.mode:OperationMode = OperationMode.INSTALL

self.is_standalone:bool = False


#
Expand All @@ -101,7 +102,7 @@ def __init__(self) -> None:
# This is the file name of the moonraker service we are targeting.
self._moonraker_service_file_name:Optional[str] = None

# self.ObserverDataPath:Optional[str] = None
self._standalone_data_path:Optional[str] = None


#
Expand Down Expand Up @@ -163,6 +164,16 @@ def has_moonraker_service_file_name(self) -> bool:
"""
return self._moonraker_service_file_name is not None and len(self._moonraker_service_file_name) > 0

@property
def has_standalone_data_path(self) -> bool:
"""
Check if the standalone data path is set.
Returns:
bool: True if the standalone data path is set, False otherwise.
"""
return self._standalone_data_path is not None and len(self._standalone_data_path) > 0

@property
def has_mobileraker_conf_link(self) -> bool:
"""
Expand Down Expand Up @@ -338,6 +349,28 @@ def moonraker_service_file_name(self, value:str) -> None:
"""
self._moonraker_service_file_name = value.strip()

@property
def standalone_data_path(self) -> str:
"""
Get the standalone data path.
Returns:
str: The standalone data path.
"""
if self._standalone_data_path is None:
raise AttributeError("Standalone data path was not set.")
return self._standalone_data_path

@standalone_data_path.setter
def standalone_data_path(self, value:str) -> None:
"""
Set the standalone data path.
Args:
value (str): The standalone data path.
"""
self._standalone_data_path = value.strip()

@property
def printer_data_folder(self) -> str:
"""
Expand Down Expand Up @@ -570,6 +603,9 @@ def validate_phase_two(self) -> None:
Raises:
ValueError: If the mode is INSTALL_STANDALONE and the platform is not Debian based.
"""
if self.is_standalone:
self._validate_path(self._standalone_data_path, "Required config var Standalone Data Path was not found")
return

self._validate_path(self._moonraker_config_file_path, "Required config var Moonraker Config File Path was not found")
self._validate_property(self._moonraker_service_file_name, "Required config var Moonraker Service File Name was not found")
Expand All @@ -581,6 +617,13 @@ def validate_phase_three(self) -> None:
"""
error = "Required config var %s was not found"

if self.is_standalone:
self._validate_path(self._printer_data_logs_folder, error % "Printer Data Logs Folder")
self._validate_property(self._service_file_path, error % "Service File Path")
self._validate_property(self._moonraker_port, error % "Moonraker Port")
self._validate_property(self._mobileraker_conf_path, error % "Mobileraker Conf Path")
return

self._validate_path(self._printer_data_folder, error % "Printer Data Folder")
self._validate_path(self._printer_data_config_folder, error % "Printer Data Config Folder")
self._validate_path(self._printer_data_logs_folder, error % "Printer Data Logs Folder")
Expand Down Expand Up @@ -641,6 +684,9 @@ def parse_bash_args(self):
elif raw_arg == "uninstall":
Logger.Info("Setup running in uninstall mode.")
self.mode = OperationMode.UNINSTALL
elif raw_arg == "standalone":
Logger.Info("Setup running in standalone mode.")
self.is_standalone = True
else:
raise AttributeError("Unknown argument found. Use install.sh -help for options.")

Expand Down
57 changes: 57 additions & 0 deletions installer/DiscoveryStandalone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import os

from .Logging import Logger
from .Context import Context
from .Util import Util

# This class does the same function as the Discovery class, but for companion or Bambu Connect plugins.
# Note that "Bambu Connect" is really just a type of companion plugin, but we use different names so it feels correct.
class DiscoveryStandalone:

# This is the base data folder name that will be used, the plugin id suffix will be added to end of it.
# The folders will always be in the user's home path.
# These MUST start with a . and be LOWER CASE for the matching logic below to work correctly!
# The primary instance (id == "1") will have no "-#" suffix on the folder or service name.
STANDALONE_DATA_FOLDER_LOWER = ".mobileraker_companion-standalone"


def start(self, context:Context):
Logger.Debug("Starting companion discovery.")

# Used for printing the type, like "would you like to install a new {pluginTypeStr} plugin?"
pluginTypeStr = "Standalone"

# Look for existing companion or bambu data installs.
existingCompanionFolders = []
# Sort so the folder we find are ordered from 1-... This makes the selection process nicer, since the ID == selection.
fileAndDirList = sorted(os.listdir(context.user_home))
for fileOrDirName in fileAndDirList:
# Use starts with to see if it matches any of our possible folder names.
# Since each setup only targets companion or bambu connect, only pick the right folder type.
fileOrDirNameLower = fileOrDirName.lower()
if context.is_standalone:
if fileOrDirNameLower.startswith(DiscoveryStandalone.STANDALONE_DATA_FOLDER_LOWER):
existingCompanionFolders.append(fileOrDirName)
Logger.Debug(f"Found existing companion data folder: {fileOrDirName}")
else:
raise Exception("DiscoveryStandalone used in non standalone context.")

# If there's an existing folders, ask the user if they want to use them.
# ToDo :: Add "discover of existing companion data" logic here again (See original code)

# Create a new instance path. Either there is no existing data path or the user wanted to create a new one.
# There is a special case for instance ID "1", we use no suffix. All others will have the suffix.
folderNameRoot = DiscoveryStandalone.STANDALONE_DATA_FOLDER_LOWER
fullFolderName = f"{folderNameRoot}"
self._setup_context_from_vars(context, fullFolderName)
Logger.Info(f"Creating a new {pluginTypeStr} data path. Path: {context.standalone_data_path}")
return


def _setup_context_from_vars(self, context:Context, folderName:str):
# Make the full path
context.standalone_data_path = os.path.join(context.user_home, folderName)

# Ensure the file exists and we have permissions
Util.ensure_dir_exists(context.standalone_data_path, context, True)

Loading

0 comments on commit 67ef129

Please sign in to comment.