Skip to content
Open
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
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
```codex_answers
<!-- placeholder for codex's responses textual data -->
# Brother HL-L1222-V Android Print Server (Raspberry Pi 4B+ / openSUSE Tumbleweed)

This repository now provides an executable setup script that configures a Raspberry Pi 4B+ (openSUSE Tumbleweed) as a print server for a USB-connected Brother HL-L1222/HL-L1222-V.

## What this enables
- Android phones on the **same Wi-Fi network** can print through IPP (Mopria-compatible workflow).
- USB Brother printer is shared by CUPS over the local network.
- Firewall and discovery services are configured for LAN printing.

## Important Bluetooth note
Android-to-Raspberry-Pi Bluetooth printing into CUPS is generally not reliable/supported in modern Android/CUPS workflows. The script still enables Bluetooth service for optional experimentation, but the production path is **Wi-Fi IPP**.

## Script
- `scripts/setup_hl_l1222_android_print.sh`

## Run
```bash
sudo bash scripts/setup_hl_l1222_android_print.sh
```

## After script completes
1. Keep printer connected via USB to Raspberry Pi.
2. Connect Android phone to same Wi-Fi as Raspberry Pi.
3. Enable Mopria Print Service (or equivalent Android print service).
4. Add printer with:
- `ipp://<raspberrypi-ip>/printers/Brother_HL_L1222_V`

## Notes
- Script uses CUPS `everywhere` model as default for broad compatibility.
- If you have an official Brother HL-L1222-V Linux PPD/driver package, install it and adjust queue model if desired.
118 changes: 118 additions & 0 deletions scripts/setup_hl_l1222_android_print.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
set -euo pipefail

# Setup script: Brother HL-L1222(-V) USB printer on openSUSE Tumbleweed (Raspberry Pi)
# Goal: receive print jobs from Android phones over the same Wi-Fi (IPP/Mopria) via CUPS.
# Note: Bluetooth printing from Android to CUPS is generally unsupported/restricted; script enables
# practical network printing path and optional USB OTG fallback guidance.

PRINTER_NAME="Brother_HL_L1222_V"
PPD_MODEL="everywhere"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The Brother HL-L1222-V is a GDI (host-based) printer and does not support the IPP Everywhere protocol. Using everywhere as the PPD model will likely result in "Filter failed" errors or no output. This printer typically requires the brlaser driver.

Suggested change
PPD_MODEL="everywhere"
PPD_MODEL="drv:///brlaser.drv/brl1222v.ppd"


if [[ ${EUID} -ne 0 ]]; then
echo "Please run as root: sudo bash $0"
exit 1
fi

REQ_CMDS=(zypper systemctl lpinfo lpadmin lpstat)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The preflight command check requires CUPS tools before they are installed, which can make the setup fail immediately on clean hosts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 17:

<comment>The preflight command check requires CUPS tools before they are installed, which can make the setup fail immediately on clean hosts.</comment>

<file context>
@@ -0,0 +1,118 @@
+  exit 1
+fi
+
+REQ_CMDS=(zypper systemctl lpinfo lpadmin lpstat)
+for c in "${REQ_CMDS[@]}"; do
+  command -v "$c" >/dev/null || { echo "Missing command: $c"; exit 1; }
</file context>

for c in "${REQ_CMDS[@]}"; do
command -v "$c" >/dev/null || { echo "Missing command: $c"; exit 1; }
done
Comment on lines +17 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 CUPS commands checked before CUPS package is installed, causing script failure on fresh systems

Lines 17-20 check for lpinfo, lpadmin, and lpstat via command -v, but these are all provided by the cups package which isn't installed until lines 24-26. On a fresh openSUSE system (the target environment), these commands won't exist, and the script will exit with "Missing command: lpinfo" before it ever reaches the zypper install cups step. This completely prevents the script from running on any system that doesn't already have CUPS — which is exactly the system this setup script is meant to configure.

Suggested change
REQ_CMDS=(zypper systemctl lpinfo lpadmin lpstat)
for c in "${REQ_CMDS[@]}"; do
command -v "$c" >/dev/null || { echo "Missing command: $c"; exit 1; }
done
REQ_CMDS=(zypper systemctl)
for c in "${REQ_CMDS[@]}"; do
command -v "$c" >/dev/null || { echo "Missing command: $c"; exit 1; }
done
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +17 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run CUPS command checks after installing dependencies

