Skip to content

Commit

Permalink
Better layer notifications (#79)
Browse files Browse the repository at this point in the history
* Pre-process files for OctoPrint to determine layers during print

* Download file when print is started to parse layers

* Add comment

* Fix random submodule

* Let OctoPrint plugin parse files to determine layers

* Fix custom notifications

* Do not send custom notification to activity

* Test and fix on Moonraker

* Bump version to 2.1.0

* Fix snapshot logic

* Fix some small things
  • Loading branch information
crysxd authored Jul 2, 2024
1 parent 2d7f0b2 commit a4c3f76
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 319 deletions.
24 changes: 24 additions & 0 deletions moonraker_octoapp/filemetadatacache.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(self, moonrakerClient) -> None:
self.FirstLayerHeight:float = -1.0
self.LayerHeight:float = -1.0
self.ObjectHeight:float = -1.0
self.Modified:float = 0.0
self.ResetCache()


Expand All @@ -38,6 +39,7 @@ def ResetCache(self):
self.FirstLayerHeight = -1.0
self.LayerHeight = -1.0
self.ObjectHeight = -1.0
self.Modified:float = 0.0


# If the estimated time for the print can be gotten from the file metadata, this will return it.
Expand Down Expand Up @@ -106,7 +108,24 @@ def GetLayerInfo(self, filename:str):

# Return the value, which could still be -1 if it failed.
return (self.LayerCount, self.LayerHeight, self.FirstLayerHeight, self.ObjectHeight)


# If the file size can be gotten from the file metadata, this will return it.
# Any of the values will return -1 if they are unknown.
def GetModified(self, filename:str):
# Check to see if we have checked for this file before.
if self.FileName is not None:
# Check if it's the same file, case sensitive.
if self.FileName == filename:
# Return the last result, this maybe valid or not.
return self.Modified

# The filename changed or we don't have one at all, do a refresh now.
self._RefreshFileMetaDataCache(filename)

# Return the value, which could still be -1 if it failed.
return self.Modified


# Does a refresh of the file name metadata cache.
def _RefreshFileMetaDataCache(self, filename:str) -> None:
Expand Down Expand Up @@ -139,6 +158,10 @@ def _RefreshFileMetaDataCache(self, filename:str) -> None:
value = int(res["size"])
if value > 0:
self.FileSizeKBytes = int(value / 1024)
if "modified" in res:
value = res["modified"]
if value > 0:
self.Modified = value
if "filament_total" in res:
value = int(res["filament_total"])
if value > 0:
Expand All @@ -160,4 +183,5 @@ def _RefreshFileMetaDataCache(self, filename:str) -> None:
if value > 0:
self.ObjectHeight = value


Sentry.Info("Metadata Cache", f"FileMetadataCache updated for file [{filename}]; est time: {str(self.EstimatedPrintTimeSec)}, size: {str(self.FileSizeKBytes)}, filament usage: {str(self.EstimatedFilamentUsageMm)}")
75 changes: 68 additions & 7 deletions moonraker_octoapp/moonrakerclient.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import os
import sys
import threading
import time
import json
import queue
import logging
import math
import urllib.request
from urllib.parse import quote

import configparser
from octoapp.compat import Compat

from octoapp.sentry import Sentry
from octoapp.notificationutils import NotificationUtils
from octoapp.websocketimpl import Client
from octoapp.notificationshandler import NotificationsHandler
from .moonrakercredentailmanager import MoonrakerCredentialManager
Expand Down Expand Up @@ -81,6 +85,7 @@ def __init__(self, isObserverMode:bool, moonrakerConfigFilePath:str, observerCon
self.ConnectionStatusHandler = connectionStatusHandler
self.PluginVersionStr = pluginVersionStr
self.MoonrakerDatabase = moonrakerDatabase
self.ScheduledNotificationsCache = {}

# Setup the json-rpc vars
self.JsonRpcIdLock = threading.Lock()
Expand Down Expand Up @@ -351,6 +356,7 @@ def _OnWsNonResponseMessage(self, msg:str):
if "filename" in jobObj:
fileName = jobObj["filename"]
self.MoonrakerCompat.OnPrintStart(fileName)
self.DownloadFileForProcessing(fileName)
return
elif action == "finished":
# This can be a finish canceled or failed.
Expand All @@ -374,6 +380,7 @@ def _OnWsNonResponseMessage(self, msg:str):
if method == "notify_status_update":
# This is shared by a few things, so get it once.
progressFloat_CanBeNone = self._GetProgressFromMsg(msg)
filePos_CanBeNone = self._GetFilePosFromMsg(msg)

# Check for a state container
stateContainerObj = self._GetWsMsgParam(msg, "print_stats")
Expand All @@ -399,13 +406,35 @@ def _OnWsNonResponseMessage(self, msg:str):

# Report progress. Do this after the others so they will report before a potential progress update.
# Progress updates super frequently (like once a second) so there's plenty of chances.
if progressFloat_CanBeNone is not None:
self.MoonrakerCompat.OnPrintProgress(progressFloat_CanBeNone)
if progressFloat_CanBeNone is not None and filePos_CanBeNone is not None:
self.MoonrakerCompat.OnPrintProgress(progressFloat_CanBeNone, filePos_CanBeNone)

# When the webcams change, kick the webcam helper.
if method == "notify_webcams_changed":
self.ConnectionStatusHandler.OnWebcamSettingsChanged()

def DownloadFileForProcessing(self, fileName):
try:
modified = FileMetadataCache.Get().GetModified(fileName)
cacheKey = fileName + ":" + str(modified)

if cacheKey in self.ScheduledNotificationsCache.keys():
Sentry.Info("Client", "Reusing cached notifications for " + fileName)
self.MoonrakerCompat.ScheduleNotifications(self.ScheduledNotificationsCache[cacheKey])
return

path = '/'.join(list(map(quote, fileName.split("/"))))
url = "http://" + self.MoonrakerHostAndPort + "/server/files/gcodes/" + path
Sentry.Info("Client", "Processing file at %s" % url)
with urllib.request.urlopen(url) as response:
notifications = NotificationUtils.ExtractNotifications(response)
self.MoonrakerCompat.ScheduleNotifications(notifications)
Sentry.Info("Client", "File processed")
self.ScheduledNotificationsCache[cacheKey] = notifications

except Exception as e:
Sentry.ExceptionNoSend("Failed to download file for notification processing", e)


# If the message has a progress contained in the virtual_sdcard, this returns it. The progress is a float from 0.0->1.0
# Otherwise None
Expand All @@ -417,6 +446,17 @@ def _GetProgressFromMsg(self, msg):
return vsd["progress"]
return None


# If the message has a progress contained in the virtual_sdcard, this returns it
# Otherwise None
def _GetFilePosFromMsg(self, msg):
vsdContainerObj = self._GetWsMsgParam(msg, "virtual_sdcard")
if vsdContainerObj is not None:
vsd = vsdContainerObj["virtual_sdcard"]
if "file_position" in vsd:
return vsd["file_position"]
return None


# Given a property name, returns the correct param object that contains that object.
def _GetWsMsgParam(self, msg, paramName):
Expand Down Expand Up @@ -719,6 +759,11 @@ def __init__(self, printerId:str) -> None:
self.NotificationHandler = NotificationsHandler(self)
# self.NotificationHandler.SetPrinterId(printerId)

# Set the LastFilePos to a high value so the layer notifications are not send if the plugin
# is started during a print
self.LastFilePos = sys.maxsize
self.ScheduledNotifications = {}


def GetNotificationHandler(self):
return self.NotificationHandler
Expand Down Expand Up @@ -815,12 +860,22 @@ def OnPrintStart(self, fileName):

# Fire on started.
self.NotificationHandler.OnStarted(fileName, fileSizeKBytes, filamentUsageMm)
self.ScheduledNotifications = {}
self.LastFilePos = 0

def _updatePrinterName(self):
# Get our name
name = MoonrakerClient.Get().MoonrakerDatabase.GetPrinterName()
self.NotificationHandler.NotificationSender.PrinterName = name
Sentry.Info("Client", "Printer is called %s" % name)
try:
# Get our name
name = MoonrakerClient.Get().MoonrakerDatabase.GetPrinterName()
self.NotificationHandler.NotificationSender.PrinterName = name
Sentry.Info("Client", "Printer is called %s" % name)
except Exception as e:
Sentry.ExceptionNoSend("Failed to update printer name", e)


def ScheduleNotifications(self, notifications):
self.ScheduledNotifications = notifications


def OnDone(self):
# Only process notifications when ready, aka after state sync.
Expand Down Expand Up @@ -873,10 +928,14 @@ def OnPrintResumed(self):


# Called when there's a print percentage progress update.
def OnPrintProgress(self, progressFloat):
def OnPrintProgress(self, progressFloat, filePos):
# Only process notifications when ready, aka after state sync.
if self.IsReadyToProcessNotifications is False:
return

# Trigger scheduled notifications
NotificationUtils.SendScheduledNotifications(self.ScheduledNotifications, self.NotificationHandler, filePos, self.LastFilePos)
self.LastFilePos = filePos

# Moonraker sends about 3 of these per second, which is way faster than we need to process them.
nowSec = time.time()
Expand Down Expand Up @@ -1070,6 +1129,8 @@ def _InitPrintStateForFreshConnect(self):
Sentry.Info("Client", "Printer state at socket connect is: "+state)
self._updatePrinterName()
self.NotificationHandler.OnRestorePrintIfNeeded(state, fileName_CanBeNone, totalDurationFloatSec_CanBeNone)
if fileName_CanBeNone:
MoonrakerClient.Get().DownloadFileForProcessing(fileName_CanBeNone)


# Queries moonraker for the current printer stats.
Expand Down
42 changes: 42 additions & 0 deletions octoapp/layerutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

class LayerUtils:

LayerChangeCommand = "OCTOAPP_LAYER_CHANGE"
DisableLegacyLayerCommand = "OCTOAPP_DISABLE_LAYER_MAGIC"

@staticmethod
def CreateLayerChangeCommand(layer):
return LayerUtils.LayerChangeCommand + " LAYER=" + str(layer)

@staticmethod
def IsLayerChange(line, context):
if line.startswith("; generated by PrusaSlicer") or line.startswith("; generated by OrcaSlicer") or line.startswith("; generated by SuperSlicer"):
context["slicer"] = "prusa"

if line.startswith(";Generated with Cura"):
context["slicer"] = "cura"

if line.startswith("; generated by Slic3r"):
context["slicer"] = "slic3r" # Doesn't mark layer changes

if line.startswith("; Generated by Kiri:Moto"):
context["slicer"] = "kirimoto"

if line.startswith("; G-Code generated by Simplify3D"):
context["slicer"] = "simplify"

slicer = context.get("slicer", None)

if slicer == "prusa":
return line.startswith(";LAYER_CHANGE")

if slicer == "cura":
return line.startswith(";LAYER:")

if slicer == "kirimoto":
return line.startswith(";; --- layer")

if slicer == "simplify":
return line.startswith("; layer ")

return line.startswith("; OCTOAPP_LAYER_CHANGE")
Loading

0 comments on commit a4c3f76

Please sign in to comment.