Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ Automatically (re)connect your printer in OctoPrint — not only over **serial**
also through the OctoPrint 2.0 **connector framework** for **Moonraker (Klipper)**,
**Bambu** and any other registered connector.

> [!NOTE]
> **About this project.** I built this for my own printer setup with AI, and if
> it helps others, even better. I have tested it to the best of my knowledge and
> ability, and every change is backed by an automated test suite, CI, and
> security scans (Bandit, CodeQL). Disclosed here per the OctoPrint plugin guidelines.
> Issues and PRs are welcome.

AutoConnectPlus is a fork of
[OctoPrint-PortRetryPlus](https://github.com/hprombex/OctoPrint-PortRetryPlus) that
keeps its proven retry/timer logic and extends it to the modern connector API.
Expand Down Expand Up @@ -44,6 +51,12 @@ OctoPrint stores a single preferred connection, so there is never any ambiguity.
it is missing or incomplete (or the matching connector plugin is not installed), the
plugin simply keeps waiting and logs the reason once instead of every interval.

**Only one reconnect plugin should be active.** AutoConnectPlus detects the original
`PortRetry` plugin (`portretry`, by vehystrix) and the `PortRetryPlus` fork
(`portretryplus`, by hprombex). If either one is enabled at the same time, AutoConnectPlus
shows a permanent error toast as soon as the OctoPrint interface loads, because both
plugins can compete to reconnect the same printer.

## Requirements

- **Serial** mode works on any reasonably recent OctoPrint (1.x included), where it
Expand Down Expand Up @@ -83,21 +96,14 @@ target, refreshed every time the dialog opens) and these options:

The printer profile used is OctoPrint's default profile.

The same options can be set in `~/.octoprint/config.yaml`:

```yaml
plugins:
autoconnectplus:
enabled: true # master switch
interval: 5.0 # seconds between retries (minimum 0.1)
forced_port: "" # serial only: used when OctoPrint's port is unset/AUTO
```

## Troubleshooting

All plugin activity is logged to `octoprint.log`, prefixed with
`octoprint.plugins.autoconnectplus`.

