Skip to content

Commit 78b34b3

Browse files
committed
Small fixes to make the Elegoo connection more reliable.
1 parent 0f4048d commit 78b34b3

File tree

8 files changed

+68
-26
lines changed

8 files changed

+68
-26
lines changed

elegoo_octoeverywhere/elegooclient.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Any, Dict, List, Optional
88

99
from octoeverywhere.compat import Compat
10+
from octoeverywhere.repeattimer import RepeatTimer
1011
from octoeverywhere.sentry import Sentry
1112
from octoeverywhere.websocketimpl import Client
1213
from octoeverywhere.octohttprequest import OctoHttpRequest
@@ -184,7 +185,7 @@ def IsDisconnectDueToTooManyClients(self) -> bool:
184185

185186
# Sends a command to the printer to enable the webcam.
186187
def SendEnableWebcamCommand(self, waitForResponse:bool=True) -> ResponseMsg:
187-
return ElegooClient.Get().SendRequest(386, {"Enable":1}, waitForResponse=waitForResponse)
188+
return self.SendRequest(386, {"Enable":1}, waitForResponse=waitForResponse)
188189

189190

190191
# Sends a command to all connected frontends to show a popup message.
@@ -205,6 +206,8 @@ def SendFrontendPopupMsg(self, title:str, text:str, msgType:str, actionText:Opti
205206

206207
# Sends a request to the printer and waits for a response.
207208
# Always returns a ResponseMsg, with various error codes.
209+
# Full protocol:
210+
# https://github.com/cbd-tech/SDCP-Smart-Device-Control-Protocol-V3.0.0/blob/main/SDCP(Smart%20Device%20Control%20Protocol)_V3.0.0_EN.md
208211
def SendRequest(self, cmdId:int, data:Optional[Dict[str, Any]]=None, waitForResponse:bool=True, timeoutSec:Optional[float]=None) -> ResponseMsg:
209212
# Generate a request id, which is a 32 char lowercase letter and number string
210213
requestId = ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))
@@ -330,11 +333,15 @@ def _ClientWorker(self):
330333
self.WebSocket = Client(url, onWsOpen=self._OnWsConnect, onWsClose=self._OnWsClose, onWsError=self._OnWsError, onWsData=self._OnWsData)
331334
self.WebSocket.SetDisableCertCheck(True)
332335

333-
# Connect to the server
334-
with self.WebSocket:
335-
# Use a more aggressive ping timeout because if the printer power cycles, we don't get the TCP close message.
336-
# Time ping timeout must be less than the ping interval.
337-
self.WebSocket.RunUntilClosed(pingIntervalSec=30)
336+
# Important! The connection to the print will close after 1 minute if we don't send any messages.
337+
# Even if the websocket sends the ws ping message, it doesn't seem to reset the idle timer.
338+
# So, we will use a repeat timer to send the SDCP protocol ping message every 50 seconds.
339+
with RepeatTimer(self.Logger, "ElegooClientWsMsgKeepalive", 50.0, self._RepeatTimerKeepaliveTick) as t:
340+
t.start()
341+
# Connect to the server
342+
with self.WebSocket:
343+
# We use a ping payload of "ping" because it's what the web portal uses.
344+
self.WebSocket.RunUntilClosed(pingPayload="ping")
338345
except Exception as e:
339346
Sentry.OnException("Elegoo client exception in main WS loop.", e)
340347

@@ -357,6 +364,16 @@ def _ClientWorker(self):
357364
self.SleepEvent.clear()
358365

359366

367+
def _RepeatTimerKeepaliveTick(self):
368+
# Any message works, but this is the lightest weight.
369+
# We don't care about the result
370+
if self._WebSocketSend(Buffer("ping".encode("utf-8"))) is False:
371+
localWs = self.WebSocket
372+
if localWs is not None and self.WebSocketConnected is True:
373+
self.Logger.info("Elegoo client - keepalive tick failed to send ping message, closing the websocket.")
374+
localWs.Close()
375+
376+
360377
# Fired whenever the client is disconnected, we need to clean up the state since it's now unknown.
361378
def _CleanupStateOnDisconnect(self):
362379
self.State = None
@@ -368,7 +385,7 @@ def _CleanupStateOnDisconnect(self):
368385

369386
# Fired when the websocket is connected.
370387
def _OnWsConnect(self, ws:IWebSocketClient):
371-
self.Logger.info("Connection to the Elegoo printer established!")
388+
self.Logger.debug("Elegoo websocket established")
372389
LocalWebApi.Get().SetPrinterConnectionState(True)
373390

