diff --git a/.vscode/launch.json b/.vscode/launch.json index c54e0bb..2851e15 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -119,7 +119,7 @@ // The module requires this json object to be passed. // Normally the install.sh script runs, ensure everything is installed, creates a virtural env, and then runs this modlue giving it these args. // But for debugging, we can skip that assuming it's already been ran. - "{\"OE_REPO_DIR\":\"/home/pi/octoeverywhere\",\"OE_ENV\":\"/home/pi/octoeverywhere-env\",\"USERNAME\":\"pi\",\"USER_HOME\":\"/home/pi\",\"CMD_LINE_ARGS\":\"-skipsudoactions -bambu -debug\"}" + "{\"OE_REPO_DIR\":\"/home/pi/octoeverywhere\",\"OE_ENV\":\"/home/pi/octoeverywhere-env\",\"USERNAME\":\"pi\",\"USER_HOME\":\"/home/pi\",\"CMD_LINE_ARGS\":\"-skipsudoactions -bambu\"}" ] }, { diff --git a/py_installer/ConfigHelper.py b/py_installer/ConfigHelper.py index f0bfd9f..6252b33 100644 --- a/py_installer/ConfigHelper.py +++ b/py_installer/ConfigHelper.py @@ -1,6 +1,7 @@ import os from linux_host.config import Config +from bambu_octoeverywhere.bambucloud import BambuCloud from .Logging import Logger from .Context import Context @@ -125,6 +126,23 @@ def WriteBambuDetails(context:Context, accessToken:str, printerSn:str): raise Exception("Failed to write bambu details to config") from e + # Given a context, this will try to find the config file and see if the bambu data is in it. + # If any data is found, it will be returned. If there's no config or the data doesn't exist, it will return None. + # Returns (email:str, password:str) + @staticmethod + def TryToGetBambuCloudData(context:Context = None, configFolderPath:str = None): + try: + # Load the config, if this returns None, there is no existing file. + c = ConfigHelper._GetConfig(context, configFolderPath) + if c is None: + return (None, None) + BambuCloud.Init(Logger.GetPyLogger(), c) + return BambuCloud.Get().GetContext() + except Exception as e: + Logger.Warn("Failed to parse bambu cloud details from existing config. "+str(e)) + return (None, None) + + # # Helpers # diff --git a/py_installer/NetworkConnectors/BambuCloudConnector.py b/py_installer/NetworkConnectors/BambuCloudConnector.py new file mode 100644 index 0000000..3b0ea9b --- /dev/null +++ b/py_installer/NetworkConnectors/BambuCloudConnector.py @@ -0,0 +1,284 @@ +from linux_host.networksearch import NetworkSearch, NetworkValidationResult + +from py_installer.Util import Util +from py_installer.Logging import Logger +from py_installer.Context import Context +from py_installer.ConfigHelper import ConfigHelper + +# A class that helps the user discover, connect, and setup the details required to connect to a remote Bambu Lab printer. +class BambuCloudConnector: + + + def EnsureBambuConnection(self, context:Context): + Logger.Debug("Running bambu connect ensure config logic.") + + # For Bambu printers, we need the IP or Hostname, the port is static, + # and we also need the printer SN and access token. + ip, port = ConfigHelper.TryToGetCompanionDetails(context) + accessCode, printerSn = ConfigHelper.TryToGetBambuData(context) + if ip is not None and port is not None and accessCode is not None and printerSn is not None: + # Check if we can still connect. This can happen if the IP address changes, the user might need to setup the printer again. + Logger.Info(f"Existing bambu config found. IP: {ip} - {printerSn}") + Logger.Info("Checking if we can connect to your Bambu Lab printer...") + result = NetworkSearch.ValidateConnection_Bambu(Logger.GetPyLogger(), ip, accessCode, printerSn, portStr=port, timeoutSec=10.0) + if result.Success(): + Logger.Info("Successfully connected to you Bambu Lab printer!") + # Ensure the X1 camera is setup. + self._EnsureX1CameraSetup(result) + return + else: + # Let the user keep this connection setup, or try to set it up again. + Logger.Blank() + Logger.Warn(f"We failed to connect or authenticate to your printer using {ip}.") + if Util.AskYesOrNoQuestion("Do you want to setup the Bambu Lab printer connection again?") is False: + Logger.Info(f"Keeping the existing Bambu Lab printer connection setup. {ip} - {printerSn}") + return + + ipOrHostname, port, accessToken, printerSn = self._SetupNewBambuConnection(context) + Logger.Info(f"You Bambu printer was found and authentication was successful! IP: {ipOrHostname}") + + # Ensure the X1 camera is setup. + self._EnsureX1CameraSetup(None, ipOrHostname, accessToken, printerSn) + + ConfigHelper.WriteCompanionDetails(context, ipOrHostname, port) + ConfigHelper.WriteBambuDetails(context, accessToken, printerSn) + Logger.Blank() + Logger.Header("Bambu Connection successful!") + Logger.Blank() + + + # Helps the user setup a bambu connection via auto scanning or manual setup. + # Returns (ip:str, port:str, accessToken:str, printerSn:str) + def _SetupNewBambuConnection(self, context:Context): + while True: + Logger.Blank() + Logger.Blank() + Logger.Blank() + Logger.Header("##################################") + Logger.Header(" Bambu Lab Printer Setup") + Logger.Header("##################################") + Logger.Blank() + Logger.Info("OctoEverywhere Bambu Connect needs to connect to your Bambu Lab printer to provide remote access.") + Logger.Info("Bambu Connect needs your printer's Access Code and Serial Number to connect to your printer.") + Logger.Info("If you have any trouble, we are happy to help! Contact us at support@octoeverywhere.com") + + # Try to get an an existing access code or SN, so the user doesn't have to re-enter them if they are already there. + oldConfigAccessCode, oldConfigPrinterSn = ConfigHelper.TryToGetBambuData(context) + + # Get the access code. + accessCode = None + while True: + Logger.Blank() + Logger.Blank() + Logger.Header("We need your Bambu Lab printer's Access Code to connect.") + Logger.Info("The Access Code can be found using the screen on your printer, in the Network settings.") + Logger.Blank() + Logger.Warn("Follow this link for a step-by-step guide to find the Access Code for your printer:") + Logger.Warn("https://octoeverywhere.com/s/access-code") + Logger.Blank() + Logger.Info("The access code is case sensitive - make sure to enter it exactly as shown on your printer.") + Logger.Blank() + + # If there is already an access code, ask if the user wants to use it. + if oldConfigAccessCode is not None and len(oldConfigAccessCode) > 0: + Logger.Info(f"Your previously entered Access Code is: '{oldConfigAccessCode}'") + if Util.AskYesOrNoQuestion("Do you want to continue using this Access Code?"): + accessCode = oldConfigAccessCode + break + # Set it to None so we wont ask again. + oldConfigAccessCode = None + Logger.Blank() + Logger.Blank() + + # Ask for the access code. + accessCode = input("Enter your printer's Access Code: ") + + # Validate + # The access code IS CASE SENSITIVE and can letters and numbers. + accessCode = accessCode.strip() + if len(accessCode) != 8: + if Util.AskYesOrNoQuestion(f"The Access Code should be 8 numbers, you have entered {len(accessCode)}. Do you want to try again? "): + continue + + retryEntry = False + for c in accessCode: + if not c.isdigit() and not c.isalpha(): + if Util.AskYesOrNoQuestion("The Access Code should only be letters and numbers, you seem to have entered something else. Do you want to try again? "): + retryEntry = True + break + if retryEntry: + continue + + # Accept the input. + break + + + Logger.Blank() + Logger.Blank() + Logger.Blank() + + # Get the serial number. + printerSn = None + while True: + Logger.Blank() + Logger.Header("Finally, Bambu Connect needs your Bambu Lab printer's Serial Number to connect.") + Logger.Info("The Serial Number is required for authentication when the printer's local network protocol.") + Logger.Info("Your Serial Number and Access Code are only stored on this device and will not be uploaded.") + Logger.Blank() + Logger.Warn("Follow this link for a step-by-step guide to find the Serial Number for your printer:") + Logger.Warn("https://octoeverywhere.com/s/bambu-sn") + Logger.Blank() + + # If there is already an sn, ask if the user wants to use it. + if oldConfigPrinterSn is not None and len(oldConfigPrinterSn) > 0: + Logger.Info(f"Your previously entered Serial Number is: '{oldConfigPrinterSn}'") + if Util.AskYesOrNoQuestion("Do you want to continue using this Serial Number?"): + printerSn = oldConfigPrinterSn + break + # Set it to None so we wont ask again. + oldConfigPrinterSn = None + Logger.Blank() + Logger.Blank() + + # Ask for the sn. + printerSn = input("Enter your printer's Serial Number: ") + + # The SN should always be upper case letters. + printerSn = printerSn.strip().upper() + + # Validate + # It seems the SN are 15 digits + if len(printerSn) != 15: + if Util.AskYesOrNoQuestion(f"The Serial Number is usually 15 letters or numbers, you have entered {len(printerSn)}. Do you want to try again? "): + continue + + retryEntry = False + for c in printerSn: + if not c.isdigit() and not c.isalpha(): + if Util.AskYesOrNoQuestion("The Serial Number should only be letters and numbers, you seem to have entered something else. Do you want to try again? "): + retryEntry = True + break + if retryEntry: + continue + + # Accept the input. + break + + # Scan for the local IP subset for possible matches. + Logger.Blank() + Logger.Blank() + Logger.Warn("Searching for your Bambu printer on your network, this will take about 5 seconds...") + ips = NetworkSearch.ScanForInstances_Bambu(Logger.GetPyLogger(), accessCode, printerSn) + + Logger.Blank() + Logger.Blank() + + # There should only be one IP found or none, because there should be no other printer that matches the same access code and printer serial number. + if len(ips) == 1: + ip = ips[0] + return (ip, NetworkSearch.c_BambuDefaultPortStr, accessCode, printerSn) + + Logger.Blank() + Logger.Blank() + Logger.Blank() + Logger.Error("We were unable to automatically find your printer on your network using these details:") + Logger.Info(f" Access Code: {accessCode}") + Logger.Info(f" Serial Number: {printerSn}") + Logger.Blank() + Logger.Header("Make sure your printer is on a full booted and verify the values above are correct.") + Logger.Blank() + if not Util.AskYesOrNoQuestion("Are the Access Code and Serial Number correct?"): + # Loop back to the very top, to restart the entire setup, allowing the user to enter their values again. + Logger.Blank() + Logger.Blank() + Logger.Blank() + Logger.Blank() + continue + + # Enter manual IP setup mode + while True: + Logger.Blank() + Logger.Blank() + Logger.Info("Since we can't automatically find your printer, we can get the IP address manually.") + Logger.Info("You Bambu printer's IP address can be found using the screen on your printer, in the Networking settings.") + Logger.Blank() + Logger.Warn("Follow this link for a step-by-step guide to find the IP address of your printer:") + Logger.Warn("https://octoeverywhere.com/s/bambu-ip") + Logger.Blank() + ip = input("Enter your printer's IP Address: ") + ip = ip.strip() + Logger.Blank() + Logger.Info("Trying to connect to your printer...") + result = NetworkSearch.ValidateConnection_Bambu(Logger.GetPyLogger(), ip, accessCode, printerSn, timeoutSec=10.0) + Logger.Blank() + Logger.Blank() + if result.Success(): + return (ip, NetworkSearch.c_BambuDefaultPortStr, accessCode, printerSn) + if result.FailedToConnect: + Logger.Error("Failed to connect to your Bambu printer, ensure the IP address is correct and the printer is connected to the network.") + elif result.FailedAuth: + Logger.Error("Failed to connect to your Bambu printer, the Access Code was incorrect.") + _ = input("Press any key to continue.") + # Breaking this loop will return us to the main setup loop, which will do the entire access code and sn entry again. + break + elif result.FailedSerialNumber: + Logger.Error("Failed to connect to your Bambu printer, the Serial Number was incorrect.") + _ = input("Press any key to continue.") + # Breaking this loop will return us to the main setup loop, which will do the entire access code and sn entry again. + break + else: + Logger.Error("Failed to connect to your Bambu printer.") + + # If we got here, the IP address is wrong or something else. + Logger.Blank() + Logger.Info("Pick one of the following:") + Logger.Info(" 1) Enter the IP address again.") + Logger.Info(" 2) Enter your Access Code and Serial Number.") + Logger.Blank() + c = input("Pick one or two: ") + try: + cInt = int(c.strip()) + if cInt == 1: + # Restart IP entry. + continue + else: + # Breaking this loop will return us to the main setup loop, which will do the entire access code and sn entry again. + break + except Exception: + # Default to a full restart. + # Breaking this loop will return us to the main setup loop, which will do the entire access code and sn entry again. + break + + + # Given either a validation result or required details, this gets the x1 carbon's ip camera state + # and ensures it's enabled properly + def _EnsureX1CameraSetup(self, result:NetworkValidationResult = None, ipOrHostname:str = None, accessToken:str = None, printerSn:str = None): + # If we didn't get passed a result, get one now. + if result is None: + result = NetworkSearch.ValidateConnection_Bambu(Logger.GetPyLogger(), ipOrHostname, accessToken, printerSn) + # If we didn't get something, it's a failure. + if result is None: + Logger.Error("Ensure camera failed to get a validation result.") + return + # Ensure success. + if result.Success() is False: + Logger.Error("Ensure camera got a validation result that failed.") + return + # If the bambu rtsp url is None, this printer doesn't support RTSP. + if result.BambuRtspUrl is None: + Logger.Info("This printer doesn't use RTSP camera streaming, skipping setup.") + return + # If there is a string and the string isn't 'disable' then we are good to go. + if len(result.BambuRtspUrl) > 0 and "disable" not in result.BambuRtspUrl: + Logger.Info(f"RTSP Liveview is enabled. '{result.BambuRtspUrl}'") + return + # Tell the user how to enable it. + Logger.Info("") + Logger.Info("") + Logger.Header("You need to enable 'LAN Mode Liveview' on your printer for webcam access.") + Logger.Blank() + Logger.Warn("Don't worry, you just need to flip a switch in the settings on your printer.") + Logger.Warn("Follow this link for a step-by-step guide:") + Logger.Warn("https://octoeverywhere.com/s/liveview") + Logger.Blank() + Util.AskYesOrNoQuestion("Did you enable 'LAN Mode Liveview'?") diff --git a/py_installer/NetworkConnectors/BambuConnector.py b/py_installer/NetworkConnectors/BambuConnector.py index f6ff9da..e12772e 100644 --- a/py_installer/NetworkConnectors/BambuConnector.py +++ b/py_installer/NetworkConnectors/BambuConnector.py @@ -5,6 +5,8 @@ from py_installer.Context import Context from py_installer.ConfigHelper import ConfigHelper +from .BambuCloudConnector import BambuCloudConnector + # A class that helps the user discover, connect, and setup the details required to connect to a remote Bambu Lab printer. class BambuConnector: @@ -12,10 +14,77 @@ class BambuConnector: def EnsureBambuConnection(self, context:Context): Logger.Debug("Running bambu connect ensure config logic.") - # For Bambu printers, we need the IP or Hostname, the port is static, - # and we also need the printer SN and access token. + # As of June of 2024, Bambu updated the firmware of the printers to only support direct connections while in LAN only mode. + # So now, we must give the user a choice, about how they want to setup Bambu Connect. + # Gather any data we have currently. + email, password = ConfigHelper.TryToGetBambuCloudData(context) + #accessCode, printerSn = ConfigHelper.TryToGetBambuData(context) + + # If either of these are set, this setup is using the cloud, so use it to ensure the connection. + if email is not None or password is not None: + bcc = BambuCloudConnector() + bcc.EnsureBambuConnection(context) + # We only get the access code for the lan only setup, so for now, we use it to determine which type this install is. + # TODO - As a temp measure to help users from old setups go to the new one, we always ask if there's an access code. + # if accessCode is not None: + # self._EnsureBambuConnection_Internal(self, context) + + # If neither are set, this is a first time install, so we need to ask the user how they want to setup Bambu Connect. + Logger.Blank() + Logger.Blank() + Logger.Blank() + Logger.Header("##################################") + Logger.Header(" Bambu Lab Printer Setup") + Logger.Header("##################################") + Logger.Blank() + Logger.Warn("As of June of 2024, Bambu updated the printer firmware to only allow local network access for") + Logger.Warn("3rd party services when the printer is in LAN only mode. We support two connection methods.") + Logger.Info(" 1) Allow access via the Bambu Cloud.") + Logger.Info(" 2) Allow local access by enabling LAN only mode on your printer.") + Logger.Blank() + Logger.Header("Access Via Bambu Cloud") + Logger.Info("Requires you to provide your Bambu Cloud email address and password.") + Logger.Info("Rest assured, your Bambu Cloud email address and password are stored locally, secured on disk, and are never sent to the OctoEverywhere service.") + Logger.Blank() + Logger.Info(" - OR -") + Logger.Blank() + Logger.Header("Access Locally via LAN Only Mode") + Logger.Info("You must have LAN only mode enabled on your printer, which will disconnect the Bambu Cloud.") + Logger.Info("However, you CAN still use Bambu Studio and Bambu Handy when your on the same network.") + + useBambuCloud = None + while useBambuCloud is None: + Logger.Blank() + Logger.Header("Which would you like to use?") + Logger.Info(" 1) Access via Bambu Cloud") + Logger.Info(" 2) Local access with LAN Only Mode") + Logger.Blank() + decision:str = input("Enter the option number: ") + try: + i = int(decision.strip()) + if i == 1: + useBambuCloud = True + break + elif i == 2: + useBambuCloud = False + break + except Exception: + continue + Logger.Error("Invalid input, try again.") + + if useBambuCloud: + bcc = BambuCloudConnector() + bcc.EnsureBambuConnection(context) + else: + self._EnsureBambuConnection_Internal(context) + + + def _EnsureBambuConnection_Internal(self, context:Context): ip, port = ConfigHelper.TryToGetCompanionDetails(context) accessCode, printerSn = ConfigHelper.TryToGetBambuData(context) + + # For Bambu printers, we need the IP or Hostname, the port is static, + # and we also need the printer SN and access token. if ip is not None and port is not None and accessCode is not None and printerSn is not None: # Check if we can still connect. This can happen if the IP address changes, the user might need to setup the printer again. Logger.Info(f"Existing bambu config found. IP: {ip} - {printerSn}") @@ -53,11 +122,6 @@ def _SetupNewBambuConnection(self, context:Context): while True: Logger.Blank() Logger.Blank() - Logger.Blank() - Logger.Header("##################################") - Logger.Header(" Bambu Lab Printer Setup") - Logger.Header("##################################") - Logger.Blank() Logger.Info("OctoEverywhere Bambu Connect needs to connect to your Bambu Lab printer to provide remote access.") Logger.Info("Bambu Connect needs your printer's Access Code and Serial Number to connect to your printer.") Logger.Info("If you have any trouble, we are happy to help! Contact us at support@octoeverywhere.com") @@ -68,7 +132,6 @@ def _SetupNewBambuConnection(self, context:Context): # Get the access code. accessCode = None while True: - Logger.Blank() Logger.Blank() Logger.Header("We need your Bambu Lab printer's Access Code to connect.") Logger.Info("The Access Code can be found using the screen on your printer, in the Network settings.") @@ -185,6 +248,7 @@ def _SetupNewBambuConnection(self, context:Context): Logger.Info(f" Access Code: {accessCode}") Logger.Info(f" Serial Number: {printerSn}") Logger.Blank() + Logger.Warn("Remember that your Bambu Lab 3D printer must be in LAN Only Mode and restarted after the change.") Logger.Header("Make sure your printer is on a full booted and verify the values above are correct.") Logger.Blank() if not Util.AskYesOrNoQuestion("Are the Access Code and Serial Number correct?"):