- **A permanent error toast mentions PortRetry** — disable either AutoConnectPlus,
PortRetry, or PortRetryPlus. Only one automatic reconnect plugin should be active;
installing both is supported, running both is not.
- **Nothing reconnects at all** — check that the plugin is enabled in its settings
and that the *detected connection* shown there is the one you expect. If a warning
is shown instead (no port detected, no preferred connection stored, connector
Expand Down Expand Up @@ -145,15 +151,9 @@ rolling `latest` release behind the stable install URL above.

- Original [OctoPrint-PortRetryPlus](https://github.com/hprombex/OctoPrint-PortRetryPlus)
by **hprombex**.
- Earlier work and inspiration credited to **vehystrix**.
- Earlier work and inspiration from [OctoPrint-PortRetry](https://github.com/vehystrix/OctoPrint-PortRetry) credited to **vehystrix**.

## License

Licensed under the **GNU Affero General Public License v3 or later
(AGPL-3.0-or-later)**, matching the original project. See [LICENSE](LICENSE).

> [!NOTE]
> **About this project.** I built this for my own printer setup with AI, and if
> it helps others, even better. I have tested it to the best of my knowledge and
> ability. Disclosed here per the OctoPrint plugin guidelines.
> Issues and PRs are welcome.
6 changes: 6 additions & 0 deletions extras/autoconnectplus.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ Offline printers are detected with a quick reachability probe and skipped
quietly, and repeated failed attempts back off progressively instead of
flooding the log.

Only one automatic reconnect plugin should be active. AutoConnectPlus checks for
both `portretry` (the original OctoPrint-PortRetry by vehystrix) and `portretryplus`
(the OctoPrint-PortRetryPlus fork by hprombex). If either is enabled, it shows a
permanent error toast immediately when the OctoPrint interface loads, because two
reconnect plugins can compete for the same printer connection.

AutoConnectPlus is a fork of [OctoPrint-PortRetryPlus](https://github.com/hprombex/OctoPrint-PortRetryPlus)
by hprombex (with earlier work credited to vehystrix). The serial retry/timer
logic is carried over; the connector support is new.
Expand Down
44 changes: 39 additions & 5 deletions octoprint_autoconnectplus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,10 +426,32 @@ def _host_reachable(self, connector: str, parameters: dict) -> bool:
# Detected connection (settings display / simple API)
# ------------------------------------------------------------------ #

def _detected_connection(self) -> dict[str, str]:
def _portretry_plugins_enabled(self) -> list[str]:
"""Return the enabled PortRetry plugins that can conflict."""
plugin_names = {
"portretry": "PortRetry",
"portretryplus": "PortRetryPlus",
}
try:
plugin_manager = octoprint.plugin.plugin_manager()
return [
name
for identifier, name in plugin_names.items()
if plugin_manager.get_plugin_info(
identifier, require_enabled=True
)
is not None
]
except Exception: # pylint: disable=broad-exception-caught
# Plugin discovery must never prevent the settings API from
# returning the detected connection.
return []

def _detected_connection(self) -> dict[str, Any]:
"""Describe the connection to reconnect, for the settings display:
label, target (serial port or host:port) and an optional warning."""
connector = self._get_preferred_connector()
portretry_plugins = self._portretry_plugins_enabled()
label = CONNECTOR_LABELS.get(connector, connector)

if self._is_serial_connector(connector):
Expand All @@ -438,7 +460,12 @@ def _detected_connection(self) -> dict[str, str]:
"No serial port detected yet; set one in OctoPrint's "
"connection dialog or configure a forced port below."
)
return {"label": label, "target": target, "warning": warning}
return {
"label": label,
"target": target,
"warning": warning,
"portretry_plugins": portretry_plugins,
}

parameters = self._get_preferred_parameters()
host = parameters.get("host", "")
Expand All @@ -459,9 +486,14 @@ def _detected_connection(self) -> dict[str, str]:
"matching connector plugin."
)

return {"label": label, "target": target, "warning": warning}
return {
"label": label,
"target": target,
"warning": warning,
"portretry_plugins": portretry_plugins,
}

def on_api_get(self, request):
def on_api_get(self, request): # type: ignore[override]
"""Serve the detected connection to the settings dialog, which fetches
it every time it is shown so the display never goes stale."""
return flask.jsonify(self._detected_connection())
Expand Down Expand Up @@ -552,7 +584,9 @@ def on_settings_save(self, data) -> dict[Any, Any]:
# Match the entry-point key so the runtime identifier is explicit (otherwise
# defaults to the package name).
__plugin_identifier__ = "autoconnectplus"
__plugin_author__ = "ajimaru"
__plugin_author__ = (
"ajimaru, based on work from vehystrix and hprombex"
)
__plugin_description__ = (
"Automatically reconnects the printer over serial, Moonraker or Bambu "
"connectors"
Expand Down
34 changes: 33 additions & 1 deletion octoprint_autoconnectplus/static/js/autoconnectplus.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@
* stale after the preferred connection changes.
*/
$(function () {
var portRetryWarningShown = false;

function showPortRetryWarning(plugins) {
if (portRetryWarningShown || typeof PNotify === "undefined") {
return;
}

new PNotify({

Check warning on line 19 in octoprint_autoconnectplus/static/js/autoconnectplus.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

octoprint_autoconnectplus/static/js/autoconnectplus.js#L19

'PNotify' is not defined.

Check warning on line 19 in octoprint_autoconnectplus/static/js/autoconnectplus.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

octoprint_autoconnectplus/static/js/autoconnectplus.js#L19

ES5 trailing commas in array/object literals are forbidden.
title: "AutoConnectPlus error",
text: plugins.join(" and ") + " is also enabled. Disable " +
"one of the reconnect plugins to prevent competing " +
"reconnect attempts.",
type: "error",
hide: false,
});
portRetryWarningShown = true;
}

function checkPortRetryConflict() {
OctoPrint.simpleApiGet("autoconnectplus").done(function (data) {

Check warning on line 31 in octoprint_autoconnectplus/static/js/autoconnectplus.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

octoprint_autoconnectplus/static/js/autoconnectplus.js#L31

'OctoPrint' is not defined.
if (data.portretry_plugins && data.portretry_plugins.length) {
showPortRetryWarning(data.portretry_plugins);
}
});
}

// Check immediately after the global plugin assets are ready, not only
// when the user opens the settings dialog.
checkPortRetryConflict();

function AutoConnectPlusViewModel(parameters) {
var self = this;

Expand All @@ -17,12 +47,14 @@
self.detectedLabel = ko.observable("");
self.detectedTarget = ko.observable("");
self.detectedWarning = ko.observable("");

self.refreshDetected = function () {
OctoPrint.simpleApiGet("autoconnectplus").done(function (data) {
self.detectedLabel(data.label || "");
self.detectedTarget(data.target || "");
self.detectedWarning(data.warning || "");
if (data.portretry_plugins && data.portretry_plugins.length) {
showPortRetryWarning(data.portretry_plugins);
}
});
};

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "OctoPrint-AutoConnectPlus"
version = "0.1.0rc3"
version = "0.1.0"
description = "Automatically reconnects the printer over serial, Moonraker or Bambu connectors"
authors = [
{name = "ajimaru", email = "ajimaru_gdr@pm.me"}
Expand Down
40 changes: 40 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@
"label": "Serial",
"target": "/dev/ttyUSB0",
"warning": "",
"portretry_plugins": [],
}


Expand All @@ -396,6 +397,45 @@
assert detected["label"] == "Serial"
assert detected["target"] == ""
assert "No serial port" in detected["warning"]
assert detected["portretry_plugins"] == []

Check warning on line 400 in tests/test_plugin.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_plugin.py#L400

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check warning on line 400 in tests/test_plugin.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_plugin.py#L400

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. (B101)


@pytest.mark.parametrize(
("identifier", "display_name"),
[("portretry", "PortRetry"), ("portretryplus", "PortRetryPlus")],
)
def test_detected_connection_warns_when_portretry_plugin_is_enabled(
plugin, identifier, display_name
):
plugin_manager = mock.Mock()

def get_plugin_info(checked_identifier, **_):
return mock.Mock() if checked_identifier == identifier else None

plugin_manager.get_plugin_info.side_effect = get_plugin_info
with mock.patch(
"octoprint_autoconnectplus.octoprint.plugin.plugin_manager",
return_value=plugin_manager,
):
detected = plugin._detected_connection()

assert detected["portretry_plugins"] == [display_name]

Check warning on line 422 in tests/test_plugin.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_plugin.py#L422

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check warning on line 422 in tests/test_plugin.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_plugin.py#L422

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. (B101)
assert plugin_manager.get_plugin_info.call_count == 2

Check warning on line 423 in tests/test_plugin.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_plugin.py#L423

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check warning on line 423 in tests/test_plugin.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_plugin.py#L423

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. (B101)


def test_detected_connection_warns_when_both_portretry_plugins_are_enabled(
plugin,
):
plugin_manager = mock.Mock()
plugin_manager.get_plugin_info.return_value = mock.Mock()
with mock.patch(
"octoprint_autoconnectplus.octoprint.plugin.plugin_manager",
return_value=plugin_manager,
):
detected = plugin._detected_connection()

assert detected["portretry_plugins"] == ["PortRetry", "PortRetryPlus"]

Check warning on line 437 in tests/test_plugin.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_plugin.py#L437

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check warning on line 437 in tests/test_plugin.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_plugin.py#L437

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. (B101)
assert plugin_manager.get_plugin_info.call_count == 2


def test_detected_connection_connector_with_default_port(
Expand Down