374391
# Set the connected flag now, so we can send messages.
@@ -519,7 +536,7 @@ def _HandleAttributesUpdate(self, attributes:Dict[str, Any]):
519536
s = PrinterAttributes(self.Logger)
520537
s.OnUpdate(attributes)
521538
self.Attributes = s
522-
self.Logger.info("Elegoo printer attributes object created.")
539+
self.Logger.debug("Elegoo printer attributes object created.")
523540
else:
524541
self.Attributes.OnUpdate(attributes)
525542
except Exception as e:
@@ -573,7 +590,7 @@ def _HandleAttributesUpdate(self, attributes:Dict[str, Any]):
573590

574591
# We have a match, so we are good.
575592
self.Config.SetStr(Config.SectionCompanion, Config.CompanionKeyIpOrHostname, wsConIp)
576-
self.Logger.info("Elegoo client connection finalized.")
593+
self.Logger.info("Elegoo client connection fully connected.")
577594

578595

579596
def _HandleStatusUpdate(self, status:Dict[str, Any]):
@@ -585,7 +602,7 @@ def _HandleStatusUpdate(self, status:Dict[str, Any]):
585602
s = PrinterState(self.Logger)
586603
s.OnUpdate(status)
587604
self.State = s
588-
self.Logger.info("Elegoo printer state object created.")
605+
self.Logger.debug("Elegoo printer state object created.")
589606
else:
590607
self.State.OnUpdate(status)
591608
except Exception as e:

elegoo_octoeverywhere/slipstream.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def _GetAndProcessIndex(self):
209209
with self.Lock:
210210
size = len(self.Cache)
211211

212-
self.Logger.info("Slipstream took "+str(time.time()-start)+f" to fully update the cache of {size} files.")
212+
self.Logger.debug("Slipstream took "+str(time.time()-start)+f" to fully update the cache of {size} files.")
213213

214214

215215
# On success returns the fully ready OctoHttpResult object.

linux_host/logger.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import logging.handlers
55
from typing import Optional
66

7+
from octoeverywhere.websocketimpl import Client
8+
79
from .config import Config
810

911
class LoggerInit:
@@ -39,6 +41,9 @@ def GetLogger(config:Config, logDir:str, logLevelOverride:Optional[str]) -> logg
3941
# Set the final log level.
4042
logger.setLevel(logLevel)
4143

44+
# Set the websocket lib debug level if needed.
45+
Client.SetWebsocketDebuggingLevel(logLevel == "DEBUG")
46+
4247
# Define our format
4348
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
4449

moonraker_octoeverywhere/moonrakerclient.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -770,12 +770,12 @@ def _onWsClose(self, ws:IWebSocketClient) -> None:
770770
def _onWsError(self, ws:IWebSocketClient, exception:Exception) -> None:
771771
if Sentry.IsCommonConnectionException(exception):
772772
# Don't bother logging, this just means there's no server to connect to.
773-
pass
774-
elif isinstance(exception, octowebsocket.WebSocketBadStatusException) and "Handshake status" in str(exception):
773+
return
774+
if isinstance(exception, octowebsocket.WebSocketBadStatusException) and "Handshake status" in str(exception):
775775
# This is moonraker specific, we sometimes see stuff like "Handshake status 502 Bad Gateway"
776776
self.Logger.info(f"Failed to connect to moonraker due to bad gateway stats. {exception}")
777-
else:
778-
Sentry.OnException("Exception rased from moonraker client websocket connection. The connection will be closed.", exception)
777+
return
778+
Sentry.OnException("Exception raised from moonraker client websocket connection. The connection will be closed.", exception)
779779

780780

781781
# A helper class used for waiting rpc requests