The preflight loop checks lpinfo, lpadmin, and lpstat before the script installs cups, so a fresh target host without CUPS exits immediately at "Missing command" and never reaches the installation step. This makes the setup non-bootstrapping in exactly the environment this script is meant to provision.

Useful? React with 👍 / 👎.


echo "[1/8] Installing print/network discovery stack..."
zypper --non-interactive refresh
zypper --non-interactive install \
cups cups-filters ghostscript avahi nss-mdns \
firewalld ipp-usb bluez bluez-tools

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ipp-usb package can conflict with the standard CUPS USB backend for printers that do not natively support IPP-over-USB (like the HL-L1222). The ipp-usb daemon may claim the USB interface, preventing CUPS from accessing it. Additionally, cups-drivers-brlaser should be included to provide the necessary driver.

Suggested change
firewalld ipp-usb bluez bluez-tools
firewalld cups-drivers-brlaser bluez bluez-tools


echo "[2/8] Enabling required services..."
systemctl enable --now cups.service
systemctl enable --now avahi-daemon.service
systemctl enable --now firewalld.service
systemctl enable --now bluetooth.service

# Ensure cupsd listens on local network for Android clients using IPP/Mopria.
CUPSD_CONF="/etc/cups/cupsd.conf"
if ! grep -q '^Port 631' "$CUPSD_CONF"; then
sed -i 's/^Listen localhost:631/# &/' "$CUPSD_CONF" || true
echo 'Port 631' >> "$CUPSD_CONF"
fi

if ! grep -q '^Browsing Yes' "$CUPSD_CONF"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The script appends Browsing Yes and DefaultShared Yes but does not comment out the conflicting Browsing No / DefaultShared No directives that typically exist in the default cupsd.conf. While Listen localhost:631 is correctly commented out via sed before appending Port 631, the same treatment is missing here. CUPS may use the earlier conflicting value, leaving sharing and browsing disabled.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 41:

<comment>The script appends `Browsing Yes` and `DefaultShared Yes` but does not comment out the conflicting `Browsing No` / `DefaultShared No` directives that typically exist in the default `cupsd.conf`. While `Listen localhost:631` is correctly commented out via `sed` before appending `Port 631`, the same treatment is missing here. CUPS may use the earlier conflicting value, leaving sharing and browsing disabled.</comment>

<file context>
@@ -0,0 +1,118 @@
+  echo 'Port 631' >> "$CUPSD_CONF"
+fi
+
+if ! grep -q '^Browsing Yes' "$CUPSD_CONF"; then
+  echo 'Browsing Yes' >> "$CUPSD_CONF"
+fi
</file context>

echo 'Browsing Yes' >> "$CUPSD_CONF"
fi

if ! grep -q '^DefaultShared Yes' "$CUPSD_CONF"; then
echo 'DefaultShared Yes' >> "$CUPSD_CONF"
fi

if ! grep -q '^WebInterface Yes' "$CUPSD_CONF"; then
echo 'WebInterface Yes' >> "$CUPSD_CONF"
fi
Comment on lines +36 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 cupsd.conf modifications are append-only and not idempotent across re-runs

The cupsd.conf modifications at lines 36-65 use a pattern of grep -q to check existence and then echo >> "$CUPSD_CONF" to append. If the script is run multiple times (e.g., after a failed USB detection), and between runs the config already has Port 631 from a previous run, the guards prevent duplication — which is good. However, appending directives like Port 631, Browsing Yes, etc. to the END of the file may cause issues if the default config has conflicting directives earlier (e.g., Listen localhost:631 and Browsing No). CUPS uses the last-wins semantic for some directives but not all. The sed -i 's/^Listen localhost:631/# &/' on line 37 partially addresses this for the Listen directive, but similar conflicting directives (Browsing No, DefaultShared No) are not commented out before appending replacements.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


if ! grep -q '<Location />' "$CUPSD_CONF"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0: Default CUPS cupsd.conf already contains a <Location /> block (restricting access to localhost). Since grep -q '<Location />' finds the existing block, the Allow @LOCAL directive is never appended, so Android LAN clients will still be denied access—defeating the purpose of this script.

