Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
57 changes: 57 additions & 0 deletions .github/workflows/on-printer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: On-printer HID tool
on:
push:
pull_request:
workflow_call:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Check embedded canonical payload and shell contracts
run: |
python on-printer/generate_payload.py --check
python -m unittest discover -s tests -p test_on_printer.py -v
sh -n on-printer/payload/installer.sh
- name: Install native build tools
run: |
rustup toolchain install 1.85.1 --profile minimal --component clippy --target armv7-unknown-linux-musleabihf
sudo apt-get update
sudo apt-get install -y gcc-arm-linux-gnueabihf qemu-user
- name: Test native protocol and compile static ARMv7 binary
env:
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-gnueabihf-gcc
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_RUNNER: qemu-arm
run: |
cargo +1.85.1 test --locked --manifest-path on-printer/Cargo.toml
cargo +1.85.1 clippy --locked --manifest-path on-printer/Cargo.toml --all-targets -- -D warnings
cargo +1.85.1 test --locked --manifest-path on-printer/Cargo.toml --target armv7-unknown-linux-musleabihf
cargo +1.85.1 build --locked --manifest-path on-printer/Cargo.toml --release --target armv7-unknown-linux-musleabihf
- name: Check static linking and smoke-test executable
run: |
mkdir -p dist
cp on-printer/target/armv7-unknown-linux-musleabihf/release/cc2camera-hid dist/cc2camera-hid-armv7-linux
file dist/cc2camera-hid-armv7-linux
if readelf -l dist/cc2camera-hid-armv7-linux | grep -q INTERP; then exit 1; fi
if readelf -d dist/cc2camera-hid-armv7-linux | grep -q NEEDED; then exit 1; fi
qemu-arm dist/cc2camera-hid-armv7-linux --help
if qemu-arm dist/cc2camera-hid-armv7-linux install; then exit 1; fi
cd dist
sha256sum cc2camera-hid-armv7-linux > cc2camera-hid-armv7-linux.sha256
wc -c cc2camera-hid-armv7-linux
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cc2camera-hid-armv7-linux
path: |
dist/cc2camera-hid-armv7-linux
dist/cc2camera-hid-armv7-linux.sha256
if-no-files-found: error
18 changes: 15 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build release executable
name: Build release executables

on:
release:
Expand Down Expand Up @@ -41,8 +41,13 @@ jobs:
dist/cc2camera.exe.sha256
if-no-files-found: error

on-printer:
uses: $/.github/workflows/on-printer.yml
permissions:
contents: read

attach:
needs: build
needs: [build, on-printer]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
Expand All @@ -52,9 +57,16 @@ jobs:
with:
name: release-executable
path: dist
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cc2camera-hid-armv7-linux
path: dist
- name: Attach to the existing manually published release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh release upload "$RELEASE_TAG" dist/cc2camera.exe dist/cc2camera.exe.sha256
run: |
gh release upload "$RELEASE_TAG" \
dist/cc2camera.exe dist/cc2camera.exe.sha256 \
dist/cc2camera-hid-armv7-linux dist/cc2camera-hid-armv7-linux.sha256
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ Thumbs.db
*.swp
*~
*.spec

# Native on-printer builds
on-printer/target/
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ dump to your device.
> It has produced the expected result in two tested cases, but if you have any
> doubts, independently audit the tools before using them on your hardware.

## Already have a root shell on the printer?

