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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,50 @@ Built-in definitions are loaded from `printers.json` at startup.

When the Bluetooth device picker appears, select the device showing a **signal strength indicator**. Devices listed without signal strength may be cached/ghost entries that won't connect properly.

## USB on Linux

On Linux, the kernel's generic USB printer driver (`usblp`) auto-binds to thermal printers when they're plugged in and claims the printer interface. WebUSB then can't claim the same interface and you'll see:

```
Failed to execute 'claimInterface' on 'USBDevice': Unable to claim interface.
```

You need to release the device from `usblp`. Pick one:

**Quick test (non-persistent):** unload the module without unplugging the printer.

```bash
sudo modprobe -r usblp
```

Click Connect again. `usblp` will rebind on the next replug or reboot.

**Persistent (blacklist):** prevents `usblp` from ever loading. Simple, but **breaks CUPS / `lp` printing for all USB printers** on this machine.

```bash
echo 'blacklist usblp' | sudo tee /etc/modprobe.d/blacklist-usblp.conf
sudo modprobe -r usblp
```

**Persistent (per-device, recommended if you also use CUPS):** detach `usblp` only from the Phomemo. Get the printer's vendor/product ID with `lsusb`, then create `/etc/udev/rules.d/70-phomemo-webusb.rules`:

```
# Replace XXXX:YYYY with your IDs from `lsusb`
SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", ATTR{idProduct}=="YYYY", MODE="0660", TAG+="uaccess"
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", ATTR{idProduct}=="YYYY", \
RUN+="/bin/sh -c 'echo -n %k:1.0 > /sys/bus/usb/drivers/usblp/unbind 2>/dev/null || true'"
```

Reload and replug:

```bash
sudo udevadm control --reload-rules
```

### Printer model not auto-detected on USB

Many Phomemo printers report a generic name like "USB Composite Device" over USB rather than a recognizable model string. When this happens, the app will prompt you to pick the model on first connection and remember your choice for next time. You can also set it manually in Print Settings → Printer Model.

## Project Structure

```
Expand Down
57 changes: 45 additions & 12 deletions src/web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4622,9 +4622,13 @@ function initPrinterModelPrompt() {
* @param {MouseEvent} event - Click event (shift+click shows all devices)
*/
async function handleConnect(event) {
// Check if printing is supported in this browser
// No transports at all → tell the user which browsers do work
if (!state.canPrint) {
alert('Printing is not available in this browser.\n\nPlease use Chrome, Edge, or Opera on desktop for Bluetooth printing.');
alert(
'Printing is not available in this browser.\n\n' +
'Bluetooth and USB printing are both unavailable. Please use Chrome, ' +
'Edge, Opera, or another Chromium-based browser to print.'
);
return;
}

Expand Down Expand Up @@ -5619,6 +5623,26 @@ function hideShortcutsModal() {
$('#shortcuts-modal').classList.add('hidden');
}

/**
* Disable options in a connection-type <select> whose underlying transport
* (Web Bluetooth / WebUSB) isn't exposed by this browser.
* @param {HTMLSelectElement|null} selectEl
*/
function disableUnsupportedConnOptions(selectEl) {
if (!selectEl) return;
const hasBluetooth = 'bluetooth' in navigator;
const hasUsb = 'usb' in navigator;
for (const opt of selectEl.options) {
if (opt.value === 'ble' && !hasBluetooth) {
opt.disabled = true;
opt.title = 'Bluetooth printing isn\'t supported in this browser';
} else if (opt.value === 'usb' && !hasUsb) {
opt.disabled = true;
opt.title = 'USB printing isn\'t supported in this browser';
}
}
}

/**
* Check browser compatibility
*/
Expand All @@ -5631,13 +5655,19 @@ function checkCompatibility() {
canPrint = false;
}

if (!('bluetooth' in navigator)) {
warnings.push('Web Bluetooth not supported - printing requires Chrome, Edge, or Opera');
canPrint = false;
const hasBluetooth = 'bluetooth' in navigator;
const hasUsb = 'usb' in navigator;

if (!hasBluetooth) {
warnings.push('Bluetooth printing isn\'t supported in this browser');
}
if (!hasUsb) {
warnings.push('USB printing isn\'t supported in this browser');
}

if (!('usb' in navigator)) {
console.warn('WebUSB not supported - USB printing will not be available');
// Need at least one transport to print at all
if (!hasBluetooth && !hasUsb) {
canPrint = false;
}

// Store print capability in state for disabling print buttons
Expand All @@ -5657,7 +5687,7 @@ function checkCompatibility() {
</div>
<div>
<h3 class="text-lg font-bold">Limited Browser Support</h3>
<p class="text-amber-100 text-xs">Printing requires Chrome, Edge, or Opera</p>
<p class="text-amber-100 text-xs">Printing requires Chrome, Edge, Opera or another Chromium-based browser</p>
</div>
</div>
</div>
Expand Down Expand Up @@ -5938,6 +5968,7 @@ function initMobileUI() {
const mobileConnType = $('#mobile-conn-type');
const desktopConnType = $('#conn-type');
if (mobileConnType && desktopConnType) {
disableUnsupportedConnOptions(mobileConnType);
mobileConnType.value = desktopConnType.value;
mobileConnType.addEventListener('change', (e) => {
desktopConnType.value = e.target.value;
Expand Down Expand Up @@ -7118,11 +7149,13 @@ function init() {
setTapeWidth(parseInt(e.target.value, 10));
});

// Connection type
// Connection type — disable options whose underlying transport is missing
const connType = $('#conn-type');
if (!('usb' in navigator)) {
const usbOption = connType.querySelector('option[value="usb"]');
if (usbOption) usbOption.remove();
disableUnsupportedConnOptions(connType);
// If default ('ble') isn't supported, fall back to USB when available
if (!('bluetooth' in navigator) && 'usb' in navigator) {
state.connectionType = 'usb';
if (connType) connType.value = 'usb';
}
connType.addEventListener('change', (e) => {
state.connectionType = e.target.value;
Expand Down
2 changes: 1 addition & 1 deletion src/web/usb.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class USBTransport {
*/
async connect(options = {}) {
if (!USBTransport.isAvailable()) {
throw new Error('WebUSB is not supported in this browser');
throw new Error('USB printing isn\'t supported in this browser');
}

// Try to reconnect to existing device first
Expand Down