The fix should inject Allow @LOCAL into the existing <Location /> block if it doesn't already contain it, or use cupsctl --remote-any --share-printers which handles this robustly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 53:

<comment>Default CUPS `cupsd.conf` already contains a `<Location />` block (restricting access to localhost). Since `grep -q '<Location />'` finds the existing block, the `Allow @LOCAL` directive is never appended, so Android LAN clients will still be denied access—defeating the purpose of this script.

The fix should inject `Allow @LOCAL` into the existing `<Location />` block if it doesn't already contain it, or use `cupsctl --remote-any --share-printers` which handles this robustly.</comment>

<file context>
@@ -0,0 +1,118 @@
+  echo 'WebInterface Yes' >> "$CUPSD_CONF"
+fi
+
+if ! grep -q '<Location />' "$CUPSD_CONF"; then
+  cat >> "$CUPSD_CONF" <<'CUPSLOC'
+<Location />
</file context>

cat >> "$CUPSD_CONF" <<'CUPSLOC'
Comment on lines +53 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Modify existing CUPS Location ACL instead of skipping it

The LAN access block is only appended when <Location /> is absent, but standard cupsd.conf files already contain that section. In that common case this script does not relax the existing ACLs for local-network clients, so CUPS may still reject remote Android IPP requests even though Port 631 and sharing are enabled.

Useful? React with 👍 / 👎.

<Location />
Order allow,deny
Allow @LOCAL
</Location>

<Location /admin>
Order allow,deny
Allow @LOCAL
</Location>
CUPSLOC
fi
Comment on lines +36 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Manually editing cupsd.conf with sed and echo is brittle and not idempotent. Specifically, the check if ! grep -q '<Location />' will almost always fail on a standard CUPS installation because that block usually exists by default, meaning the Allow @LOCAL directive will never be added. This will prevent network clients from accessing the printer. Using cupsctl is the recommended, robust way to enable sharing and remote access.

Suggested change
if ! grep -q '^Port 631' "$CUPSD_CONF"; then
sed -i 's/^Listen localhost:631/# &/' "$CUPSD_CONF" || true
echo 'Port 631' >> "$CUPSD_CONF"
fi
if ! grep -q '^Browsing Yes' "$CUPSD_CONF"; then
echo 'Browsing Yes' >> "$CUPSD_CONF"
fi
if ! grep -q '^DefaultShared Yes' "$CUPSD_CONF"; then
echo 'DefaultShared Yes' >> "$CUPSD_CONF"
fi
if ! grep -q '^WebInterface Yes' "$CUPSD_CONF"; then
echo 'WebInterface Yes' >> "$CUPSD_CONF"
fi
if ! grep -q '<Location />' "$CUPSD_CONF"; then
cat >> "$CUPSD_CONF" <<'CUPSLOC'
<Location />
Order allow,deny
Allow @LOCAL
</Location>
<Location /admin>
Order allow,deny
Allow @LOCAL
</Location>
CUPSLOC
fi
cupsctl --remote-any --share-printers --user-interface

Comment on lines +53 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Existing <Location /> block in default CUPS config prevents Allow @LOCAL from being added

Line 53 uses grep -q '<Location />' to decide whether to append the <Location /> block with Allow @LOCAL. However, a default CUPS installation already ships a <Location /> block that restricts access to localhost only (no Allow @LOCAL). The grep will find this existing block and skip adding the Allow @LOCAL directive, meaning Android clients on the LAN will be denied access to the printer — defeating the entire purpose of the script.

Default CUPS cupsd.conf typically contains
<Location />
  Order allow,deny
</Location>

Since this block exists, the grep matches, the script's Allow @LOCAL block is never appended, and network clients are denied.

Prompt for agents
The script checks whether `<Location />` exists in cupsd.conf and only appends the Allow @LOCAL block if it doesn't. But the default CUPS config already has a `<Location />` block (restricting access to localhost), so the check always matches and the Allow @LOCAL directive is never added.

The fix needs to handle both cases:
1. If `<Location />` exists but doesn't contain `Allow @LOCAL`, inject the Allow @LOCAL directive into the existing block.
2. If `<Location />` doesn't exist at all, append the full block.

Similar logic is needed for `<Location /admin>`.