octoeverywhere/notificationshandler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def OnRestorePrintIfNeeded(self, isPrinting:bool, isPaused:bool, printCookie_Can
236236
self.StopTimers()
237237
return
238238
else:
239-
self.Logger.info("Restore client sync state: There's no print and none of the timers are running.")
239+
self.Logger.debug("Restore client sync state: There's no print and none of the timers are running.")
240240
return
241241

242242

octoeverywhere/repeattimer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def run(self):
2626
self.callback()
2727
except Exception as e:
2828
Sentry.OnException("Exception in RepeatTimer thread.", e)
29-
self.logger.info("RepeatTimer thread exit")
29+
self.logger.debug("RepeatTimer thread exit")
3030

3131

3232
# Used to update the repeat interval. This can be called while the timer is running

octoeverywhere/sentry.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,13 +330,18 @@ def _HandleCantCreateThreadException(logger:logging.Logger, e:Exception) -> bool
330330
@staticmethod
331331
def IsCommonConnectionException(e:Exception) -> bool:
332332
def matchesException(exception:Exception, exceptionClass:Any, msgs:Optional[List[str]]=None) -> bool:
333+
# First, ensure that the exception matches the one we are filtering for.
333334
if not isinstance(exception, exceptionClass):
334335
return False
336+
# If no messages are provided, then any exception of this type matches.
337+
if msgs is None:
338+
return True
339+
# Check the exception string for any of the provided messages.
335340
exceptionStr = str(exception).lower().strip()
336-
if msgs is not None:
337-
for t in msgs:
338-
if t.lower() in exceptionStr:
339-
return True
341+
for t in msgs:
342+
if t.lower() in exceptionStr:
343+
return True
344+
# If no messages matched, then we matched the exception type, but not the message, so return false.
340345
return False
341346

342347
try:
@@ -366,6 +371,9 @@ def matchesException(exception:Exception, exceptionClass:Any, msgs:Optional[List
366371
# We don't care.
367372
if matchesException(e, octowebsocket.WebSocketConnectionClosedException):
368373
return True
374+
# If the ping pong tim
375+
if matchesException(e, octowebsocket.WebSocketTimeoutException, ["ping/pong timed out"]):
376+
return True
369377
except Exception:
370378
pass
371379
return False

octoeverywhere/websocketimpl.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import queue
22
import ssl
33
import threading
4+
import logging
45
from typing import Any, Dict, List, Callable, Optional
56

67
import certifi
@@ -15,6 +16,20 @@
1516
# This class gives a bit of an abstraction over the normal ws
1617
class Client(IWebSocketClient):
1718

19+
20+
# Allows us to still enable the websocket debug logs if we want.
21+
@staticmethod
22+
def SetWebsocketDebuggingLevel(debug:bool) -> None:
23+
# The websocket lib logs quite a lot of stuff, even to info. It will also always logs errors,
24+
# even after our handler had handled them. So we will disable it by default.
25+
wsLibLogger = logging.getLogger("websocket")
26+
if debug is False:
27+
wsLibLogger.disabled = True
28+
return
29+
wsLibLogger.disabled = False
30+
wsLibLogger.setLevel(logging.DEBUG)
31+
32+
1833
def __init__(
1934
self,
2035
url:str,
@@ -47,9 +62,6 @@ def __init__(
4762
self.SendQueue:queue.Queue[SendQueueContext] = queue.Queue()
4863
self.SendThread:threading.Thread = None #pyright: ignore[reportAttributeAccessIssue]
4964

50-
# Used to log more details about what's going on with the websocket.
51-
# websocket.enableTrace(True)
52-
5365
# Used to indicate if the client has started to close this WS. If so, we won't fire
5466
# any errors.
5567
self.hasClientRequestedClose = False
@@ -105,7 +117,7 @@ def SetDisableCertCheck(self, disable:bool):
105117

106118

107119
# Runs the websocket blocking until it closes.
108-
def RunUntilClosed(self, pingIntervalSec:Optional[int]=None, pingTimeoutSec:Optional[int]=None):
120+
def RunUntilClosed(self, pingIntervalSec:Optional[int]=None, pingTimeoutSec:Optional[int]=None, pingPayload:str="") -> None:
109121
#
110122
# The client is responsible for sending keep alive pings the server will then pong respond to.
111123
# If that's not done, the connection will timeout. We will send a ping every 10 minutes.
@@ -151,7 +163,7 @@ def RunUntilClosed(self, pingIntervalSec:Optional[int]=None, pingTimeoutSec:Opti
151163
if self.isClosed:
152164
return
153165

154-
self.Ws.run_forever(skip_utf8_validation=True, ping_interval=pingIntervalSec, ping_timeout=pingTimeoutSec, sslopt=sslopt) #pyright: ignore[reportUnknownMemberType]
166+
self.Ws.run_forever(skip_utf8_validation=True, ping_interval=pingIntervalSec, ping_timeout=pingTimeoutSec, sslopt=sslopt, ping_payload=pingPayload) #pyright: ignore[reportUnknownMemberType]
155167
except Exception as e:
156168
# There's a compat issue where run_forever will try to access "isAlive" when the socket is closing
157169
# "isAlive" apparently doesn't exist in some PY versions of thread, so this throws. We will ignore that error,

0 commit comments

Comments
 (0)