Skip to content
Closed
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
44 changes: 42 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
name: Build release tarball
name: Build and test multiACE packages

on:
push:
tags:
- 'v*'
pull_request:

permissions:
contents: write
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate managed package sources
run: |
python -m py_compile \
multiace/klipper/extras/ace.py \
multiace/web/backend/i18n_path.py \
multiace/web/backend/main.py \
multiace/managed/build_package.py \
multiace/managed/tests/test_package.py \
multiace/managed/tests/test_i18n_path.py
node --check multiace/web/frontend/app.js
bash -n \
multiace/install_multiace.sh \
multiace/uninstall_multiace.sh \
multiace/tools/multiace_update.sh \
multiace/config/extended/multiace/ace_mode_switch.sh \
multiace/web/deploy/S98multiace-web
bash multiace/managed/tests/test_managed_guards.sh
python -m unittest discover -s multiace/managed/tests -v

python multiace/managed/build_package.py \
--output "$RUNNER_TEMP/multiace-managed-check.tar.gz"
(cd "$RUNNER_TEMP" && \
sha256sum --check multiace-managed-check.tar.gz.sha256)

release:
if: startsWith(github.ref, 'refs/tags/v')
needs: validate
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -21,13 +56,18 @@ jobs:
TAG="${GITHUB_REF_NAME}"
OUT="dist/release"
mkdir -p "$OUT"
VERSION="$(tr -d '\r\n' < multiace/VERSION)"
test -n "$VERSION"
test "${TAG#v}" = "$VERSION"
# The whole multiace/ tree at its repo layout (same as the
# release recipe): the updater runs install_multiace.sh from
# inside, and the installer needs deploy/, config/, tools/,
# i18n/ and web/ next to it.
tar -czf "$OUT/multiace-${TAG}.tar.gz" multiace/
(cd "$OUT" && sha256sum "multiace-${TAG}.tar.gz" \
> "multiace-${TAG}.tar.gz.sha256")
python multiace/managed/build_package.py \
--output "$OUT/multiace-managed-${VERSION}.tar.gz"
ls -la "$OUT/"

- name: Publish
Expand Down
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,33 @@ ACE units do not read or expose the spools uid so it uses the sku field. (Spoolm
- **Online Updates **
- **Auto-Load** - Load all filaments autmatically, Parallel preload in bg mode
- **RFID Handling** - Automatic RFID detection and display across ACE switches
- **PAXX Firmware Compatible / Installer** - Works with PAXX firmware which provides display mirroring, allowing full load/unload control from your computer / Integrated PAXX Firmware
- **Clean Install/Uninstall** - One-command scripts with automatic backup and restore
- **PAXX Firmware Compatible / Installer** - Works with PAXX firmware which provides display mirroring, allowing full load/unload control from your computer / Integrated PAXX Firmware
- **Clean Install/Uninstall** - One-command scripts with automatic backup and restore

### Platform-managed package

The repository also provides a platform-neutral managed package in
`multiace/managed/`. A host platform can pin the archive and SHA-256 checksum,
install the payload under its own application root, and own activation,
persistent configuration, updates, rollback, and user-facing controls. The
managed package is intended to share the same contract across platform
integrations, including PAXX and the planned Bespok3d packaging.

The managed package does not include the standalone SSH installer, uninstaller,
self-updater, boot service, or file-copy mode switch helper. Those remain in
the repository for stock-firmware users who choose a standalone installation.
Managed hosts pass `MULTIACE_MANAGED=1` and the shared path variables described
in `multiace/managed/README.md`; multiACE then defers update and installation
ownership to the platform.

Build the managed archive and matching checksum from the repository root with:

```bash
python3 multiace/managed/build_package.py
```

The `release.yml` workflow publishes both the standalone source archive and
the managed archive with their checksums for each `v<version>` release tag.


## ACE Pro 2 Support
Expand Down
7 changes: 7 additions & 0 deletions multiace/config/extended/multiace/ace_mode_switch.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
#!/bin/bash
set -e
if [ "${MULTIACE_MANAGED:-0}" = "1" ] || \
[ "${MULTIACE_MANAGED:-}" = "true" ] || \
[ -e "${MULTIACE_MANAGED_MARKER:-${MULTIACE_CONFIG_DIR:-/home/lava/printer_data/config}/extended/multiace/.multiace-managed}" ]; then
echo "multiACE mode switching is managed by the platform; refusing to copy Klipper files" >&2
exit 2
fi

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
HOME_DIR="/home/lava"
EXTRAS_DIR="${HOME_DIR}/klipper/klippy/extras"
Expand Down
1 change: 1 addition & 0 deletions multiace/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
"update_failed": "Update fehlgeschlagen",
"update_intro": "Holt die neueste multiACE-Release und installiert sie.",
"update_latest": "Verfügbar",
"update_managed": "Updates werden von der Plattform verwaltet. Verwende die multiACE-Integration deines Druckers, um eine geprüfte Version zu installieren.",
"update_not_checked": "noch nicht geprüft",
"update_title": "multiACE Updates"
},
Expand Down
1 change: 1 addition & 0 deletions multiace/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
"update_failed": "Update failed",
"update_intro": "Pulls the latest multiACE release and installs it.",
"update_latest": "Available",
"update_managed": "Updates are managed by the platform. Use your printer's multiACE integration to install a tested release.",
"update_not_checked": "not checked yet",
"update_title": "multiACE updates"
},
Expand Down
1 change: 1 addition & 0 deletions multiace/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"update_intro": "拉取最新的 multiACE 版本并安装。",
"update_current": "已安装",
"update_latest": "可用版本",
"update_managed": "更新由平台管理。请使用打印机的 multiACE 集成安装经过验证的版本。",
"update_not_checked": "尚未检查",
"update_check_btn": "检查更新",
"update_checking": "检查中...",
Expand Down
8 changes: 8 additions & 0 deletions multiace/install_multiace.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
#!/bin/bash
set -e