One approach: use sed to find the existing `<Location />` block and add `Allow @LOCAL` inside it if not already present. For example:
  sed -i '/<Location \/>/,/<\/Location>/ { /Order allow,deny/a\  Allow @LOCAL' } cupsd.conf

Alternatively, replace the existing `<Location />` blocks entirely with the desired configuration using a more robust sed or awk transformation. The key file is /etc/cups/cupsd.conf and the functions involved are the cupsd.conf configuration section around line 53 of setup_hl_l1222_android_print.sh.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


echo "[3/8] Opening firewall for IPP + mDNS..."
firewall-cmd --permanent --add-service=ipp || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Do not swallow firewall-cmd failures; this can report success while network printing is still blocked.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 68:

<comment>Do not swallow firewall-cmd failures; this can report success while network printing is still blocked.</comment>

<file context>
@@ -0,0 +1,118 @@
+fi
+
+echo "[3/8] Opening firewall for IPP + mDNS..."
+firewall-cmd --permanent --add-service=ipp || true
+firewall-cmd --permanent --add-service=mdns || true
+firewall-cmd --reload || true
</file context>

firewall-cmd --permanent --add-service=mdns || true
firewall-cmd --reload || true
Comment on lines +67 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Silencing firewall-cmd failures may hide real configuration problems.

Because every firewall-cmd call is suffixed with || true, failures (e.g., firewalld not running, missing zones/services, or permission issues) are silently ignored and the script still appears to succeed, even if printing won’t work. Since you enable firewalld.service earlier, consider treating at least the first firewall-cmd invocation as mandatory and failing with a clear error if it doesn’t succeed, while leaving later calls non-fatal if desired.

Suggested change
echo "[3/8] Opening firewall for IPP + mDNS..."
firewall-cmd --permanent --add-service=ipp || true
firewall-cmd --permanent --add-service=mdns || true
firewall-cmd --reload || true
echo "[3/8] Opening firewall for IPP + mDNS..."
if ! firewall-cmd --permanent --add-service=ipp; then
echo "Error: Failed to configure firewall for IPP via firewall-cmd."
echo "Ensure firewalld is installed, running, and that you have sufficient permissions, then re-run this script."
exit 1
fi
firewall-cmd --permanent --add-service=mdns || true
firewall-cmd --reload || true


echo "[4/8] Detecting USB printer URI..."
USB_URI="$(lpinfo -v | awk '/^direct usb:\/\/Brother\//{print $2; exit}')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: USB URI detection awk pattern may not match all lpinfo output formats

Line 73 uses awk '/^direct usb:\/\/Brother\//{print $2; exit}' to extract the USB URI from lpinfo -v. The regex assumes the output starts with direct usb://Brother/ with exactly one space between direct and usb://. Some CUPS versions pad the scheme class field with multiple spaces or tabs. If the whitespace differs, the regex won't match even though the printer is connected. The $2 field extraction via awk's default whitespace splitting would still work for extracting the URI, but the regex anchor ^direct usb:// is fragile. A safer pattern might be /usb:\/\/Brother\//{print $2; exit} without anchoring to ^direct . This wasn't flagged as a bug because the standard lpinfo -v format on openSUSE with CUPS 2.x typically uses a single space, but it's worth noting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

if [[ -z "$USB_URI" ]]; then
echo "No Brother USB URI auto-detected. Available devices:"
lpinfo -v
echo "Connect the printer via USB and re-run this script."
exit 1
fi
Comment on lines +73 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Script uses set -euo pipefail but pipes lpinfo -v through awk without handling pipefail

Line 73: USB_URI="$(lpinfo -v | awk '...')" — with pipefail enabled, if lpinfo -v fails (e.g., CUPS service not fully started yet after the enable --now on line 29), the pipeline exit status will be non-zero and set -e will terminate the script before reaching the friendlier error message at lines 74-78. The || true pattern used elsewhere (e.g., line 100) is not applied here, which is actually reasonable since a failed lpinfo is a real error, but the error message won't be the script's helpful diagnostic — it'll be an abrupt exit. This is a minor UX concern, not a correctness bug.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


echo "Detected URI: $USB_URI"