The [experimental on-printer installer](on-printer/README.md) installs the
startup fix while a working, affected camera stays connected inside the CC2.
See the guide for instructions and the risk of installation without a backup or
free-space check. It has been tested on a previously modified camera; volunteers
with an unmodified, supported camera are invited to
[open an issue](https://github.com/OpenCentauri/cc2_camera_fix/issues/new) to share
results and get help.

## First identify your camera

The known failure affects one 8 MiB flash-layout variant of the
Expand Down
7 changes: 7 additions & 0 deletions on-printer/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions on-printer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "cc2camera-hid"
version = "0.1.0"
edition = "2021"
publish = false

[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true
134 changes: 134 additions & 0 deletions on-printer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Install the camera fix from a CC2 shell

This tool installs the startup fix on a working, affected camera without
disconnecting it from the printer. You need root shell access to an idle CC2
and a USB drive to transfer the executable. Obtaining shell access is outside
this guide.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Put the power-cycle warning before this identification link. In the linked section, the fallback for an unavailable or unknown stream tells readers to switch the printer off and remove the camera for visual inspection. To use this installer afterward, they must power the camera on again. That is the extra boot which this guide warns can exhaust a nearly full configuration partition and brick the camera. Explain that risk before sending a user with an already-running camera to the physical-inspection fallback, and make the safe stop/decision point explicit.

First [identify your camera](../README.md#first-identify-your-camera). This
route supports the affected 8 MiB 30B family, not the early 16 MiB 30B or the
30D. A camera that no longer boots needs [hardware recovery](../README.md#failed-camera-hardware-recovery).

**Experimental:** installation and verification have succeeded on a previously
modified 30B. First installation on an unmodified, supported camera still needs
volunteers. If you try it, please [open an issue](https://github.com/OpenCentauri/cc2_camera_fix/issues/new)
to share results and get help with problems or tool adjustments.

## Before installing

The installer writes two startup scripts to the camera. It does **not** make a
backup or check how much safely writable configuration space remains. If that
space is nearly exhausted, installing the scripts can leave the camera unable
to boot, requiring an external SPI programmer for recovery. Power loss during
installation can also cause failure.

Leaving an affected camera unchanged also consumes configuration space on each
boot. A camera close enough to exhaustion for these writes to cause failure
may have only a few normal boots left. Moving it to a computer for the
[USB procedure](../README.md#working-camera-usb-prevention) adds a boot that can
itself trigger failure. That procedure offers a backup and space check once
connected; this route avoids the extra boot and cable work, but is less tested.
The number of remaining boots cannot be determined by this tool.

**Keep the camera powered for its first installation. Do not reboot as a
preparation step.** If you have already attempted `install` or `verify` during
this camera boot, see [If a command fails](#if-a-command-fails) before doing more.

## Download and inspect

Download `cc2camera-hid-armv7-linux` and `cc2camera-hid-armv7-linux.sha256` from
[Releases](https://github.com/OpenCentauri/cc2_camera_fix/releases) and put both
files at the root of a USB drive. Insert it into the printer.

In the printer's root shell, with the drive mounted at `/mnt/exUDISK`:

```sh
cd /mnt/exUDISK
sha256sum -c cc2camera-hid-armv7-linux.sha256
```

Continue only if the checksum reports `OK`. If `sha256sum` is unavailable,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sha256sum is available.

verify the checksum on your computer first. Adjust the drive path if needed.

```sh
cp cc2camera-hid-armv7-linux /tmp/cc2camera-hid
chmod 755 /tmp/cc2camera-hid
/tmp/cc2camera-hid inspect
```

`inspect` reads the camera identification and queries its version. It does not
upload anything or change camera files. **You can proceed directly to install
without rebooting.** A message saying the version is unavailable is expected
on cameras without a temporary version file; installation performs the firmware

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...some cameas don't have a version file, in that case it might say "vesion unavailable". This does not affect if the fix is possible.

checks. Stop if inspection reports an error or an unsupported camera.

## Install

If you accept the installation risk above, run:

```sh
/tmp/cc2camera-hid install --accept-no-backup-space-risk --verbose > /tmp/cc2camera-install.log 2>&1
cat /tmp/cc2camera-install.log
```

Keep power on until the command finishes. Success begins with:

```text
Camera reports exact installed hook contents and permissions verified.
```

This means the startup scripts were written and checked. They take effect on
the next camera boot. Any error instead requires the steps below, not a retry.

If you know your camera already has custom startup scripts, adding
`--overwrite-managed-scripts` permits replacing `/etc/conf.d/system.sh` and
`/etc/conf.d/enabled/10-erase-fix.sh`. Custom behavior in those two files will be
replaced; other enabled scripts remain. Without the flag, different contents
are refused. A stock camera does not need this flag. If you encounter that
refusal unexpectedly, keep the log and ask for help before another attempt.

## Verify after installation

After the success message, wait at least two minutes, then turn the idle printer
fully off and back on. A shell `reboot` may leave the camera powered and will
not reliably activate the fix. Copy the executable back from the USB drive:

```sh
cp /mnt/exUDISK/cc2camera-hid-armv7-linux /tmp/cc2camera-hid
chmod 755 /tmp/cc2camera-hid
/tmp/cc2camera-hid verify --verbose > /tmp/cc2camera-verify.log 2>&1
cat /tmp/cc2camera-verify.log
```

Verification runs a temporary probe on the camera, without changing its
persistent files or applying the fix itself. Success is:

```text
Camera reports canonical hooks and the live erase correction verified for this boot.
```

Check that the camera feed works too, and include that result in your issue.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the reader, "in your issue" comes without any context here.
Add somehting along the lines of

...please consider opening a github issue and letting us know it worked.


## If a command fails

Keep the camera powered and save the log to the USB drive before it is lost
on a printer restart, for example:

```sh
cp /tmp/cc2camera-install.log /mnt/exUDISK/
sync
```

Use `cc2camera-verify.log` instead for a verification failure. Open an issue with
the log and command you ran. `--verbose` captures the HID communication; it can
also be added to `inspect` when diagnosing a connection problem.

Do not repeat an upload or restart simply because a command failed. A preflight
refusal means installation writes did not start; a partial-installation error
means they did. A timeout or USB error leaves the outcome unknown, and a camera
worker may still be running. The uploader also cannot accept another attempt
after launching a worker during the same camera boot. Resolve the failure with
help before deciding whether a power cycle is appropriate.

For protocol details, build instructions and validation evidence, see the
[technical report](report.md).
44 changes: 44 additions & 0 deletions on-printer/failure-reasons.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
01 chmod
02 config-changing
03 config-mount
04 config-path
05 config-read
06 copy
07 destination-appeared
08 enabled-path
09 final-missing
10 fix-inactive
11 hid-fingerprint
12 identity-file
13 incomplete-install
14 instructions
15 kernel-fingerprint
16 managed-link
17 missing-hooks
18 mkdir
19 mode
20 mount-topology
21 partition-map
22 pointer
23 pointer-changed
24 pointer-format
25 pointer-range
26 rename
27 root
28 staged-fix
29 staged-runner
30 symbols
31 sync
32 temporary-readback
33 unknown-managed-file
34 unrelated-hook-type
35 starter-type
36 starter-stat
37 starter-mode
38 starter-content
39 starter-compare
40 erase-hook-type
41 erase-hook-stat
42 erase-hook-mode
43 erase-hook-content
44 erase-hook-compare
61 changes: 61 additions & 0 deletions on-printer/generate_payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Build the native tool's embedded installer from canonical hook bytes.

Python is required only on the development/build machine, never on the printer.
"""
import hashlib
import re
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from cc2camera.startup_payloads import RUNNER, erase_hook, KERNEL_MD5
from cc2camera.adb_backup import EXPECTED_PARTITIONS
from cc2camera.restore_prepare import INSTRUCTIONS, SYMBOLS

ROOT = Path(__file__).resolve().parent

def generate():
fix = erase_hook()
files = {'fix': fix, 'runner': RUNNER}
payloads = '\n'.join(
' cat > "$STAGE/' + name + '" <<\'CC2_PAYLOAD_END\' || fail payload\n'
+ data.decode('ascii') + 'CC2_PAYLOAD_END'
for name, data in files.items()
)
checks = '\n'.join(
f'[ "$(busybox devmem {a:#x} 32)" = "0x{v:08X}" ] || fail instructions'
for a, v in INSTRUCTIONS.items()
) + '\n' + '\n'.join(
f'[ "$(busybox awk \'$3=="{n}" {{print $1}}\' /proc/kallsyms)" = "{a:08x}" ] || fail symbols'
for n, a in SYMBOLS.items()
)
reasons = [line.split() for line in (ROOT/'failure-reasons.txt').read_text().splitlines()]
replacements = {
'@FAIL_CASES@': '\n'.join(f' {name}) code={code};;' for code,name in reasons),
'@KERNEL_MD5@': KERNEL_MD5,
'@HID_MD5@': '8091751fdd4d0d50ea31901663797a86',
'@MTD@': '\n'.join(f'mtd{i}: {size:08x} 00004000 "{name}"' for i, (name,size) in enumerate(EXPECTED_PARTITIONS)),
'@PAYLOADS@': payloads,
'@FIX_MD5@': hashlib.md5(fix).hexdigest(),
'@RUNNER_MD5@': hashlib.md5(RUNNER).hexdigest(),
'@LIVE_CHECKS@': checks,
}
script = (ROOT/'installer.sh.in').read_text()
for key,value in replacements.items(): script=script.replace(key,value)
# Check only this worker and generated live checks, not the embedded hooks.
worker = (ROOT/'installer.sh.in').read_text() + checks
used = set(re.findall(r'\bfail ([a-z][a-z-]*)', worker))
if used != {name for _,name in reasons}:
raise ValueError('Worker failure-reasons.txt does not match fail sites')
if len({code for code,_ in reasons}) != len(reasons) or any(not re.fullmatch(r'[0-9]{2}', code) or code == '00' for code,_ in reasons):
raise ValueError('Failure codes must be unique two-digit values other than 00')
return script

if __name__ == '__main__':
path = ROOT/'payload/installer.sh'
data = generate()
if sys.argv[1:] == ['--check']:
if path.read_text() != data: raise SystemExit('Embedded installer is stale; run on-printer/generate_payload.py')
elif not sys.argv[1:]:
path.write_text(data)
else: raise SystemExit('Usage: generate_payload.py [--check]')
Loading
Loading