if [ "${MULTIACE_MANAGED:-0}" = "1" ] || \
[ "${MULTIACE_MANAGED:-}" = "true" ] || \
[ -e "${MULTIACE_MANAGED_MARKER:-${MULTIACE_CONFIG_DIR:-/home/lava/printer_data/config}/extended/multiace/.multiace-managed}" ]; then
echo "multiACE is managed by the platform; use its integration instead of install_multiace.sh" >&2
exit 2
fi

INSTALL_WEB=0
KEEP_CONFIG=0
for arg in "$@"; do
Expand Down
49 changes: 43 additions & 6 deletions multiace/klipper/extras/ace.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@
MULTIACE_BUILD_TAG = "eed86c9b"
MULTIACE_BUNDLE_SHA1 = "40a00eb"


def _env_flag(name):
return os.environ.get(name, '').strip().lower() in ('1', 'true', 'yes', 'on')


_MULTIACE_CONFIG_DIR = os.environ.get(
'MULTIACE_CONFIG_DIR', '/home/lava/printer_data/config')
MULTIACE_MANAGED_MARKER = os.environ.get(
'MULTIACE_MANAGED_MARKER', '').strip() or os.path.join(
_MULTIACE_CONFIG_DIR, 'extended', 'multiace', '.multiace-managed')
MULTIACE_MANAGED = (
_env_flag('MULTIACE_MANAGED')
or os.path.exists(MULTIACE_MANAGED_MARKER))

