Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dfe8c42
fix: update patch script to remove printer.cfg if it is there before …
pdscomp Jul 5, 2026
e1a43d1
feat: record firmware build details
pdscomp Jul 31, 2026
f26a4ef
feat: support 1.4.49 misc app configuration
pdscomp Jul 31, 2026
ff5a2bb
ci: publish firmware build summaries
pdscomp Jul 31, 2026
45561f8
feat: enable OTA and UART patches on 1.4.49
pdscomp Jul 31, 2026
d1d652a
feat: enable firmware version patch on 1.4.49
pdscomp Jul 31, 2026
7831365
feat: enable front-right homing on 1.4.49
pdscomp Aug 1, 2026
e5140ff
ci: build firmware v1.4.49 by default
pdscomp Aug 1, 2026
e2e9bf6
ci: link PR builds and flash command
pdscomp Aug 1, 2026
bca6444
ci: clean up firmware build summaries
pdscomp Aug 1, 2026
a05c04a
ci: update the existing PR build comment
pdscomp Aug 1, 2026
2369d8d
ci: preserve PR commit metadata in builds
pdscomp Aug 1, 2026
68ba7bb
ci: update patch summary heading
pdscomp Aug 1, 2026
7d896ec
ci: format build time with colons
pdscomp Aug 1, 2026
3c6578a
feat: port chamber light gcode patch to 1.4.49
pdscomp Aug 1, 2026
9e116cf
feat: report vX.Y.Z-beta-<sha>-oc versions on integrate branches
pdscomp Aug 1, 2026
6fdb4c9
fix: read OC_BUILD_BRANCH for integrate version (survives sudo env ch…
pdscomp Aug 1, 2026
cc6aed1
feat: port spoof-slicer-firmware-version patch to 1.4.49
pdscomp Aug 2, 2026
8d1281c
feat: port allow-uploads-during-printing patch to 1.4.49
pdscomp Aug 3, 2026
e0b6138
feat: port allow-api-during-printing patch to 1.4.49
pdscomp Aug 3, 2026
387811a
feat: port do-not-block-z-offset-adjust patch to 1.4.49
pdscomp Aug 3, 2026
4c631bc
feat: port disable-exhaust-fan patch to 1.4.49
pdscomp Aug 3, 2026
2a2fb08
feat: port wait-for-chamber-temp patch to 1.4.49
pdscomp Aug 3, 2026
38e5e59
feat: port report-filament-usage patch to 1.4.49
pdscomp Aug 3, 2026
a2d0b96
docs: map 1.4.49 patch sites and cave allocations for six ported patches
pdscomp Aug 3, 2026
dfeb60c
fix: PR build-summary uses nightly.link for download + flash (no logi…
pdscomp Aug 3, 2026
e2dffa7
fix: package USB NIC modules for firmware 1.4.49
pdscomp Aug 14, 2026
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
74 changes: 70 additions & 4 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ on:
- main

env:
FW_VER: 1.4.46
FW_VER: 1.4.49
OC_BUILD_BRANCH: ${{ github.head_ref || github.ref_name }}
OC_BUILD_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
OC_BUILD_DIRTY: false

permissions:
contents: write
issues: write
pull-requests: write
id-token: write
attestations: write

Expand All @@ -38,7 +43,10 @@ jobs:
rm ./oc-patches/patch_config
cp ./firmware-editions/patched ./oc-patches/patch_config
./build.sh $FW_VER
cp ./update/update.swu update.swu
final_swu_path="$(python3 -c 'import json; print(json.load(open("update/manifest.json"))["final_firmware"]["path"])')"
cp "$final_swu_path" "$(basename "$final_swu_path")"
cp ./update/manifest.json manifest.json
cp ./update/patches.md patches.md

# TODO: Re-enable klipper-pi build once KIP_BOOTSTRAP_URL is configured
# - name: build klipper-pi firmware
Expand All @@ -48,11 +56,69 @@ jobs:
# ./build.sh $FW_VER
# cp ./update/update.swu klipper-pi.swu

- uses: actions/upload-artifact@v4
- id: upload_firmware
uses: actions/upload-artifact@v4
with:
name: CC1 Firmware
path: |
update.swu
update-*.swu
manifest.json
patches.md
# klipper-pi.swu

- name: Post patch summary to PR
if: github.event_name == 'pull_request'
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const marker = '<!-- opencentauri-patch-summary -->';
const summary = fs.readFileSync('patches.md', 'utf8');
const { owner, repo } = context.repo;
const runId = context.runId;
const artifactId = process.env.ARTIFACT_ID;
const artifactUrl = artifactId
? `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifactId}`
: `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts`;
const nightlyUrl = `https://nightly.link/${owner}/${repo}/actions/runs/${runId}/CC1%20Firmware.zip`;
const body = `${marker}
## PR build artifact

- [Download the CC1 Firmware](${nightlyUrl})
- Flash this exact build from an OpenCentauri patched printer running v0.4.0 or later:
\`\`\`sh
flash-artifact ${nightlyUrl}
\`\`\`
- Artifact details (GitHub login required): <${artifactUrl}>

## Firmware patches included

${summary}

Please provide feedback as comments in this PR or in the \`#patched-fw\` channel on Discord.`;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const existing = comments.find(comment => comment.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
env:
ARTIFACT_ID: ${{ steps.upload_firmware.outputs.artifact-id }}
16 changes: 13 additions & 3 deletions .github/workflows/promote-beta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ jobs:
run: |
VERSION="v0.0.0"
TITLE="${VERSION} - beta pre-release, do not install unless instructed"
NOTES="Latest beta build from run ${{ github.event.inputs.run_id }} — commit ${{ github.sha }}"
NOTES=$(cat <<EOF
Latest beta build from run ${{ github.event.inputs.run_id }} — commit ${{ github.sha }}

$(cat patches.md)
EOF
)

if gh release view "${VERSION}" >/dev/null 2>&1; then
echo "Release ${VERSION} exists; updating target, title, and notes."
Expand All @@ -49,5 +54,10 @@ jobs:
--prerelease
fi

# Upload (or overwrite) update.swu to the v0.0.0 release
gh release upload "${VERSION}" update.swu --clobber
# Upload (or overwrite) the firmware, manifest, and patch summary.
swu_path="$(find . -maxdepth 1 -type f -name '*.swu' -print -quit)"
if [ -z "$swu_path" ]; then
echo "No firmware .swu artifact was downloaded"
exit 1
fi
gh release upload "${VERSION}" "$swu_path" manifest.json patches.md --clobber
13 changes: 12 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ jobs:
with:
path: artifacts

- name: Find patch summary
id: patch_summary
run: |
summary_path="$(find artifacts -type f -name patches.md -print -quit)"
if [ -z "$summary_path" ]; then
echo "patches.md was not found in the build artifacts"
exit 1
fi
echo "path=$summary_path" >> "$GITHUB_OUTPUT"

- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
Expand All @@ -50,6 +60,7 @@ jobs:
draft: ${{ github.event.inputs.draft || false }}
prerelease: false
generate_release_notes: true
body_path: ${{ steps.patch_summary.outputs.path }}
files: artifacts/**/*

- name: Attest Release
Expand All @@ -62,4 +73,4 @@ jobs:
"version": "${{ github.event.inputs.version || github.ref_name }}",
"commit": "${{ github.sha }}",
"branch": "${{ github.event.inputs.branch || github.ref_name }}"
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ OC_APP_BOOT_DELAY=0
1. Stages, flashes or stages and flashes the file generated in update/update.sw by `pack.sh`.
1. Run with no arguments for usage.
1. Alternative (if just jailbroken but not running OpenCentauri yet):
1. Copy update/update.swu to USB stick, in the folder update, file update.swu
1. Copy the generated `.swu` from `update/` to a USB stick in a folder named `update`, renaming it to `update.swu`.
1. Reboot the printer and it should detect an update is available!

## Special Commands on Printer
Expand Down
27 changes: 17 additions & 10 deletions TOOLS/auto_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import time
import getpass
import json

# Create hostname, username, ip struct to save

Expand All @@ -33,9 +34,17 @@ def handle_progress(filename: bytes, size: int, sent: int):
print(f'{filename.decode("utf-8")}: {sent/size*100:.2f}%')

if __name__ == "__main__":
# Check if update/update.swu exists
if not os.path.exists('update/update.swu'):
print('update/update.swu not found')
firmware_path = 'update/update.swu'
if os.path.exists('update/manifest.json'):
try:
with open('update/manifest.json', encoding='utf-8') as file:
firmware_path = json.load(file)['final_firmware']['path']
except (KeyError, json.JSONDecodeError):
print('update/manifest.json does not contain a final firmware path')
sys.exit(1)

if not os.path.exists(firmware_path):
print(f'{firmware_path} not found')
sys.exit(1)

# Connect to the printer
Expand All @@ -60,18 +69,18 @@ def handle_progress(filename: bytes, size: int, sent: int):

print('Connected to the printer')

print('Copying update/update.swu to /mnt/UDISK/update.swu')
print(f'Copying {firmware_path} to /mnt/UDISK/update.swu')

scp = SCPClient(ssh.get_transport(), progress=handle_progress)
scp.put('update/update.swu', remote_path='/mnt/UDISK/update.swu', recursive=True)
scp.put(firmware_path, remote_path='/mnt/UDISK/update.swu', recursive=True)
scp.close()

# md5sum update/update.swu with /mnt/UDISK/update.swu. If they don't match, try again 3 times
# Compare the local firmware with /mnt/UDISK/update.swu. If they don't match, try again 3 times.
for i in range(3):
# Get md5sum of both files
stdin, stdout, stderr = ssh.exec_command('md5sum /mnt/UDISK/update.swu')
md5sum_remote = stdout.read().decode('utf-8').split(' ')[0]
md5sum_local = os.popen('md5sum update/update.swu').read().split(' ')[0]
md5sum_local = os.popen(f'md5sum "{firmware_path}"').read().split(' ')[0]

print(f'MD5 sum of local: {md5sum_local}')
print(f'MD5 sum of remote: {md5sum_remote}')
Expand Down Expand Up @@ -105,7 +114,5 @@ def handle_progress(filename: bytes, size: int, sent: int):
# Delete the file and try again
print(f'MD5 sums do not match... Trying again. Current attempt: {i+1}')
ssh.exec_command('rm /mnt/UDISK/update.swu')
scp.put('update/update.swu', remote_path='/mnt/UDISK/update.swu', recursive=True)
scp.put(firmware_path, remote_path='/mnt/UDISK/update.swu', recursive=True)
scp.close()


129 changes: 129 additions & 0 deletions TOOLS/create_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Create a build-details manifest for a firmware update."""

import argparse
from datetime import datetime, timezone
import hashlib
import json
import os
import re
import subprocess
import sys

sys.path.insert(0, os.path.dirname(__file__))
from firmware_version import firmware_version


def sha256(path: str) -> str:
digest = hashlib.sha256()
with open(path, "rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()


def git_output(repo_root: str, *args: str) -> str:
return subprocess.run(
["git", "-C", repo_root, *args],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
).stdout.strip()


def git_metadata(repo_root: str) -> dict:
try:
branch = os.getenv("OC_BUILD_BRANCH")
if not branch:
try:
branch = git_output(repo_root, "symbolic-ref", "--quiet", "--short", "HEAD")
except subprocess.CalledProcessError:
branch = "detached"

commit = os.getenv("OC_BUILD_COMMIT") or git_output(repo_root, "rev-parse", "HEAD")
short_commit = git_output(repo_root, "rev-parse", "--short=7", commit)

dirty_override = os.getenv("OC_BUILD_DIRTY")
if dirty_override:
dirty = dirty_override.lower() in {"1", "true", "yes", "y"}
else:
# Restrict the diff pathspec to files present in HEAD. This detects
# staged and unstaged changes to tracked files while ignoring all
# untracked files, including staged additions.
tracked_files = git_output(repo_root, "ls-tree", "-r", "--name-only", "HEAD").splitlines()
dirty = subprocess.run(
["git", "-C", repo_root, "diff", "--quiet", "HEAD", "--", *tracked_files],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
).returncode != 0
except subprocess.CalledProcessError as error:
detail = error.stderr.strip() or "git metadata is unavailable"
raise SystemExit(f"Error reading git metadata: {detail}") from error

# Keep the version suitable for filenames and package metadata while
# preserving the original branch name in the separate branch field.
version_branch = re.sub(r"[^A-Za-z0-9._-]+", "-", branch).strip(".-") or "detached"
version = f"oc-{version_branch}-{short_commit}"
if dirty:
version += "-dirty"

metadata = {
"version": version,
"branch": branch,
"commit": commit,
"dirty": dirty,
}
return metadata


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--original", required=True, help="Original firmware file passed to unpack.sh")
parser.add_argument("--patch-list", required=True, help="JSON list written by patch_planner.py")
parser.add_argument("--final", required=True, help="Final packed firmware file")
parser.add_argument("--output", required=True, help="Manifest JSON output path")
parser.add_argument("--repo-root", required=True, help="Git repository containing the build")
args = parser.parse_args()

for path in (args.original, args.patch_list, args.final):
if not os.path.isfile(path):
raise SystemExit(f"Error: required manifest input not found: {path}")

with open(args.patch_list) as file:
patches = json.load(file)

build = git_metadata(args.repo_root)
if any(patch["id"] == "set_firmware_version" for patch in patches):
try:
build["firmware_version"] = firmware_version(args.repo_root)
except subprocess.CalledProcessError:
build["firmware_version"] = "Unknown-oc"

manifest = {
"manifest_version": 1,
"original_firmware": {
"file_name": os.path.basename(args.original),
"sha256": sha256(args.original),
},
"patches": patches,
"final_firmware": {
"file_name": os.path.basename(args.final),
"path": args.final,
"sha256": sha256(args.final),
},
"build": build,
}
manifest["build"]["completed_at"] = datetime.now(timezone.utc).strftime(
"%Y-%m-%d %H:%M:%S UTC"
)

with open(args.output, "w") as file:
json.dump(manifest, file, indent=2)
file.write("\n")


if __name__ == "__main__":
main()
Loading
Loading