echo "[5/8] Creating/updating CUPS queue..."
if lpstat -p "$PRINTER_NAME" >/dev/null 2>&1; then
lpadmin -x "$PRINTER_NAME"
fi
lpadmin -p "$PRINTER_NAME" -E -v "$USB_URI" -m "$PPD_MODEL"
lpoptions -d "$PRINTER_NAME"

# Disable duplex (hardware lacks automatic duplex)
lpadmin -p "$PRINTER_NAME" -o sides=one-sided -o media=A4

# Enable sharing explicitly
lpadmin -p "$PRINTER_NAME" -o printer-is-shared=true

echo "[6/8] Restarting CUPS..."
systemctl restart cups.service

echo "[7/8] Printing a test page..."
lp -d "$PRINTER_NAME" /usr/share/cups/data/testprint || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The path /usr/share/cups/data/testprint may not exist or may require a specific file extension (like .ps or .pdf) depending on the CUPS version and installed filters. If this file is missing, the test print will fail.

Comment on lines +99 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Test page is printed unconditionally on every run

Line 100 prints a test page (lp -d "$PRINTER_NAME" /usr/share/cups/data/testprint || true) every time the script is run. If an operator re-runs the script (e.g., to fix config), a physical page will be printed each time, wasting paper. This is not a bug per se, but could be surprising for re-runs. A --no-test flag or a prompt would be a more user-friendly approach.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


echo "[8/8] Final status"
lpstat -t

IP_ADDR="$(hostname -I | awk '{print $1}')"
cat <<MSG
Comment on lines +105 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Using hostname -I | awk '{print $1}' may select an unexpected or unusable IP address.

On hosts with multiple interfaces (Wi‑Fi, Ethernet, VPNs, docker, etc.), hostname -I often returns several addresses, and the first one may be unreachable from Android (e.g., link-local or internal). It would be more robust to target a specific interface (e.g., via ip -4 addr show <iface> and filtering out link-local addresses) or, at minimum, detect multiple candidates and prompt the user to choose the correct IP.

Suggested change
IP_ADDR="$(hostname -I | awk '{print $1}')"
cat <<MSG
# Detect candidate IPv4 addresses (non-loopback, global scope)
CANDIDATE_IPS=()
if command -v ip >/dev/null 2>&1; then
# Use `ip` if available for more reliable address detection
while IFS= read -r ip; do
CANDIDATE_IPS+=("$ip")
done < <(ip -4 addr show scope global | awk '/inet / {print $2}' | cut -d/ -f1)
fi
# Fallback to hostname -I if `ip` produced nothing
if [ "${#CANDIDATE_IPS[@]}" -eq 0 ]; then
for ip in $(hostname -I 2>/dev/null); do
# skip obvious loopback addresses just in case
case "$ip" in
127.*) continue ;;
esac
CANDIDATE_IPS+=("$ip")
done
fi
# If we still have nothing, leave IP_ADDR empty
IP_ADDR=""
if [ "${#CANDIDATE_IPS[@]}" -eq 1 ]; then
IP_ADDR="${CANDIDATE_IPS[0]}"
elif [ "${#CANDIDATE_IPS[@]}" -gt 1 ]; then
echo
echo "Multiple candidate IP addresses were detected for this Raspberry Pi:"
i=1
for ip in "${CANDIDATE_IPS[@]}"; do
echo " [$i] $ip"
i=$((i+1))
done
echo
# prompt user to choose; default to first if they just press Enter
read -rp "Select the IP address that your Android device can reach [1]: " choice
case "$choice" in
""|*[!0-9]*)
IP_ADDR="${CANDIDATE_IPS[0]}"
;;
*)
idx=$((choice-1))
if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#CANDIDATE_IPS[@]}" ]; then
IP_ADDR="${CANDIDATE_IPS[$idx]}"
else
IP_ADDR="${CANDIDATE_IPS[0]}"
fi
;;
esac
fi
cat <<MSG


Setup complete.

Android printing method (recommended):
1) Connect phone to same Wi-Fi as Raspberry Pi.
2) Ensure Mopria Print Service is enabled on Android.
3) Add/find printer: ipp://$IP_ADDR/printers/$PRINTER_NAME

Bluetooth note:
- Direct Android->CUPS Bluetooth printing is typically not supported on modern Android/CUPS stacks.
- Use Wi-Fi IPP/Mopria path above, or USB-OTG direct phone-to-printer where supported by phone/app.
MSG