def _load_i18n_catalog(i18n_dir, lang):
"""Read <i18n_dir>/<lang>.json overlaid on en.json. Returns a dict
(possibly empty if the i18n dir is missing) - caller falls back to
Expand Down Expand Up @@ -697,8 +711,9 @@ def __init__(self, config):

self.paths = _resolve_multiace_paths(config)
self.host = self.paths['host_type']
# Derived, never a config option: the web backend edits the same
# file (MULTIACE_CFG_PATH), so the two must not be able to diverge.
self._managed_by_host = MULTIACE_MANAGED
# Derived from the shared config-directory contract so Klipper and
# the web UI always write through to the same ace.cfg.
self.ACE_CFG_PATH = self.paths['ace_cfg']

self.gate_status = [GATE_UNKNOWN, GATE_UNKNOWN, GATE_UNKNOWN, GATE_UNKNOWN]
Expand Down Expand Up @@ -1433,7 +1448,11 @@ def _parse_idx_list(key):
'inbox_max_mb', 256, minval=1, maxval=4096)

self._i18n_primary = config.get('i18n_dir', self.paths['i18n_primary'])
self._i18n_fallback = os.path.join(self._web_dir, 'i18n')
self._i18n_fallbacks = []
app_dir = os.environ.get('MULTIACE_APP_DIR', '').strip()
if app_dir:
self._i18n_fallbacks.append(os.path.join(app_dir, 'i18n'))
self._i18n_fallbacks.append(os.path.join(self._web_dir, 'i18n'))
self._reload_i18n_catalog()

self._head_source = {0: None, 1: None, 2: None, 3: None}
Expand Down Expand Up @@ -2347,8 +2366,9 @@ def _t(self, key, **params):
def _reload_i18n_catalog(self):
"""(Re)load self._i18n for the current self._language. Used at startup
and live by MULTIACE_SET_LANGUAGE."""
i18n_dir = self._i18n_primary if os.path.isdir(self._i18n_primary) \
else self._i18n_fallback
candidates = [self._i18n_primary] + self._i18n_fallbacks
i18n_dir = next((path for path in candidates if os.path.isdir(path)),
self._i18n_primary)
try:
self._i18n = _load_i18n_catalog(i18n_dir, self._language)
except Exception as e:
Expand Down Expand Up @@ -20732,7 +20752,13 @@ def cmd_ACE_RUN_MODE_SWITCH(self, gcmd):
# multi<->head stay on the SAME ace files -> pure runtime flip, no file
# swap / reboot. Only transitions involving 'normal' (stock files) run
# the file switch script below.
if mode in ('multi', 'head') and current in ('multi', 'head'):
if self._managed_by_host and mode == 'normal':
raise gcmd.error(
'[multiACE] Normal mode is controlled by the host platform. '
'Disable the managed multiACE integration and reboot.')

if mode in ('multi', 'head') and (
current in ('multi', 'head') or self._managed_by_host):
self.gcode.run_script_from_command(
"SAVE_VARIABLE VARIABLE=ace__mode VALUE=\"'%s'\"" % mode)
self._ace_mode = mode
Expand Down Expand Up @@ -20766,6 +20792,14 @@ def cmd_ACE_RUN_MODE_SWITCH(self, gcmd):
pass
return

if self._managed_by_host:
# The host has already selected and activated the ACE modules.
# Managed mode changes runtime state only; it never invokes the
# standalone helper that copies over stock Klipper files.
raise gcmd.error(
'[multiACE] This mode change is not supported by the managed '
'runtime; the host platform controls file activation.')

save_vars = self.printer.lookup_object('save_variables')
vars_path = save_vars.filename
script_dir = os.path.dirname(os.path.abspath(vars_path))
Expand Down Expand Up @@ -20818,6 +20852,9 @@ def cmd_ACE_RUN_MODE_SWITCH(self, gcmd):
_UPDATE_SCRIPT = '/home/lava/multiace_update.sh' if os.path.isfile('/home/lava/multiace_update.sh') else os.path.expanduser('~/multiace_update.sh')

def _run_update_script(self, gcmd, sub_args, timeout):
if self._managed_by_host:
raise gcmd.error(
'[multiACE] Updates are managed by the platform.')
if not os.path.isfile(self._UPDATE_SCRIPT):
raise gcmd.error(
'[multiACE] Updater script not found at %s - re-run '
Expand Down
62 changes: 62 additions & 0 deletions multiace/managed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# multiACE managed package

This package is the platform-neutral payload for host-managed multiACE
installations. It is separate from the standalone SSH installer so users on
stock firmware can continue using the existing standalone installation and
self-update process.

## Ownership boundary

- multiACE owns ACE behavior, Klipper modules, and the web interface.
- The host platform owns the selected multiACE version, package installation,
activation, persistent configuration, and upgrades.
- The package omits the standalone installer, uninstaller, updater, boot
service, and file-copy mode-switch helper. The source repository still
contains those standalone components.

Managed deployments use a shared runtime contract. The host passes these
variables to both Klipper and the web service:

- `MULTIACE_MANAGED=1` enables managed behavior.
- `MULTIACE_MANAGED_MARKER` points to the durable `.multiace-managed` marker
under the shared multiACE configuration/state directory.
- `MULTIACE_CONFIG_DIR` is the directory containing `printer.cfg` (for the
U1, `/home/lava/printer_data/config`).
- `MULTIACE_PRINTER_DATA` is the printer-data root (for the U1,
`/home/lava/printer_data`).
- `MULTIACE_APP_DIR` is the active package root, used to locate packaged
provider data such as translation catalogs.

The two printer paths follow the shared contract in
[issue #142](https://github.com/decay71/multiACE/issues/142). multiACE derives
`extended/ace.cfg` and its persistent state from these roots; a host should not
provide a second config-file path that could diverge.

In managed mode, install, uninstall, and self-update entry points refuse to
run. The web Update area reports that updates are managed by the platform
instead of invoking the updater. `SET_ACE_MODE MODE=normal` and the standalone
file-copy mode switch are refused; switching between multi and head remains a
runtime-only operation.

The managed archive contains the provider's clean `ace.cfg` defaults and
macros, but omits the standalone `[save_variables]` path and self-update
wrapper macros. Each platform seeds its own persistent save-variable path and
preserves existing user values. The standalone source config remains
unchanged.

## Archive

The allowlisted archive is named `multiace-managed-<version>.tar.gz` and is
published with a matching SHA256 file for each release. The host pins both the
release and checksum before installation.

From the repository root, a package and checksum can be built with:

```text
python3 multiace/managed/build_package.py
```

The `release.yml` workflow builds both the standalone source archive and this
managed archive from the same release tag, then publishes both checksums on
that release. Package construction does not replace or alter the standalone
installation path.
Loading
Loading