From 351d2935afeca171a6bdf407f1be6d39903beaee Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 13:59:12 +0100 Subject: [PATCH] USB printers in the network: system app "printer" (ipp-usb, p910nd) Current printers with IPP over USB print and scan without drivers (ipp-usb 0.9.34, patched to announce through static avahi service files because the firmware's avahi-daemon has no D-Bus); older printers over port 9100 with p910nd. Controller shareos-printer (follows plugging via mdev), rpcd shareos.printer, Services -> Printers in the interface, tests/test-printer.sh. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmfmQxsE59Ms99VrbCi1J1 --- .github/workflows/build.yml | 2 + CHANGELOG.md | 7 + CLAUDE.md | 2 +- Config.in | 1 + README.md | 1 + board/dlink/dns320l/post-build.sh | 3 + configs/shareos_sysapps_defconfig | 5 + package/shareos-core/shareos-core.mk | 2 + ...001-dnssd-static-avahi-service-files.patch | 647 ++++++++++++++++++ package/shareos-ipp-usb/Config.in | 16 + package/shareos-ipp-usb/shareos-ipp-usb.hash | 3 + package/shareos-ipp-usb/shareos-ipp-usb.mk | 27 + src/shareos-core/etc/config/printer | 8 + src/shareos-core/usr/sbin/shareos-printer | 229 +++++++ .../usr/share/rpcd/acl.d/shareos.json | 7 + .../usr/share/rpcd/ucode/shareos.uc | 52 +- src/shareos-core/www/app.js | 70 +- src/shareos-core/www/i18n/ar.json | 26 +- src/shareos-core/www/i18n/bn.json | 26 +- src/shareos-core/www/i18n/en.json | 26 +- src/shareos-core/www/i18n/es.json | 26 +- src/shareos-core/www/i18n/fr.json | 26 +- src/shareos-core/www/i18n/hi.json | 26 +- src/shareos-core/www/i18n/pt.json | 26 +- src/shareos-core/www/i18n/ru.json | 26 +- src/shareos-core/www/i18n/zh.json | 26 +- sysapps/printer/app.json | 18 + sysapps/printer/icon.svg | 1 + sysapps/printer/pack.json | 1 + tests/mock-ubus.py | 12 + tests/test-printer.sh | 68 ++ tools/i18n/ar.tsv | 24 + tools/i18n/bn.tsv | 24 + tools/i18n/es.tsv | 24 + tools/i18n/fr.tsv | 24 + tools/i18n/hi.tsv | 24 + tools/i18n/pt.tsv | 24 + tools/i18n/ru.tsv | 24 + tools/i18n/zh.tsv | 24 + 39 files changed, 1595 insertions(+), 13 deletions(-) create mode 100644 package/shareos-ipp-usb/0001-dnssd-static-avahi-service-files.patch create mode 100644 package/shareos-ipp-usb/Config.in create mode 100644 package/shareos-ipp-usb/shareos-ipp-usb.hash create mode 100644 package/shareos-ipp-usb/shareos-ipp-usb.mk create mode 100644 src/shareos-core/etc/config/printer create mode 100755 src/shareos-core/usr/sbin/shareos-printer create mode 100644 sysapps/printer/app.json create mode 100644 sysapps/printer/icon.svg create mode 100644 sysapps/printer/pack.json create mode 100755 tests/test-printer.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af78178..90bd6a1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,6 +48,8 @@ jobs: run: tests/test-bootlog.sh - name: Power schedule (shareos-power) run: LD_LIBRARY_PATH="$HOME/hostroot/lib" tests/test-power.sh "$HOME/hostroot/bin" + - name: Printers (shareos-printer with a fake /sys, ipp-usb and p910nd) + run: LD_LIBRARY_PATH="$HOME/hostroot/lib" tests/test-printer.sh "$HOME/hostroot/bin" - name: Zabbix agent controller (settings, disk/RAID items) run: LD_LIBRARY_PATH="$HOME/hostroot/lib" tests/test-zabbix-items.sh "$HOME/hostroot/bin" - name: SMART evaluation (disk health) diff --git a/CHANGELOG.md b/CHANGELOG.md index faf65fc..2513a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Changes per release (newest first). The section of a version is added to its GitHub release automatically when the release is built. Test versions are pre-releases for test devices only. +## 0.2.0-beta.18 + +- **USB printers in the network** (Dienste → Drucker, new system app **"printer"** in the store: ipp-usb 0.9.34 + p910nd 0.97, 3 MB). + - *Current printers and multifunction devices* that speak IPP over USB (most since about 2015, e.g. HP Smart Tank, OfficeJet, Canon PIXMA, Epson EcoTank) print **without a driver**: Windows 10/11, macOS, iPhone/iPad and Android find them by themselves (IPP Everywhere, AirPrint, Mopria) – and their **scanner** works over the network as well (eSCL). ipp-usb (OpenPrinting) does the work; ShareOS's avahi-daemon has no D-Bus, so a patch makes ipp-usb announce the devices through static avahi service files. The port of each device stays the same (kept on the flash). + - *Older printers* (usblp, `/dev/usb/lpN`): p910nd on port 9100 + N, announced as `_pdl-datastream`; the computer uses the maker's driver ("Standard TCP/IP port" in Windows, AppSocket/JetDirect). *Testseite* sends a plain-text/PCL page. + - Printers are followed when plugged in or pulled (mdev rule for `lp*`, ipp-usb watches USB itself). Mode *Immer Port 9100* for printers whose driverless printing misbehaves. `shareos-printer`, rpcd `shareos.printer`, `tests/test-printer.sh` (14 checks with a fake /sys). + ## 0.2.0-beta.17 - **SMB 3 with signing about three times as fast: the crypto engine computes the signatures** ([#94](https://github.com/UniqueDroid/ShareOS/issues/94)). Windows 11 signs every SMB connection, and on the 800 MHz Kirkwood the signature limited writing: measured on a DNS-320L, about 35 MB/s without signing (the disks), only 4–10 MB/s with SMB 3.1.1 – ksmbd computes AES-CMAC on the CPU, where AES manages about 12 MB/s. The Kirkwood has a crypto engine (Marvell CESA) that does AES-CBC at about 38 MB/s with DMA, but ksmbd could not use it. A kernel patch (`0001-ksmbd-cmac-via-cbc`) computes the AES-CMAC of messages from 8 KiB through `cbc(aes)` – the engine – (CMAC is a CBC-MAC; the last block and the subkeys on the CPU), checks the first result against the old way and falls back to it on any error. Measured: **19–23 MB/s** signed, the CPU load of ksmbd went from about 64 % to under 10 %. `shareos-shares` loads `marvell-cesa` before ksmbd; module parameter `ksmbd.cbc_cmac=0` switches it off. Tested before on a UML kernel with Samba's smbclient (signing required, checksums equal). Tried as well and dropped: AES-GMAC signing (GHASH in C on ARMv5 is not faster, 3–8 MB/s). diff --git a/CLAUDE.md b/CLAUDE.md index 1fa6455..076fb4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ - Tags/releases are created by Jan; prepare the release text and the `CHANGELOG.md` section. - **Every release updates the docs** – see `docs/RELEASE.md` (CHANGELOG, README status/features, TESTEN/TESTING, ENTWICKLUNG/DEVELOPMENT). - Photos for the docs: strip EXIF/GPS and blur serial-number stickers before committing. -- Tests: `tests/` (host: test-update, test-net, test-flash-bin, test-boards, test-raid-wait, test-smart, test-bootlog, test-power, test-notify, test-zabbix-items, check-acl.py, check-i18n.py; ARM emulator with sudo: run-stack-qemu --check, test-users, test-storage, test-cloud-paperless, test-apps, test-firstinstall, test-transmission, test-media, test-dlna, test-vpn, test-tailscale, test-autobrr, test-zabbix – these seven need `make sysapps-pack`, test-snapshots, test-sysperms). UI work: `tests/mock-ubus.py`; app SDK in Chromium: `node tests/sdk-storage.js` (Playwright). ucode plugin: `tests/check-ucode-order.py` (no hoisting in ucode). +- Tests: `tests/` (host: test-update, test-net, test-flash-bin, test-boards, test-raid-wait, test-smart, test-bootlog, test-power, test-notify, test-zabbix-items, test-printer, check-acl.py, check-i18n.py; ARM emulator with sudo: run-stack-qemu --check, test-users, test-storage, test-cloud-paperless, test-apps, test-firstinstall, test-transmission, test-media, test-dlna, test-vpn, test-tailscale, test-autobrr, test-zabbix – these seven need `make sysapps-pack`, test-snapshots, test-sysperms). UI work: `tests/mock-ubus.py`; app SDK in Chromium: `node tests/sdk-storage.js` (Playwright). ucode plugin: `tests/check-ucode-order.py` (no hoisting in ucode). - Never `rm -rf` a chroot that still has mounts below it (it would delete the host's /dev). - Apps live in the separate repository UniqueDroid/ShareOS-Apps. - New UI texts: add them to `i18n/en.json` and to every `tools/i18n/.tsv` (new numbers at the end), then `tools/i18n/build-lang.py`; `tests/check-i18n.py` must pass. diff --git a/Config.in b/Config.in index bbb1a24..5b65948 100644 --- a/Config.in +++ b/Config.in @@ -10,6 +10,7 @@ source "$BR2_EXTERNAL_SHAREOS_PATH/package/shareos-tailscale/Config.in" source "$BR2_EXTERNAL_SHAREOS_PATH/package/shareos-autobrr/Config.in" source "$BR2_EXTERNAL_SHAREOS_PATH/package/shareos-tuya/Config.in" source "$BR2_EXTERNAL_SHAREOS_PATH/package/shareos-zabbix-agent/Config.in" +source "$BR2_EXTERNAL_SHAREOS_PATH/package/shareos-ipp-usb/Config.in" endmenu diff --git a/README.md b/README.md index 4638001..68cfe8b 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Version in brackets = first release with the feature. - **USB sticks and disks:** mounted at `/mnt/usb-` when plugged in (FAT32, exFAT, NTFS, ext4, Btrfs), **Safely remove**, **Format** with exFAT, FAT32, NTFS or ext4 (beta.7) **Shares, users, files** +- USB printers in the network: current printers without a driver (IPP over USB: AirPrint, Mopria, Windows), scanning over the network (eSCL), older printers over port 9100 (system app "printer", 0.2.0-beta.18) - SMB 2/3 (ksmbd) and NFS v4 shares with read/write permissions per user and group, guest access optional; the crypto engine of the SoC computes the SMB 3 signatures (about three times as fast, 0.2.0-beta.17); compatibility mode up to SMB 2.1 for difficult clients (0.2.0-beta.17) - Users and groups; one account for shares, web interface and apps; storage limit per user (ext4); import of Alt-F users with their passwords - "My ShareOS": users with web access manage their own password, look, shares, storage, cloud and Paperless diff --git a/board/dlink/dns320l/post-build.sh b/board/dlink/dns320l/post-build.sh index cc8c2ca..ff658c7 100755 --- a/board/dlink/dns320l/post-build.sh +++ b/board/dlink/dns320l/post-build.sh @@ -79,6 +79,9 @@ done # (shareos-usb ignores the internal SATA disks) grep -q shareos-usb "$TARGET_DIR/etc/mdev.conf" || \ printf 'sd[a-z][0-9]*\troot:disk 660 */usr/sbin/shareos-usb hotplug\n' >> "$TARGET_DIR/etc/mdev.conf" +# USB printers (usblp, /dev/usb/lpN): the printer service follows them +grep -q shareos-printer "$TARGET_DIR/etc/mdev.conf" || \ + printf 'lp[0-9]*\troot:lp 660 */usr/sbin/shareos-printer hotplug\n' >> "$TARGET_DIR/etc/mdev.conf" # ntfs-3g: only mkntfs/ntfslabel are used (mounting goes through the kernel's ntfs3) for p in ntfs-3g ntfs-3g.probe lowntfs-3g ntfscat ntfscluster ntfscmp ntfsfix ntfsinfo ntfsls \ diff --git a/configs/shareos_sysapps_defconfig b/configs/shareos_sysapps_defconfig index 0d64061..c46d6ed 100644 --- a/configs/shareos_sysapps_defconfig +++ b/configs/shareos_sysapps_defconfig @@ -49,6 +49,11 @@ BR2_PACKAGE_SHAREOS_AUTOBRR=y # Zabbix agent, classic C version (system app "zabbix-agent"), see # package/shareos-zabbix-agent BR2_PACKAGE_SHAREOS_ZABBIX_AGENT=y +# printer (system app "printer"): IPP over USB for current printers and +# scanners (driverless), p910nd (port 9100, raw) for older ones, see +# package/shareos-ipp-usb +BR2_PACKAGE_SHAREOS_IPP_USB=y +BR2_PACKAGE_P910ND=y BR2_PACKAGE_HTOP=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_IPERF3=y diff --git a/package/shareos-core/shareos-core.mk b/package/shareos-core/shareos-core.mk index b02e40a..fd4c920 100644 --- a/package/shareos-core/shareos-core.mk +++ b/package/shareos-core/shareos-core.mk @@ -77,6 +77,7 @@ define SHAREOS_CORE_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0644 $(@D)/etc/config/dockerhosts $(TARGET_DIR)/etc/config/dockerhosts $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-dlna $(TARGET_DIR)/usr/sbin/shareos-dlna $(INSTALL) -D -m 0644 $(@D)/etc/config/dlna $(TARGET_DIR)/etc/config/dlna + $(INSTALL) -D -m 0644 $(@D)/etc/config/printer $(TARGET_DIR)/etc/config/printer $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-zabbix $(TARGET_DIR)/usr/sbin/shareos-zabbix $(INSTALL) -D -m 0644 $(@D)/etc/config/zabbix $(TARGET_DIR)/etc/config/zabbix $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-reset-access $(TARGET_DIR)/usr/sbin/shareos-reset-access @@ -89,6 +90,7 @@ define SHAREOS_CORE_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0755 $(@D)/etc/init.d/rcS $(TARGET_DIR)/etc/init.d/rcS $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-bootlog $(TARGET_DIR)/usr/sbin/shareos-bootlog $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-power $(TARGET_DIR)/usr/sbin/shareos-power + $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-printer $(TARGET_DIR)/usr/sbin/shareos-printer $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-users $(TARGET_DIR)/usr/sbin/shareos-users $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-shares $(TARGET_DIR)/usr/sbin/shareos-shares $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-quota $(TARGET_DIR)/usr/sbin/shareos-quota diff --git a/package/shareos-ipp-usb/0001-dnssd-static-avahi-service-files.patch b/package/shareos-ipp-usb/0001-dnssd-static-avahi-service-files.patch new file mode 100644 index 0000000..d53d16f --- /dev/null +++ b/package/shareos-ipp-usb/0001-dnssd-static-avahi-service-files.patch @@ -0,0 +1,647 @@ +From 89f8717bb322e6123eee8f772a97a33a56286f70 Mon Sep 17 00:00:00 2001 +From: ShareOS +Date: Sun, 1 Nov 2026 13:49:26 +0100 +Subject: [PATCH] DNS-SD through static avahi service files (avahi-daemon + without D-Bus) + +ShareOS runs avahi-daemon without D-Bus, so there is no avahi-client. +The services of a device are written as a static service file to +/etc/avahi/services (or $SHAREOS_AVAHI_SERVICES), which avahi-daemon +picks up by itself; Halt removes it. +--- + dnssd_avahi.go | 431 ------------------------------------------------ + dnssd_static.go | 184 +++++++++++++++++++++ + 2 files changed, 184 insertions(+), 431 deletions(-) + delete mode 100644 dnssd_avahi.go + create mode 100644 dnssd_static.go + +diff --git a/dnssd_avahi.go b/dnssd_avahi.go +deleted file mode 100644 +index a0a9bac..0000000 +--- a/dnssd_avahi.go ++++ /dev/null +@@ -1,431 +0,0 @@ +-//go:build linux || freebsd +-// +build linux freebsd +- +-/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device +- * +- * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) +- * See LICENSE for license terms and conditions +- * +- * DNS-SD publisher: Avahi-based system-dependent part +- */ +- +-package main +- +-// #cgo pkg-config: avahi-client +-// +-// #include +-// #include +-// #include +-// #include +-// #include +-// +-// void avahiClientCallback(AvahiClient*, AvahiClientState, void*); +-// void avahiEntryGroupCallback(AvahiEntryGroup*, AvahiEntryGroupState, void*); +-import "C" +- +-import ( +- "bytes" +- "errors" +- "fmt" +- "net/url" +- "sync" +- "unsafe" +-) +- +-var ( +- avahiInitLock sync.Mutex +- avahiThreadedPoll *C.AvahiThreadedPoll +- avahiClientMap = make(map[*C.AvahiClient]*dnssdSysdep) +- avahiEgroupMap = make(map[*C.AvahiEntryGroup]*dnssdSysdep) +-) +- +-// dnssdSysdep represents a system-dependent DNS-SD advertiser +-type dnssdSysdep struct { +- log *Logger // Device's logger +- instance string // Service Instance Name +- fqdn string // Host's fully-qualified domain name +- client *C.AvahiClient // Avahi client +- egroup *C.AvahiEntryGroup // Avahi entry group +- statusChan chan DNSSdStatus // Status notifications channel +-} +- +-// dnssdSysdepErr implements error interface on a top of +-// Avahi error codes +-type dnssdSysdepErr C.int +- +-// Error returns error string for the dnssdSysdepErr +-func (err dnssdSysdepErr) Error() string { +- return "Avahi error: " + C.GoString(C.avahi_strerror(C.int(err))) +-} +- +-// newDnssdSysdep creates new dnssdSysdep instance +-func newDnssdSysdep(log *Logger, instance string, +- services DNSSdServices) *dnssdSysdep { +- +- log.Debug(' ', "DNS-SD: %s: trying", instance) +- +- var err error +- var poll *C.AvahiPoll +- var rc C.int +- var proto, iface int +- +- sysdep := &dnssdSysdep{ +- log: log, +- instance: instance, +- statusChan: make(chan DNSSdStatus, 10), +- } +- +- // Obtain index of loopback interface +- loopback, err := Loopback() +- if err != nil { +- goto ERROR // Very unlikely to happen +- } +- +- // Obtain AvahiPoll +- poll, err = avahiGetPoll() +- if err != nil { +- goto ERROR +- } +- +- // Synchronize with Avahi thread +- avahiThreadLock() +- defer avahiThreadUnlock() +- +- // Create Avahi client +- sysdep.client = C.avahi_client_new( +- poll, +- C.AVAHI_CLIENT_NO_FAIL, +- C.AvahiClientCallback(C.avahiClientCallback), +- nil, +- &rc, +- ) +- +- if sysdep.client == nil { +- goto AVAHI_ERROR +- } +- +- avahiClientMap[sysdep.client] = sysdep +- +- sysdep.fqdn = C.GoString(C.avahi_client_get_host_name_fqdn(sysdep.client)) +- sysdep.log.Debug(' ', "DNS-SD: FQDN: %q", sysdep.fqdn) +- +- // Create entry group +- sysdep.egroup = C.avahi_entry_group_new( +- sysdep.client, +- C.AvahiEntryGroupCallback(C.avahiEntryGroupCallback), +- nil, +- ) +- +- if sysdep.egroup == nil { +- rc = C.avahi_client_errno(sysdep.client) +- goto AVAHI_ERROR +- } +- +- avahiEgroupMap[sysdep.egroup] = sysdep +- +- // Compute iface and proto, adjust fqdn +- iface = C.AVAHI_IF_UNSPEC +- if Conf.LoopbackOnly { +- iface = loopback +- old := sysdep.fqdn +- sysdep.fqdn = "localhost" +- sysdep.log.Debug(' ', "DNS-SD: FQDN: %q->%q", old, sysdep.fqdn) +- } +- +- proto = C.AVAHI_PROTO_UNSPEC +- if !Conf.IPV6Enable { +- proto = C.AVAHI_PROTO_INET +- } +- +- // Populate entry group +- for _, svc := range services { +- // Prepare TXT record +- var cTxt *C.AvahiStringList +- cTxt, err = sysdep.avahiTxtRecord(svc.Port, svc.Txt) +- if err != nil { +- goto ERROR +- } +- +- // Prepare C strings for service instance and type +- cSvcType := C.CString(svc.Type) +- +- var cInstance *C.char +- if svc.Instance != "" { +- cInstance = C.CString(svc.Instance) +- } else { +- cInstance = C.CString(instance) +- } +- +- // Handle loopback-only mode +- ifaceInUse := iface +- if svc.Loopback { +- ifaceInUse = loopback +- } +- +- // Register service type +- rc = C.avahi_entry_group_add_service_strlst( +- sysdep.egroup, +- C.AvahiIfIndex(ifaceInUse), +- C.AvahiProtocol(proto), +- 0, +- cInstance, +- cSvcType, +- nil, // Domain +- nil, // Host +- C.uint16_t(svc.Port), +- cTxt, +- ) +- +- // Register subtypes, if any +- for _, subtype := range svc.SubTypes { +- if rc != C.AVAHI_OK { +- break +- } +- +- sysdep.log.Debug(' ', "DNS-SD: +subtype: %q", subtype) +- +- cSubtype := C.CString(subtype) +- rc = C.avahi_entry_group_add_service_subtype( +- sysdep.egroup, +- C.AvahiIfIndex(ifaceInUse), +- C.AvahiProtocol(proto), +- 0, +- cInstance, +- cSvcType, +- nil, +- cSubtype, +- ) +- C.free(unsafe.Pointer(cSubtype)) +- +- } +- +- // Release C memory +- C.free(unsafe.Pointer(cInstance)) +- C.free(unsafe.Pointer(cSvcType)) +- C.avahi_string_list_free(cTxt) +- +- // Check for Avahi error +- if rc != C.AVAHI_OK { +- goto AVAHI_ERROR +- } +- } +- +- // Commit changes +- rc = C.avahi_entry_group_commit(sysdep.egroup) +- if rc != C.AVAHI_OK { +- goto AVAHI_ERROR +- } +- +- // Create and return dnssdSysdep +- return sysdep +- +- // Error: cleanup and exit +-AVAHI_ERROR: +- err = dnssdSysdepErr(rc) +-ERROR: +- +- // Raise an error event +- sysdep.log.Error(' ', "DNS-SD: %s: %s", sysdep.instance, err) +- sysdep.haltLocked() +- +- if err == dnssdSysdepErr(C.AVAHI_ERR_COLLISION) { +- sysdep.notify(DNSSdCollision) +- } else { +- sysdep.notify(DNSSdFailure) +- } +- +- return sysdep +-} +- +-// Halt dnssdSysdep +-// +-// It cancel all activity related to the dnssdSysdep instance, +-// but sysdep.Chan() remains valid, though no notifications +-// will be pushed there anymore +-func (sysdep *dnssdSysdep) Halt() { +- avahiThreadLock() +- sysdep.haltLocked() +- avahiThreadUnlock() +-} +- +-// Get status change notification channel +-func (sysdep *dnssdSysdep) Chan() <-chan DNSSdStatus { +- return sysdep.statusChan +-} +- +-// Halt dnssdSysdep -- internal version +-// +-// Must be called under avahiThreadLock +-// Can be used with semi-constructed dnssdSysdep +-func (sysdep *dnssdSysdep) haltLocked() { +- // Free all Avahi stuff +- if sysdep.egroup != nil { +- C.avahi_entry_group_free(sysdep.egroup) +- delete(avahiEgroupMap, sysdep.egroup) +- sysdep.egroup = nil +- } +- +- if sysdep.client != nil { +- C.avahi_client_free(sysdep.client) +- delete(avahiClientMap, sysdep.client) +- sysdep.client = nil +- } +- +- // Drain status channel +- for len(sysdep.statusChan) > 0 { +- <-sysdep.statusChan +- } +-} +- +-// Push status change notification +-func (sysdep *dnssdSysdep) notify(status DNSSdStatus) { +- sysdep.statusChan <- status +-} +- +-// avahiTxtRecord converts DNSSdTxtRecord to AvahiStringList +-func (sysdep *dnssdSysdep) avahiTxtRecord(port int, txt DNSSdTxtRecord) ( +- *C.AvahiStringList, error) { +- var buf bytes.Buffer +- var list, prev *C.AvahiStringList +- +- for _, t := range txt { +- buf.Reset() +- buf.WriteString(t.Key) +- buf.WriteByte('=') +- +- if !t.URL || sysdep.fqdn == "" { +- buf.WriteString(t.Value) +- } else { +- value := t.Value +- if parsed, err := url.Parse(value); err == nil && parsed.IsAbs() { +- parsed.Host = sysdep.fqdn +- if port != 0 { +- parsed.Host += fmt.Sprintf(":%d", port) +- } +- +- value = parsed.String() +- } +- buf.WriteString(value) +- } +- +- b := buf.Bytes() +- +- prev, list = list, C.avahi_string_list_add_arbitrary( +- list, +- (*C.uint8_t)(unsafe.Pointer(&b[0])), +- C.size_t(len(b)), +- ) +- +- if list == nil { +- C.avahi_string_list_free(prev) +- return nil, ErrNoMemory +- } +- } +- +- return C.avahi_string_list_reverse(list), nil +-} +- +-// avahiClientCallback called by Avahi client to notify us about +-// client state change +-// +-//export avahiClientCallback +-func avahiClientCallback(client *C.AvahiClient, +- state C.AvahiClientState, _ unsafe.Pointer) { +- +- sysdep := avahiClientMap[client] +- if sysdep == nil { +- return +- } +- +- status := DNSSdNoStatus +- event := "" +- +- switch state { +- case C.AVAHI_CLIENT_S_REGISTERING: +- event = "AVAHI_CLIENT_S_REGISTERING" +- case C.AVAHI_CLIENT_S_RUNNING: +- event = "AVAHI_CLIENT_S_RUNNING" +- case C.AVAHI_CLIENT_S_COLLISION: +- // This is host name collision. We can't recover +- // it here, so lets consider it as DNSSdFailure +- event = "AVAHI_CLIENT_S_COLLISION" +- status = DNSSdFailure +- case C.AVAHI_CLIENT_FAILURE: +- event = "AVAHI_CLIENT_FAILURE" +- status = DNSSdFailure +- case C.AVAHI_CLIENT_CONNECTING: +- event = "AVAHI_CLIENT_CONNECTING" +- default: +- event = fmt.Sprintf("Unknown event %d", state) +- } +- +- sysdep.log.Debug(' ', "DNS-SD: %s: %s", sysdep.instance, event) +- if status != DNSSdNoStatus { +- sysdep.notify(status) +- } +-} +- +-// avahiEntryGroupCallback called by Avahi client to notify us about +-// entry group state change +-// +-//export avahiEntryGroupCallback +-func avahiEntryGroupCallback(egroup *C.AvahiEntryGroup, +- state C.AvahiEntryGroupState, _ unsafe.Pointer) { +- +- sysdep := avahiEgroupMap[egroup] +- if sysdep == nil { +- return +- } +- +- status := DNSSdNoStatus +- event := "" +- +- switch state { +- case C.AVAHI_ENTRY_GROUP_UNCOMMITED: +- event = "AVAHI_ENTRY_GROUP_UNCOMMITED" +- case C.AVAHI_ENTRY_GROUP_REGISTERING: +- event = "AVAHI_ENTRY_GROUP_REGISTERING" +- case C.AVAHI_ENTRY_GROUP_ESTABLISHED: +- event = "AVAHI_ENTRY_GROUP_ESTABLISHED" +- status = DNSSdSuccess +- case C.AVAHI_ENTRY_GROUP_COLLISION: +- event = "AVAHI_ENTRY_GROUP_COLLISION" +- status = DNSSdCollision +- case C.AVAHI_ENTRY_GROUP_FAILURE: +- event = "AVAHI_ENTRY_GROUP_FAILURE" +- status = DNSSdFailure +- } +- +- sysdep.log.Debug(' ', "DNS-SD: %s: %s", sysdep.instance, event) +- if status != DNSSdNoStatus { +- sysdep.notify(status) +- } +-} +- +-// avahiGetPoll returns pointer to AvahiPoll +-// Avahi helper thread is created on demand +-func avahiGetPoll() (*C.AvahiPoll, error) { +- avahiInitLock.Lock() +- defer avahiInitLock.Unlock() +- +- if avahiThreadedPoll == nil { +- avahiThreadedPoll = C.avahi_threaded_poll_new() +- if avahiThreadedPoll == nil { +- return nil, errors.New("initialization failed, not enough memory") +- } +- +- C.avahi_threaded_poll_start(avahiThreadedPoll) +- } +- +- return C.avahi_threaded_poll_get(avahiThreadedPoll), nil +-} +- +-// Lock Avahi thread +-func avahiThreadLock() { +- C.avahi_threaded_poll_lock(avahiThreadedPoll) +-} +- +-// Unlock Avahi thread +-func avahiThreadUnlock() { +- C.avahi_threaded_poll_unlock(avahiThreadedPoll) +-} +diff --git a/dnssd_static.go b/dnssd_static.go +new file mode 100644 +index 0000000..8736f70 +--- /dev/null ++++ b/dnssd_static.go +@@ -0,0 +1,184 @@ ++//go:build linux || freebsd ++// +build linux freebsd ++ ++/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device ++ * ++ * DNS-SD publisher for ShareOS: the box runs avahi-daemon without D-Bus, ++ * so there is no avahi-client to talk to. Instead the services of a device ++ * are written as a static service file to /etc/avahi/services (or the ++ * folder in SHAREOS_AVAHI_SERVICES), which avahi-daemon picks up by itself ++ * (inotify). Halt removes the file again. ++ */ ++ ++package main ++ ++import ( ++ "bytes" ++ "fmt" ++ "net/url" ++ "os" ++ "path/filepath" ++ "strings" ++) ++ ++// dnssdSysdep represents a system-dependent DNS-SD advertiser ++type dnssdSysdep struct { ++ log *Logger // Device's logger ++ instance string // Service Instance Name ++ file string // the service file written for it ++ statusChan chan DNSSdStatus // Status notifications channel ++} ++ ++func dnssdStaticDir() string { ++ if d := os.Getenv("SHAREOS_AVAHI_SERVICES"); d != "" { ++ return d ++ } ++ return "/etc/avahi/services" ++} ++ ++func dnssdXMLEscape(s string) string { ++ var b bytes.Buffer ++ for _, r := range s { ++ switch r { ++ case '&': ++ b.WriteString("&") ++ case '<': ++ b.WriteString("<") ++ case '>': ++ b.WriteString(">") ++ case '"': ++ b.WriteString(""") ++ case '\'': ++ b.WriteString("'") ++ default: ++ if r >= 0x20 || r == '\t' { ++ b.WriteRune(r) ++ } ++ } ++ } ++ return b.String() ++} ++ ++// file name of an instance: only harmless characters ++func dnssdFileName(instance string) string { ++ var b strings.Builder ++ for _, r := range instance { ++ if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' { ++ b.WriteRune(r) ++ } else { ++ b.WriteByte('_') ++ } ++ } ++ return "ipp-usb-" + b.String() + ".service" ++} ++ ++// newDnssdSysdep creates new dnssdSysdep instance ++func newDnssdSysdep(log *Logger, instance string, ++ services DNSSdServices) *dnssdSysdep { ++ ++ sysdep := &dnssdSysdep{ ++ log: log, ++ instance: instance, ++ statusChan: make(chan DNSSdStatus, 10), ++ } ++ ++ host, _ := os.Hostname() ++ if i := strings.IndexByte(host, '.'); i > 0 { ++ host = host[:i] ++ } ++ fqdn := host + ".local" ++ if Conf.LoopbackOnly { ++ fqdn = "localhost" ++ } ++ ++ proto := "any" ++ if !Conf.IPV6Enable { ++ proto = "ipv4" ++ } ++ ++ var x bytes.Buffer ++ x.WriteString("\n") ++ x.WriteString("\n") ++ x.WriteString("\n") ++ x.WriteString("\n") ++ fmt.Fprintf(&x, " %s\n", dnssdXMLEscape(instance)) ++ n := 0 ++ for _, svc := range services { ++ // a static file is announced on every interface: services meant ++ // for the loopback interface only are left out ++ if svc.Loopback || Conf.LoopbackOnly { ++ continue ++ } ++ n++ ++ fmt.Fprintf(&x, " \n", proto) ++ fmt.Fprintf(&x, " %s\n", dnssdXMLEscape(svc.Type)) ++ for _, st := range svc.SubTypes { ++ fmt.Fprintf(&x, " %s\n", dnssdXMLEscape(st)) ++ } ++ if svc.Instance != "" && svc.Instance != instance { ++ log.Debug(' ', "DNS-SD: instance %q announced as %q", svc.Instance, instance) ++ } ++ fmt.Fprintf(&x, " %d\n", svc.Port) ++ for _, t := range svc.Txt { ++ value := t.Value ++ if t.URL { ++ if parsed, err := url.Parse(value); err == nil && parsed.IsAbs() { ++ parsed.Host = fqdn ++ if svc.Port != 0 { ++ parsed.Host += fmt.Sprintf(":%d", svc.Port) ++ } ++ value = parsed.String() ++ } ++ } ++ fmt.Fprintf(&x, " %s=%s\n", ++ dnssdXMLEscape(t.Key), dnssdXMLEscape(value)) ++ } ++ x.WriteString(" \n") ++ } ++ x.WriteString("\n") ++ ++ if n == 0 { ++ sysdep.notify(DNSSdSuccess) ++ return sysdep ++ } ++ ++ dir := dnssdStaticDir() ++ sysdep.file = filepath.Join(dir, dnssdFileName(instance)) ++ tmp := sysdep.file + ".tmp" ++ err := os.WriteFile(tmp, x.Bytes(), 0644) ++ if err == nil { ++ err = os.Rename(tmp, sysdep.file) ++ } ++ if err != nil { ++ os.Remove(tmp) ++ sysdep.file = "" ++ log.Error(' ', "DNS-SD: %s: %s", instance, err) ++ sysdep.notify(DNSSdFailure) ++ return sysdep ++ } ++ ++ log.Debug(' ', "DNS-SD: %s: written to %s", instance, sysdep.file) ++ sysdep.notify(DNSSdSuccess) ++ return sysdep ++} ++ ++// Halt dnssdSysdep: the service file goes away ++func (sysdep *dnssdSysdep) Halt() { ++ if sysdep.file != "" { ++ os.Remove(sysdep.file) ++ sysdep.file = "" ++ } ++ for len(sysdep.statusChan) > 0 { ++ <-sysdep.statusChan ++ } ++} ++ ++// Get status change notification channel ++func (sysdep *dnssdSysdep) Chan() <-chan DNSSdStatus { ++ return sysdep.statusChan ++} ++ ++// Push status change notification ++func (sysdep *dnssdSysdep) notify(status DNSSdStatus) { ++ sysdep.statusChan <- status ++} +-- +2.43.0 + diff --git a/package/shareos-ipp-usb/Config.in b/package/shareos-ipp-usb/Config.in new file mode 100644 index 0000000..d7cbf95 --- /dev/null +++ b/package/shareos-ipp-usb/Config.in @@ -0,0 +1,16 @@ +config BR2_PACKAGE_SHAREOS_IPP_USB + bool "shareos-ipp-usb" + depends on BR2_PACKAGE_HOST_GO_TARGET_ARCH_SUPPORTS + depends on BR2_PACKAGE_HOST_GO_TARGET_CGO_LINKING_SUPPORTS + depends on BR2_TOOLCHAIN_HAS_THREADS # libusb + select BR2_PACKAGE_HOST_GO + select BR2_PACKAGE_LIBUSB + help + ipp-usb for the system app "printer": makes a USB printer or + scanner that speaks IPP over USB (most printers since about + 2015) available in the network – driverless printing (AirPrint, + Mopria, IPP Everywhere) and scanning (eSCL). DNS-SD through + static avahi service files, as the firmware's avahi-daemon has + no D-Bus. + + https://github.com/OpenPrinting/ipp-usb diff --git a/package/shareos-ipp-usb/shareos-ipp-usb.hash b/package/shareos-ipp-usb/shareos-ipp-usb.hash new file mode 100644 index 0000000..da43690 --- /dev/null +++ b/package/shareos-ipp-usb/shareos-ipp-usb.hash @@ -0,0 +1,3 @@ +# Locally calculated (git tag 0.9.34, Go modules vendored by buildroot) +sha256 062fbbb29519ef5786f7f06d5c1200e9208d3f744094a32c19ae9a2761cb347b shareos-ipp-usb-0.9.34-git4-go2.tar.gz +sha256 744244cc574688a0017c85d81b2555b1af768601f242f079fac4f7570c822e47 LICENSE diff --git a/package/shareos-ipp-usb/shareos-ipp-usb.mk b/package/shareos-ipp-usb/shareos-ipp-usb.mk new file mode 100644 index 0000000..15f45ea --- /dev/null +++ b/package/shareos-ipp-usb/shareos-ipp-usb.mk @@ -0,0 +1,27 @@ +################################################################################ +# +# shareos-ipp-usb – IPP over USB proxy for the system app "printer" +# +################################################################################ + +SHAREOS_IPP_USB_VERSION = 0.9.34 +SHAREOS_IPP_USB_SITE = https://github.com/OpenPrinting/ipp-usb.git +SHAREOS_IPP_USB_SITE_METHOD = git +SHAREOS_IPP_USB_LICENSE = BSD-2-Clause +SHAREOS_IPP_USB_LICENSE_FILES = LICENSE +SHAREOS_IPP_USB_GOMOD = github.com/OpenPrinting/ipp-usb +SHAREOS_IPP_USB_DEPENDENCIES = libusb host-pkgconf +SHAREOS_IPP_USB_GO_ENV = GOPROXY=https://proxy.golang.org,direct +SHAREOS_IPP_USB_BUILD_TARGETS = . +# no symbol tables: about a third smaller +SHAREOS_IPP_USB_LDFLAGS = -s -w +SHAREOS_IPP_USB_BIN_NAME = ipp-usb + +# the quirks of known devices (the box keeps them with the program) +define SHAREOS_IPP_USB_INSTALL_QUIRKS + mkdir -p $(TARGET_DIR)/usr/share/ipp-usb/quirks + cp $(@D)/ipp-usb-quirks/*.conf $(TARGET_DIR)/usr/share/ipp-usb/quirks/ +endef +SHAREOS_IPP_USB_POST_INSTALL_TARGET_HOOKS += SHAREOS_IPP_USB_INSTALL_QUIRKS + +$(eval $(golang-package)) diff --git a/src/shareos-core/etc/config/printer b/src/shareos-core/etc/config/printer new file mode 100644 index 0000000..710c097 --- /dev/null +++ b/src/shareos-core/etc/config/printer @@ -0,0 +1,8 @@ +# USB printers (system app "printer": ipp-usb, p910nd) – managed by +# shareos-printer (web interface: Services -> Printers). +# +# option mode 'auto' # auto: IPP over USB where the printer can | raw: port 9100 for all + +config printer 'main' + option enabled '0' + option mode 'auto' diff --git a/src/shareos-core/usr/sbin/shareos-printer b/src/shareos-core/usr/sbin/shareos-printer new file mode 100755 index 0000000..7ab38ec --- /dev/null +++ b/src/shareos-core/usr/sbin/shareos-printer @@ -0,0 +1,229 @@ +#!/bin/sh +# +# shareos-printer – USB printers in the network (system app "printer") +# +# shareos-printer start | stop | restart +# shareos-printer apply start or stop what the plugged-in printers need +# (mdev calls it when a printer comes or goes) +# shareos-printer status key=value lines for the web interface +# shareos-printer test print a test page on an older printer (port 9100) +# shareos-printer boot start at boot when it is switched on +# shareos-printer stop-all stop (system app removed, shutdown) +# shareos-printer hotplug called by mdev (ACTION, MDEV) – runs apply +# +# shareos-service: boot start stop-all +# +# Two ways, chosen per printer: +# - Printers that speak IPP over USB (most since about 2015, interface class +# 7/1/4): ipp-usb makes them printers and scanners in the network – +# driverless printing (AirPrint, Mopria, IPP Everywhere, Windows 10/11 +# finds them by itself) and scanning (eSCL). Port 60000 and up. +# - Older printers: p910nd passes the print data from port 9100 + N to +# /dev/usb/lpN as it is; the computer needs the printer's driver +# ("Standard TCP/IP port" in Windows, AppSocket/JetDirect elsewhere). +# Both are announced in the network (avahi service files). +# +# Settings: UCI printer.main – enabled, mode auto|raw (raw: p910nd for every +# printer, also those with IPP over USB). ipp-usb keeps the port of each +# device in /etc/shareos/printer/dev, so computers keep finding it. +# SHAREOS_PRINTER_SYS= instead of /sys (tests) +# SHAREOS_ROOT= fake root for files (tests) +# +[ -n "$SHAREOS_ROOT" ] || PATH=/usr/sbin:/usr/bin:/sbin:/bin:/opt/shareos/bin +R=${SHAREOS_ROOT:-} +SYS=${SHAREOS_PRINTER_SYS:-/sys} +IPPUSB=${SHAREOS_PRINTER_IPPUSB:-/opt/shareos/bin/ipp-usb} +P910ND=${SHAREOS_PRINTER_P910ND:-/opt/shareos/bin/p910nd} +QUIRKS=/opt/shareos/sysapps/printer/share/ipp-usb/quirks +RUN=$R/tmp/shareos-printer +STATE=$R/etc/shareos/printer +AVAHI=$R/etc/avahi/services + +die() { echo "error: $*"; exit 1; } +uget() { if [ -n "$SHAREOS_ROOT" ]; then uci -q -c "$R/etc/config" get "$1"; else uci -q get "$1"; fi; } +enabled() { [ "$(uget printer.main.enabled)" = 1 ]; } +mode() { [ "$(uget printer.main.mode)" = raw ] && echo raw || echo auto; } +installed() { [ -x "$IPPUSB" ] && [ -x "$P910ND" ]; } + +# printers: one line per USB printer +# | +printers() { + for i in "$SYS"/bus/usb/devices/*:*; do + [ "$(cat "$i/bInterfaceClass" 2>/dev/null)" = 07 ] || continue + echo "${i%:*}" + done | sort -u | while read -r d; do + ipp=0; lp=- + for i in "$d":*; do + [ "$(cat "$i/bInterfaceClass" 2>/dev/null)" = 07 ] || continue + [ "$(cat "$i/bInterfaceProtocol" 2>/dev/null)" = 04 ] && ipp=1 + for n in "$i"/usbmisc/lp*; do [ -e "$n" ] && lp=$(basename "$n"); done + done + echo "$(basename "$d") $(cat "$d/idVendor" 2>/dev/null):$(cat "$d/idProduct" 2>/dev/null) $ipp $lp $(cat "$d/manufacturer" 2>/dev/null) | $(cat "$d/product" 2>/dev/null)" + done +} + +ippusb_pid() { + p=$(cat $RUN/ipp-usb.pid 2>/dev/null) + [ -n "$p" ] && kill -0 "$p" 2>/dev/null && grep -q ipp-usb /proc/$p/cmdline 2>/dev/null && echo "$p" +} +p910_pid() { # + p=$(cat $RUN/p910$1.pid 2>/dev/null) + [ -n "$p" ] && kill -0 "$p" 2>/dev/null && echo "$p" +} + +ippusb_conf() { + mkdir -p $RUN/conf $RUN/log "$STATE/dev" + cat > $RUN/conf/ipp-usb.conf <>$RUN/ipp-usb.out 2>&1 & + echo $! > $RUN/ipp-usb.pid +} + +ippusb_stop() { + p=$(ippusb_pid) + if [ -n "$p" ]; then + kill "$p" + i=0; while [ $i -lt 25 ] && kill -0 "$p" 2>/dev/null; do sleep 0.2; i=$((i + 1)); done + kill -9 "$p" 2>/dev/null + fi + rm -f $RUN/ipp-usb.pid "$AVAHI"/ipp-usb-*.service +} + +p910_service() { # + cat > "$AVAHI/shareos-printer-lp$1.service.tmp" < + + + $(echo "$2" | sed 's/&/\&/g;s//\>/g') + + _pdl-datastream._tcp + $((9100 + $1)) + txtvers=1 + qtotal=1 + ty=$(echo "$2" | sed 's/&/\&/g;s//\>/g') + note=ShareOS + + +EOF + mv "$AVAHI/shareos-printer-lp$1.service.tmp" "$AVAHI/shareos-printer-lp$1.service" +} + +p910_start() { # + [ -n "$(p910_pid $1)" ] && return 0 + mkdir -p $RUN $R/var/lock/subsys + # -b: bidirectional (the printer's answers go back); stays in front, the + # controller keeps its pid + "$P910ND" -f $R/dev/usb/lp$1 -b -d $1 >>$RUN/p910$1.out 2>&1 & + echo $! > $RUN/p910$1.pid + p910_service $1 "$2" +} + +p910_stop() { # + p=$(p910_pid $1) + [ -n "$p" ] && kill "$p" 2>/dev/null + rm -f $RUN/p910$1.pid "$AVAHI/shareos-printer-lp$1.service" +} + +# start what the printers need, stop the rest +apply() { + mkdir -p $RUN "$AVAHI" + if ! enabled || ! installed; then stop_all; return 0; fi + m=$(mode) + [ $m = auto ] && ippusb_start || ippusb_stop + want= + printers > $RUN/printers + while read -r dev id ipp lp name; do + [ "$lp" = - ] && continue + [ $m = auto ] && [ $ipp = 1 ] && continue + n=${lp#lp} + want="$want $n" + p910_start $n "$(echo "$name" | sed 's/^ *| *//;s/ *| */ /')" + done < $RUN/printers + for f in $RUN/p910*.pid; do + [ -e "$f" ] || continue + n=${f#$RUN/p910}; n=${n%.pid} + case " $want " in *" $n "*) ;; *) p910_stop $n ;; esac + done + return 0 +} + +stop_all() { + ippusb_stop + for f in $RUN/p910*.pid; do + [ -e "$f" ] || continue + n=${f#$RUN/p910}; n=${n%.pid} + p910_stop $n + done + rm -f "$AVAHI"/shareos-printer-lp*.service +} + +status() { + echo "installed=$(installed && echo 1 || echo 0)" + echo "enabled=$(enabled && echo 1 || echo 0)" + echo "mode=$(mode)" + echo "ippusb=$([ -n "$(ippusb_pid)" ] && echo 1 || echo 0)" + # ports of the IPP-over-USB devices from ipp-usb itself + ports= + if [ -n "$(ippusb_pid)" ]; then + ports=$("$IPPUSB" status -path-ctrl-sock $RUN/ipp-usb.sock -path-conf-files-srch $RUN/conf 2>/dev/null \ + | sed -n 's/^ *[0-9]*\. *Bus [0-9]* Device [0-9]* *\([0-9a-f]*:[0-9a-f]*\) *\([0-9-]*\) .*/\1=\2/p') + fi + printers | while read -r dev id ipp lp name; do + via=; port= + if [ $(mode) = auto ] && [ $ipp = 1 ]; then + via=ipp-usb; port=$(echo "$ports" | sed -n "s/^$id=\([0-9]*\)$/\1/p" | head -1) + elif [ "$lp" != - ]; then + via=p910nd; n=${lp#lp}; [ -n "$(p910_pid $n)" ] && port=$((9100 + n)) + fi + enabled || port= + echo "printer=$id|$ipp|$lp|$via|$port|$name" + done +} + +# a test page for printers that take plain text or PCL (port 9100 ones; a +# printer that only understands its driver's language prints nothing) +testpage() { + echo "$1" | grep -qE '^lp[0-9]+$' || die "no such printer" + [ -e $R/dev/usb/$1 ] || die "no such printer" + n=${1#lp} + host=$(cat /proc/sys/kernel/hostname 2>/dev/null) + # straight to the device (p910nd only holds it during a job) + printf '\033E\r\nShareOS test page\r\nBox: %s\r\nPort: %s\r\nTime: %s\r\n\f\033E' "$host" "$((9100 + n))" "$(date '+%Y-%m-%d %H:%M')" > $RUN/testpage + timeout 15 sh -c "cat $RUN/testpage > $R/dev/usb/$1" 2>/dev/null || die "the printer did not take the page" +} + +case "$1" in + start|boot) + [ "$1" = boot ] && ! enabled && exit 0 + installed || { [ "$1" = boot ] && exit 0; die "the system app \"printer\" is not installed (store)"; } + apply ;; + apply) apply ;; + stop|stop-all) stop_all ;; + restart) stop_all; apply ;; + hotplug) ( sleep 2; "$0" apply ) >/dev/null 2>&1 & ;; + status) status ;; + test) testpage "$2" ;; + *) sed -n '3,30s/^# \{0,1\}//p' "$0"; exit 2 ;; +esac +exit 0 diff --git a/src/shareos-core/usr/share/rpcd/acl.d/shareos.json b/src/shareos-core/usr/share/rpcd/acl.d/shareos.json index 2f1ba43..955b1f4 100644 --- a/src/shareos-core/usr/share/rpcd/acl.d/shareos.json +++ b/src/shareos-core/usr/share/rpcd/acl.d/shareos.json @@ -107,6 +107,9 @@ ], "shareos.status": [ "chips" + ], + "shareos.printer": [ + "status" ] }, "uci": [ @@ -287,6 +290,10 @@ "set", "test", "telegram_chats" + ], + "shareos.printer": [ + "set", + "test" ] }, "uci": [ diff --git a/src/shareos-core/usr/share/rpcd/ucode/shareos.uc b/src/shareos-core/usr/share/rpcd/ucode/shareos.uc index 9a13d8c..f5c8ca3 100644 --- a/src/shareos-core/usr/share/rpcd/ucode/shareos.uc +++ b/src/shareos-core/usr/share/rpcd/ucode/shareos.uc @@ -841,7 +841,7 @@ function default_password() { /* ---------- settings backup ---------- */ const CRYPT = '/usr/sbin/shareos-crypt'; -const BACKUP_CONFIGS = [ 'shareos', 'storage', 'network', 'accounts', 'shares', 'paperless', 'cloud', 'apps', 'snapshots', 'dlna', 'dockerhosts', 'zabbix' ]; +const BACKUP_CONFIGS = [ 'shareos', 'storage', 'network', 'accounts', 'shares', 'paperless', 'cloud', 'apps', 'snapshots', 'dlna', 'dockerhosts', 'zabbix', 'printer' ]; const BACKUP_FILES = [ '/etc/ksmbd/ksmbdpwd.db', '/etc/ssl/shareos/server.crt', '/etc/ssl/shareos/server.key', '/etc/hostname', '/etc/timezone', '/etc/shareos/zabbix.psk' ]; function lines_of(path) { return filter(split(readfile(path) ?? '', '\n'), (l) => l != ''); } @@ -1166,6 +1166,25 @@ function dlna_status() { shares: map(filter(share_list().shares, (x) => x.volume), (x) => x.name) }; } +/* ---------- printers (system app "printer": ipp-usb, p910nd) ---------- */ + +const PRINTER = '/usr/sbin/shareos-printer'; +function printer_status() { + let st = {}, printers = []; + for (let l in split(sh(PRINTER + ' status').out, '\n')) { + let m = match(l, /^([a-z0-9]+)=(.*)$/); + if (!m) continue; + if (m[1] == 'printer') { + let f = split(m[2], '|'); + push(printers, { id: f[0], ipp: f[1] == '1', lp: f[2] == '-' ? null : f[2], via: f[3] || null, port: f[4] ? +f[4] : null, + maker: trim(f[5] ?? ''), model: trim(join('|', slice(f, 6))) }); + } + else st[m[1]] = m[2]; + } + return { installed: st.installed == '1', enabled: st.enabled == '1', mode: st.mode ?? 'auto', ippusb: st.ippusb == '1', printers, + hostname: trim(readfile('/proc/sys/kernel/hostname') ?? '') }; +} + /* ---------- snapshots (rsync with hard links, shareos-snapshot) ---------- */ const SNAP = '/usr/sbin/shareos-snapshot'; @@ -3688,6 +3707,37 @@ return { } }, + 'shareos.printer': { + status: { call: function() { return printer_status(); } }, + + set: { + args: { enabled: false, mode: '' }, + call: function(req) { + let a = req.args, c = cursor(); + if (a.mode && !(a.mode in { auto: 1, raw: 1 })) return { error: 'invalid mode' }; + c.set('printer', 'main', 'printer'); + c.set('printer', 'main', 'enabled', a.enabled === true ? '1' : '0'); + if (a.mode) c.set('printer', 'main', 'mode', a.mode); + c.commit('printer'); + let r = sh(PRINTER + (a.enabled === true ? ' restart' : ' stop')); + let m = match(r.out, /error: ([^\n]*)/); + if (m || r.rc != 0) return { error: m ? m[1] : (trim(r.out) || 'failed') }; + sleep(500); + return printer_status(); + } + }, + + test: { + args: { lp: '' }, + call: function(req) { + if (!match(req.args.lp ?? '', /^lp[0-9]$/)) return { error: 'no such printer' }; + let r = sh(PRINTER + ' test ' + req.args.lp); + let m = match(r.out, /error: ([^\n]*)/); + return m || r.rc != 0 ? { error: m ? m[1] : 'failed' } : { ok: true }; + } + } + }, + // chips in the header of the interface: services and apps of the account 'shareos.status': { chips: { diff --git a/src/shareos-core/www/app.js b/src/shareos-core/www/app.js index eafdb45..34e7bac 100644 --- a/src/shareos-core/www/app.js +++ b/src/shareos-core/www/app.js @@ -156,7 +156,7 @@ const TABS = [ ['apps', 'APPS'], ['cloud', 'CLOUD'], ['paperless', 'PAPERLESS'], ['system', 'SYSTEM'] ]; const SYS_SUBS = [['general', 'ALLGEMEIN'], ['appearance', 'DARSTELLUNG'], ['network', 'NETZWERK'], ['notifications', 'MELDUNGEN'], ['logs', 'PROTOKOLLE'], ['terminal', 'TERMINAL'], ['time', 'ZEIT'], ['power', 'ENERGIE & RESET'], ['firmware', 'FIRMWARE']]; -const SVC_SUBS = [['status', 'STATUS'], ['autostart', 'AUTOSTART'], ['dlna', 'MEDIENSERVER']]; +const SVC_SUBS = [['status', 'STATUS'], ['autostart', 'AUTOSTART'], ['dlna', 'MEDIENSERVER'], ['printer', 'DRUCKER']]; const SHR_SUBS = [['shares', 'FREIGABEN'], ['files', 'DATEIEN'], ['users', 'BENUTZER'], ['groups', 'GRUPPEN']]; const STO_SUBS = [['volumes', 'LAUFWERKE'], ['health', 'GESUNDHEIT'], ['snapshots', 'SNAPSHOTS']]; const ROLE_TXT = { admin: 'Administrator', user: 'Benutzer', share: 'Nur Freigaben' }; @@ -819,7 +819,7 @@ const VIEWS = { const sub = S.route.sub; return html` - ${sub === 'dlna' ? dlnaView() : sub === 'autostart' ? autostartView() + ${sub === 'dlna' ? dlnaView() : sub === 'printer' ? printerView() : sub === 'autostart' ? autostartView() : html`
${secHead('services', 'DIENSTE', 'Start, Stopp, Neustart')}${serviceCards(true, isTable('services'))}
`}`; }, @@ -1594,6 +1594,65 @@ function dlnaAfter() { }); } +/* ---------- printers (Services -> Printers, system app "printer") ------------------- */ + +async function loadPrinter() { + clearTimeout(S.prnTimer); + try { S.prn = await rpc('shareos.printer', 'status'); } catch (e) { toast('Drucker: ' + e.message, true); } + if (S.route.tab === 'services' && S.route.sub === 'printer') { + if (!document.activeElement?.matches('input,select')) renderMain(); + // printers come and go: look again now and then + S.prnTimer = setTimeout(loadPrinter, 10000); + } +} + +function printerView() { + const d = S.prn; + if (!d) return html`
Lade Drucker …
`; + if (!d.installed) return html`
${svg(I.info)}
+ Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner. +
`; + const host = location.hostname; + const card = (p) => html`
+
${(p.maker + ' ' + p.model).trim() || p.id}${p.port ? html`im Netzwerk` : d.enabled ? html`nicht erreichbar` : html`aus`}
+
+
Art
${p.via === 'ipp-usb' ? 'Ohne Treiber (IPP over USB)' : 'Mit Treiber am Rechner (Port 9100)'}
+ ${p.via === 'ipp-usb' ? html`
Scannen
${p.port ? 'über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat' : '–'}
` : ''} +
Adresse
${p.port ? (p.via === 'ipp-usb' ? `ipp://${host}:${p.port}/ipp/print` : `${host}:${p.port}`) : '–'}
+ ${p.port && p.via === 'ipp-usb' ? html`
Geräteseite
http://${host}:${p.port}/
` : ''} +
USB
${p.id}${p.lp ? ' · ' + p.lp : ''}
+
+ ${p.port && p.via === 'ipp-usb' ? html`

Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.

` + : p.port ? html`

Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ → ${host}, Port ${p.port}, Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.

+
` : ''} +
`; + return html`
+ ${d.printers.length ? html`
${d.printers.map(card)}
` + : html`
${svg(I.info)}Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.
`} +
+
+
EINSTELLUNGEN${d.enabled ? html`an` : html`aus`}
+ +
+

Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.

+
+
`; +} + +function printerAfter() { + if (!S.prn) { loadPrinter(); return; } + if (!S.prnTimer) S.prnTimer = setTimeout(loadPrinter, 10000); + $('#f-prn')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const f = e.target, b = $('button[type=submit]', f); b.disabled = true; + try { S.prn = await rpc('shareos.printer', 'set', { enabled: f.enabled.checked, mode: f.mode.value }); toast('Drucker gespeichert.'); } + catch (err) { toast('Drucker: ' + err.message, true); } + loadPrinter(); + }); +} + function snapForm() { const f = $('#f-snap'); const keep = {}; @@ -3159,6 +3218,7 @@ const VIEWS_AFTER = { }, services() { if (S.route.sub === 'dlna') { dlnaAfter(); return; } + if (S.route.sub === 'printer') { printerAfter(); return; } if (!S.services) loadServices(); document.querySelectorAll('.as-toggle').forEach((c) => c.addEventListener('change', async () => { c.disabled = true; @@ -4015,6 +4075,11 @@ const ACTIONS = { }, + async 'prn-test'(el) { + try { await rpc('shareos.printer', 'test', { lp: el.dataset.id }); toast('Testseite geschickt.'); } + catch (e) { toast('Testseite: ' + e.message, true); } + }, + async 'cert-self'() { if (!await confirmDlg('Neues selbstsigniertes Zertifikat?', 'Ein neues Zertifikat für den aktuellen Namen und die aktuelle IP wird erstellt und ersetzt das bisherige. Browser zeigen danach einmalig wieder eine Warnung.', 'Erstellen')) return; try { const r = await rpc('shareos.web', 'selfsigned'); S.web.cert = r.cert; toast('Zertifikat erstellt.'); afterWebRestart(); } @@ -4075,6 +4140,7 @@ window.addEventListener('hashchange', () => { if (S.route.tab !== 'storage' || S.route.sub !== 'snapshots') { clearTimeout(S.snapTimer); if (S.snap) { S.snap.edit = null; S.snap.log = null; } } if (S.route.tab !== 'shares') { S.shEdit = null; S.uEdit = null; } if (S.route.tab !== 'services' || S.route.sub !== 'dlna') clearTimeout(S.dlnaTimer); + if (S.route.tab !== 'services' || S.route.sub !== 'printer') { clearTimeout(S.prnTimer); S.prnTimer = null; } if (S.fb && S.route.sub === 'files') S.fb.roots = null; // volumes may have changed if (S.route.tab !== 'paperless') { S.plEdit = null; S.plLog = null; } else if (S.pl) loadPaperless(); if (S.route.tab !== 'cloud') { clearTimeout(S.cloudTimer); if (S.cloud?.scope === 'admin') { S.cloud.remoteEdit = null; S.cloud.jobEdit = null; S.cloud.log = null; } } diff --git a/src/shareos-core/www/i18n/ar.json b/src/shareos-core/www/i18n/ar.json index f79dcb3..4c72d0b 100644 --- a/src/shareos-core/www/i18n/ar.json +++ b/src/shareos-core/www/i18n/ar.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "لم تُضبط أوقات.", "GERÄT & ZUGANG": "الجهاز والوصول", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "النجمة تجعل التطبيق مفضلاً – تظهر المفضلة أعلى «ShareOS الخاص بي».", - "Alle Hinweise anzeigen": "عرض كل التنبيهات" + "Alle Hinweise anzeigen": "عرض كل التنبيهات", + "DRUCKER": "الطابعات", + "Lade Drucker …": "جارٍ تحميل الطابعات …", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "الطابعات في الشبكة تطبيق نظام: تطبع الطابعات والأجهزة متعددة الوظائف الحديثة وتمسح ضوئيًا دون برامج تشغيل (AirPrint، Mopria، Windows 10/11)، والقديمة عبر المنفذ 9100 مع برنامج التشغيل على الحاسوب.", + "Im Store installieren (ipp-usb + p910nd)": "التثبيت من المتجر (ipp-usb + p910nd)", + "im Netzwerk": "في الشبكة", + "nicht erreichbar": "غير متاح", + "Ohne Treiber (IPP over USB)": "دون برنامج تشغيل (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "مع برنامج التشغيل على الحاسوب (المنفذ 9100)", + "Scannen": "المسح الضوئي", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "عبر الشبكة (eSCL) إذا كان للجهاز ماسح ضوئي", + "Adresse": "العنوان", + "Geräteseite": "صفحة الجهاز", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "يعثر Windows 10/11 وmacOS وiPhone/iPad وAndroid على الطابعة تلقائيًا («إضافة طابعة»). يدويًا: أدخل العنوان أعلاه كطابعة IPP.", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows: «إضافة طابعة» ← «عنوان TCP/IP» ←", + ", Port": "، المنفذ", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": "، برنامج تشغيل الشركة المصنّعة. macOS/Linux: «HP JetDirect – Socket» أو AppSocket.", + "Testseite": "صفحة اختبار", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "لا توجد طابعة USB متصلة. صِلها – تظهر هنا بعد ثوانٍ.", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "مشاركة الطابعات في الشبكة (تبدأ أيضًا بعد إعادة التشغيل)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "تلقائي – دون برنامج تشغيل حيث تستطيع الطابعة", + "Immer Port 9100 (Treiber am Rechner)": "دائمًا المنفذ 9100 (برنامج التشغيل على الحاسوب)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "يستطيع أي شخص في الشبكة الطباعة دون تسجيل دخول. اختر «دائمًا المنفذ 9100» فقط إذا لم تطبع الطابعة جيدًا دون برنامج تشغيل.", + "Drucker gespeichert.": "تم حفظ الطابعات.", + "Testseite geschickt.": "تم إرسال صفحة الاختبار." }, "patterns": [ [ diff --git a/src/shareos-core/www/i18n/bn.json b/src/shareos-core/www/i18n/bn.json index b0c402a..059ace5 100644 --- a/src/shareos-core/www/i18n/bn.json +++ b/src/shareos-core/www/i18n/bn.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "কোনো সময় সেট করা নেই।", "GERÄT & ZUGANG": "ডিভাইস ও অ্যাক্সেস", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "তারকা দিয়ে অ্যাপ প্রিয় হয় – প্রিয়গুলি \"আমার ShareOS\"-এর উপরে দেখায়।", - "Alle Hinweise anzeigen": "সব বিজ্ঞপ্তি দেখান" + "Alle Hinweise anzeigen": "সব বিজ্ঞপ্তি দেখান", + "DRUCKER": "প্রিন্টার", + "Lade Drucker …": "প্রিন্টার লোড হচ্ছে …", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "নেটওয়ার্কে প্রিন্টার একটি সিস্টেম অ্যাপ: নতুন প্রিন্টার ও মাল্টিফাংশন ডিভাইস ড্রাইভার ছাড়াই প্রিন্ট ও স্ক্যান করে (AirPrint, Mopria, Windows 10/11), পুরোনোগুলো পোর্ট 9100 দিয়ে, ড্রাইভার কম্পিউটারে।", + "Im Store installieren (ipp-usb + p910nd)": "স্টোর থেকে ইনস্টল করুন (ipp-usb + p910nd)", + "im Netzwerk": "নেটওয়ার্কে", + "nicht erreichbar": "পৌঁছানো যাচ্ছে না", + "Ohne Treiber (IPP over USB)": "ড্রাইভার ছাড়া (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "কম্পিউটারে ড্রাইভারসহ (পোর্ট 9100)", + "Scannen": "স্ক্যান", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "নেটওয়ার্কের মাধ্যমে (eSCL), যদি ডিভাইসে স্ক্যানার থাকে", + "Adresse": "ঠিকানা", + "Geräteseite": "ডিভাইসের পেজ", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "Windows 10/11, macOS, iPhone/iPad ও Android প্রিন্টার নিজেই খুঁজে পায় (“প্রিন্টার যোগ করুন”)। নিজে করতে: উপরের ঠিকানা IPP প্রিন্টার হিসেবে দিন।", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows: “প্রিন্টার যোগ করুন” → “TCP/IP ঠিকানা” →", + ", Port": ", পোর্ট", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": ", নির্মাতার ড্রাইভার। macOS/Linux: “HP JetDirect – Socket” বা AppSocket।", + "Testseite": "টেস্ট পেজ", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "কোনো USB প্রিন্টার যুক্ত নেই। লাগান – কয়েক সেকেন্ডে এখানে দেখা যাবে।", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "প্রিন্টার নেটওয়ার্কে শেয়ার করুন (রিস্টার্টের পরেও চালু হবে)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "স্বয়ংক্রিয় – প্রিন্টার পারলে ড্রাইভার ছাড়া", + "Immer Port 9100 (Treiber am Rechner)": "সবসময় পোর্ট 9100 (কম্পিউটারে ড্রাইভার)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "নেটওয়ার্কের যে কেউ সাইন-ইন ছাড়াই প্রিন্ট করতে পারে। “সবসময় পোর্ট 9100” কেবল তখনই, যখন প্রিন্টার ড্রাইভার ছাড়া ঠিকমতো প্রিন্ট করে না।", + "Drucker gespeichert.": "প্রিন্টার সংরক্ষিত।", + "Testseite geschickt.": "টেস্ট পেজ পাঠানো হয়েছে।" }, "patterns": [ [ diff --git a/src/shareos-core/www/i18n/en.json b/src/shareos-core/www/i18n/en.json index 0e9102d..fb2c9a4 100644 --- a/src/shareos-core/www/i18n/en.json +++ b/src/shareos-core/www/i18n/en.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "No times set up.", "GERÄT & ZUGANG": "DEVICE & ACCESS", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "The star makes an app a favourite – favourites are shown at the top of “My ShareOS”.", - "Alle Hinweise anzeigen": "Show all notices" + "Alle Hinweise anzeigen": "Show all notices", + "DRUCKER": "PRINTERS", + "Lade Drucker …": "Loading printers …", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "Printers in the network are a system app: current printers and multifunction devices then print and scan without drivers (AirPrint, Mopria, Windows 10/11), older ones over port 9100 with the driver on the computer.", + "Im Store installieren (ipp-usb + p910nd)": "Install from the store (ipp-usb + p910nd)", + "im Netzwerk": "in the network", + "nicht erreichbar": "not reachable", + "Ohne Treiber (IPP over USB)": "Without driver (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "With the driver on the computer (port 9100)", + "Scannen": "Scanning", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "over the network (eSCL), if the device has a scanner", + "Adresse": "Address", + "Geräteseite": "Device page", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "Windows 10/11, macOS, iPhone/iPad and Android find the printer by themselves (“Add printer”). By hand: enter the address above as an IPP printer.", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows: “Add printer” → “TCP/IP address” →", + ", Port": ", port", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": ", the maker's driver. macOS/Linux: “HP JetDirect – Socket” or AppSocket.", + "Testseite": "Test page", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "No USB printer connected. Plug one in – it shows up here after a few seconds.", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "Share printers in the network (also starts after a restart)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "Automatic – without driver where the printer can", + "Immer Port 9100 (Treiber am Rechner)": "Always port 9100 (driver on the computer)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "Everyone in the network can print, without signing in. “Always port 9100” only if a printer does not print properly without a driver.", + "Drucker gespeichert.": "Printers saved.", + "Testseite geschickt.": "Test page sent." }, "patterns": [ [ diff --git a/src/shareos-core/www/i18n/es.json b/src/shareos-core/www/i18n/es.json index 03f6f19..6983002 100644 --- a/src/shareos-core/www/i18n/es.json +++ b/src/shareos-core/www/i18n/es.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "No hay horas configuradas.", "GERÄT & ZUGANG": "EQUIPO Y ACCESO", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "La estrella convierte una app en favorita: las favoritas aparecen arriba en «Mi ShareOS».", - "Alle Hinweise anzeigen": "Mostrar todos los avisos" + "Alle Hinweise anzeigen": "Mostrar todos los avisos", + "DRUCKER": "IMPRESORAS", + "Lade Drucker …": "Cargando impresoras …", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "Las impresoras en red son una app del sistema: las impresoras y equipos multifunción actuales imprimen y escanean sin controladores (AirPrint, Mopria, Windows 10/11); las antiguas por el puerto 9100 con el controlador en el ordenador.", + "Im Store installieren (ipp-usb + p910nd)": "Instalar desde la tienda (ipp-usb + p910nd)", + "im Netzwerk": "en la red", + "nicht erreichbar": "no accesible", + "Ohne Treiber (IPP over USB)": "Sin controlador (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "Con el controlador en el ordenador (puerto 9100)", + "Scannen": "Escaneo", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "por la red (eSCL), si el equipo tiene escáner", + "Adresse": "Dirección", + "Geräteseite": "Página del equipo", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "Windows 10/11, macOS, iPhone/iPad y Android encuentran la impresora solos («Agregar impresora»). A mano: introduce la dirección de arriba como impresora IPP.", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows: «Agregar impresora» → «Dirección TCP/IP» →", + ", Port": ", puerto", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": ", controlador del fabricante. macOS/Linux: «HP JetDirect – Socket» o AppSocket.", + "Testseite": "Página de prueba", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "No hay ninguna impresora USB conectada. Conéctala – aparece aquí en unos segundos.", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "Compartir impresoras en la red (también arranca tras reiniciar)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "Automático – sin controlador cuando la impresora lo permite", + "Immer Port 9100 (Treiber am Rechner)": "Siempre puerto 9100 (controlador en el ordenador)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "Cualquiera en la red puede imprimir, sin iniciar sesión. «Siempre puerto 9100» solo si una impresora no imprime bien sin controlador.", + "Drucker gespeichert.": "Impresoras guardadas.", + "Testseite geschickt.": "Página de prueba enviada." }, "patterns": [ [ diff --git a/src/shareos-core/www/i18n/fr.json b/src/shareos-core/www/i18n/fr.json index 53480eb..745e837 100644 --- a/src/shareos-core/www/i18n/fr.json +++ b/src/shareos-core/www/i18n/fr.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "Aucune heure définie.", "GERÄT & ZUGANG": "APPAREIL & ACCÈS", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "L’étoile fait d’une app un favori – les favoris apparaissent en haut de « Mon ShareOS ».", - "Alle Hinweise anzeigen": "Afficher toutes les notifications" + "Alle Hinweise anzeigen": "Afficher toutes les notifications", + "DRUCKER": "IMPRIMANTES", + "Lade Drucker …": "Chargement des imprimantes …", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "Les imprimantes en réseau sont une application système : les imprimantes et appareils multifonctions récents impriment et numérisent alors sans pilote (AirPrint, Mopria, Windows 10/11), les plus anciens via le port 9100 avec le pilote sur l'ordinateur.", + "Im Store installieren (ipp-usb + p910nd)": "Installer depuis le store (ipp-usb + p910nd)", + "im Netzwerk": "dans le réseau", + "nicht erreichbar": "injoignable", + "Ohne Treiber (IPP over USB)": "Sans pilote (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "Avec le pilote sur l'ordinateur (port 9100)", + "Scannen": "Numérisation", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "via le réseau (eSCL), si l'appareil a un scanner", + "Adresse": "Adresse", + "Geräteseite": "Page de l'appareil", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "Windows 10/11, macOS, iPhone/iPad et Android trouvent l'imprimante tout seuls (« Ajouter une imprimante »). À la main : saisir l'adresse ci-dessus comme imprimante IPP.", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows : « Ajouter une imprimante » → « Adresse TCP/IP » →", + ", Port": ", port", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": ", pilote du fabricant. macOS/Linux : « HP JetDirect – Socket » ou AppSocket.", + "Testseite": "Page de test", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "Aucune imprimante USB branchée. Branchez-en une – elle apparaît ici après quelques secondes.", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "Partager les imprimantes sur le réseau (démarre aussi après un redémarrage)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "Automatique – sans pilote quand l'imprimante le permet", + "Immer Port 9100 (Treiber am Rechner)": "Toujours le port 9100 (pilote sur l'ordinateur)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "Tout le monde sur le réseau peut imprimer, sans connexion. « Toujours le port 9100 » seulement si une imprimante n'imprime pas correctement sans pilote.", + "Drucker gespeichert.": "Imprimantes enregistrées.", + "Testseite geschickt.": "Page de test envoyée." }, "patterns": [ [ diff --git a/src/shareos-core/www/i18n/hi.json b/src/shareos-core/www/i18n/hi.json index 36ed0fd..51e940c 100644 --- a/src/shareos-core/www/i18n/hi.json +++ b/src/shareos-core/www/i18n/hi.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "कोई समय सेट नहीं है।", "GERÄT & ZUGANG": "डिवाइस और पहुँच", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "स्टार से ऐप पसंदीदा बनता है – पसंदीदा \"मेरा ShareOS\" के ऊपर दिखते हैं।", - "Alle Hinweise anzeigen": "सभी सूचनाएँ दिखाएँ" + "Alle Hinweise anzeigen": "सभी सूचनाएँ दिखाएँ", + "DRUCKER": "प्रिंटर", + "Lade Drucker …": "प्रिंटर लोड हो रहे हैं …", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "नेटवर्क में प्रिंटर एक सिस्टम ऐप हैं: नए प्रिंटर और मल्टीफ़ंक्शन डिवाइस बिना ड्राइवर के प्रिंट और स्कैन करते हैं (AirPrint, Mopria, Windows 10/11), पुराने पोर्ट 9100 से, ड्राइवर कंप्यूटर पर।", + "Im Store installieren (ipp-usb + p910nd)": "स्टोर से इंस्टॉल करें (ipp-usb + p910nd)", + "im Netzwerk": "नेटवर्क में", + "nicht erreichbar": "पहुँच से बाहर", + "Ohne Treiber (IPP over USB)": "बिना ड्राइवर (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "कंप्यूटर पर ड्राइवर के साथ (पोर्ट 9100)", + "Scannen": "स्कैनिंग", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "नेटवर्क से (eSCL), यदि डिवाइस में स्कैनर है", + "Adresse": "पता", + "Geräteseite": "डिवाइस पेज", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "Windows 10/11, macOS, iPhone/iPad और Android प्रिंटर खुद ढूँढ लेते हैं (“प्रिंटर जोड़ें”)। हाथ से: ऊपर का पता IPP प्रिंटर के रूप में डालें।", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows: “प्रिंटर जोड़ें” → “TCP/IP पता” →", + ", Port": ", पोर्ट", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": ", निर्माता का ड्राइवर। macOS/Linux: “HP JetDirect – Socket” या AppSocket।", + "Testseite": "टेस्ट पेज", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "कोई USB प्रिंटर जुड़ा नहीं है। लगाइए – कुछ सेकंड में यहाँ दिखेगा।", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "प्रिंटर नेटवर्क में साझा करें (रीस्टार्ट के बाद भी शुरू होगा)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "स्वचालित – जहाँ प्रिंटर सपोर्ट करे, बिना ड्राइवर", + "Immer Port 9100 (Treiber am Rechner)": "हमेशा पोर्ट 9100 (कंप्यूटर पर ड्राइवर)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "नेटवर्क में कोई भी बिना साइन-इन प्रिंट कर सकता है। “हमेशा पोर्ट 9100” तभी चुनें जब प्रिंटर बिना ड्राइवर ठीक से प्रिंट न करे।", + "Drucker gespeichert.": "प्रिंटर सहेजे गए।", + "Testseite geschickt.": "टेस्ट पेज भेजा गया।" }, "patterns": [ [ diff --git a/src/shareos-core/www/i18n/pt.json b/src/shareos-core/www/i18n/pt.json index 149669b..c33c893 100644 --- a/src/shareos-core/www/i18n/pt.json +++ b/src/shareos-core/www/i18n/pt.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "Nenhum horário configurado.", "GERÄT & ZUGANG": "EQUIPAMENTO E ACESSO", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "A estrela torna um app favorito – os favoritos aparecem no topo de “Meu ShareOS”.", - "Alle Hinweise anzeigen": "Mostrar todos os avisos" + "Alle Hinweise anzeigen": "Mostrar todos os avisos", + "DRUCKER": "IMPRESSORAS", + "Lade Drucker …": "A carregar impressoras …", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "As impressoras em rede são uma app de sistema: impressoras e multifunções atuais imprimem e digitalizam sem controladores (AirPrint, Mopria, Windows 10/11); as mais antigas pela porta 9100 com o controlador no computador.", + "Im Store installieren (ipp-usb + p910nd)": "Instalar a partir da loja (ipp-usb + p910nd)", + "im Netzwerk": "na rede", + "nicht erreichbar": "inacessível", + "Ohne Treiber (IPP over USB)": "Sem controlador (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "Com o controlador no computador (porta 9100)", + "Scannen": "Digitalização", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "pela rede (eSCL), se o dispositivo tiver scanner", + "Adresse": "Endereço", + "Geräteseite": "Página do dispositivo", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "Windows 10/11, macOS, iPhone/iPad e Android encontram a impressora sozinhos («Adicionar impressora»). À mão: introduza o endereço acima como impressora IPP.", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows: «Adicionar impressora» → «Endereço TCP/IP» →", + ", Port": ", porta", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": ", controlador do fabricante. macOS/Linux: «HP JetDirect – Socket» ou AppSocket.", + "Testseite": "Página de teste", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "Nenhuma impressora USB ligada. Ligue uma – aparece aqui após alguns segundos.", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "Partilhar impressoras na rede (também arranca após reiniciar)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "Automático – sem controlador quando a impressora o permite", + "Immer Port 9100 (Treiber am Rechner)": "Sempre porta 9100 (controlador no computador)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "Qualquer pessoa na rede pode imprimir, sem iniciar sessão. «Sempre porta 9100» só se uma impressora não imprimir bem sem controlador.", + "Drucker gespeichert.": "Impressoras guardadas.", + "Testseite geschickt.": "Página de teste enviada." }, "patterns": [ [ diff --git a/src/shareos-core/www/i18n/ru.json b/src/shareos-core/www/i18n/ru.json index 7e776ee..5cb93f2 100644 --- a/src/shareos-core/www/i18n/ru.json +++ b/src/shareos-core/www/i18n/ru.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "Время не задано.", "GERÄT & ZUGANG": "УСТРОЙСТВО И ДОСТУП", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "Звёздочка делает приложение избранным – избранные показаны вверху «Мой ShareOS».", - "Alle Hinweise anzeigen": "Показать все уведомления" + "Alle Hinweise anzeigen": "Показать все уведомления", + "DRUCKER": "ПРИНТЕРЫ", + "Lade Drucker …": "Загрузка принтеров …", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "Сетевые принтеры – это системное приложение: современные принтеры и МФУ печатают и сканируют без драйверов (AirPrint, Mopria, Windows 10/11), старые – через порт 9100 с драйвером на компьютере.", + "Im Store installieren (ipp-usb + p910nd)": "Установить из магазина (ipp-usb + p910nd)", + "im Netzwerk": "в сети", + "nicht erreichbar": "недоступен", + "Ohne Treiber (IPP over USB)": "Без драйвера (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "С драйвером на компьютере (порт 9100)", + "Scannen": "Сканирование", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "по сети (eSCL), если у устройства есть сканер", + "Adresse": "Адрес", + "Geräteseite": "Страница устройства", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "Windows 10/11, macOS, iPhone/iPad и Android находят принтер сами («Добавить принтер»). Вручную: укажите адрес выше как IPP-принтер.", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows: «Добавить принтер» → «TCP/IP-адрес» →", + ", Port": ", порт", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": ", драйвер производителя. macOS/Linux: «HP JetDirect – Socket» или AppSocket.", + "Testseite": "Тестовая страница", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "USB-принтер не подключён. Подключите – он появится здесь через несколько секунд.", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "Открыть доступ к принтерам по сети (запускается и после перезагрузки)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "Автоматически – без драйвера, где принтер это умеет", + "Immer Port 9100 (Treiber am Rechner)": "Всегда порт 9100 (драйвер на компьютере)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "Печатать может любой в сети, без входа. «Всегда порт 9100» – только если принтер без драйвера печатает неправильно.", + "Drucker gespeichert.": "Принтеры сохранены.", + "Testseite geschickt.": "Тестовая страница отправлена." }, "patterns": [ [ diff --git a/src/shareos-core/www/i18n/zh.json b/src/shareos-core/www/i18n/zh.json index 7d0e039..d8bf785 100644 --- a/src/shareos-core/www/i18n/zh.json +++ b/src/shareos-core/www/i18n/zh.json @@ -1274,7 +1274,31 @@ "Keine Zeiten eingerichtet.": "未设置时间。", "GERÄT & ZUGANG": "设备与访问", "Mit dem Stern wird eine App zum Favoriten – Favoriten stehen oben auf „Mein ShareOS“.": "星标可将应用设为收藏——收藏显示在“我的 ShareOS”顶部。", - "Alle Hinweise anzeigen": "显示所有提示" + "Alle Hinweise anzeigen": "显示所有提示", + "DRUCKER": "打印机", + "Lade Drucker …": "正在加载打印机……", + "Drucker im Netzwerk sind eine System-App: Aktuelle Drucker und Multifunktionsgeräte drucken und scannen dann ohne Treiber (AirPrint, Mopria, Windows 10/11), ältere über Port 9100 mit dem Treiber am Rechner.": "网络打印是一个系统应用:较新的打印机和多功能一体机可免驱动打印和扫描(AirPrint、Mopria、Windows 10/11),较旧的通过 9100 端口打印,驱动装在电脑上。", + "Im Store installieren (ipp-usb + p910nd)": "从商店安装 (ipp-usb + p910nd)", + "im Netzwerk": "已共享到网络", + "nicht erreichbar": "无法访问", + "Ohne Treiber (IPP over USB)": "免驱动 (IPP over USB)", + "Mit Treiber am Rechner (Port 9100)": "驱动装在电脑上(9100 端口)", + "Scannen": "扫描", + "über das Netzwerk (eSCL), wenn das Gerät einen Scanner hat": "通过网络 (eSCL),如果设备带扫描仪", + "Adresse": "地址", + "Geräteseite": "设备页面", + "Windows 10/11, macOS, iPhone/iPad und Android finden den Drucker von selbst („Drucker hinzufügen“). Von Hand: Adresse oben als IPP-Drucker eintragen.": "Windows 10/11、macOS、iPhone/iPad 和 Android 会自动找到打印机(“添加打印机”)。手动添加:将上面的地址作为 IPP 打印机填入。", + "Windows: „Drucker hinzufügen“ → „TCP/IP-Adresse“ →": "Windows:“添加打印机” → “TCP/IP 地址” →", + ", Port": ",端口", + ", Treiber des Herstellers. macOS/Linux: „HP JetDirect – Socket“ bzw. AppSocket.": ",使用厂商驱动。macOS/Linux:“HP JetDirect – Socket”或 AppSocket。", + "Testseite": "测试页", + "Kein USB-Drucker angeschlossen. Einstecken – er erscheint hier nach ein paar Sekunden.": "未连接 USB 打印机。插上后几秒钟即会在此显示。", + "Drucker im Netzwerk freigeben (startet auch nach einem Neustart)": "在网络中共享打印机(重启后也会启动)", + "Automatisch – ohne Treiber, wo der Drucker es kann": "自动——打印机支持时免驱动", + "Immer Port 9100 (Treiber am Rechner)": "始终使用 9100 端口(驱动装在电脑上)", + "Jeder im Netzwerk kann drucken, ohne Anmeldung. „Immer Port 9100“ nur, wenn ein Drucker ohne Treiber nicht richtig druckt.": "网络中的任何人都可以打印,无需登录。仅当打印机免驱动打印不正常时才选“始终使用 9100 端口”。", + "Drucker gespeichert.": "打印机设置已保存。", + "Testseite geschickt.": "测试页已发送。" }, "patterns": [ [ diff --git a/sysapps/printer/app.json b/sysapps/printer/app.json new file mode 100644 index 0000000..0439929 --- /dev/null +++ b/sysapps/printer/app.json @@ -0,0 +1,18 @@ +{ + "id": "printer", + "name": "ipp-usb + p910nd", + "version": "0.9.34", + "publisher": "UniqueDroid", + "description": { + "de": "USB-Drucker im Netzwerk: Aktuelle Drucker und Multifunktionsgeräte mit IPP over USB (die meisten seit etwa 2015) drucken und scannen ohne Treiber – AirPrint, Mopria, IPP Everywhere, Windows 10/11 findet sie von selbst (ipp-usb). Ältere Drucker über Port 9100 mit dem Treiber am Rechner (p910nd). Einstellungen unter Dienste → Drucker.", + "en": "USB printers in the network: current printers and multifunction devices with IPP over USB (most since about 2015) print and scan without drivers – AirPrint, Mopria, IPP Everywhere, Windows 10/11 finds them by itself (ipp-usb). Older printers over port 9100 with the driver on the computer (p910nd). Settings under Services → Printers." + }, + "type": "system", + "commands": [ "ipp-usb", "p910nd" ], + "service": { "control": "printer", "delay": 5 }, + "icon": "icon.svg", + "category": "system", + "license": "BSD-2-Clause, GPL-2.0-only", + "homepage": "https://github.com/OpenPrinting/ipp-usb", + "min_shareos": "0.2.0-beta.18" +} diff --git a/sysapps/printer/icon.svg b/sysapps/printer/icon.svg new file mode 100644 index 0000000..7b08ca9 --- /dev/null +++ b/sysapps/printer/icon.svg @@ -0,0 +1 @@ + diff --git a/sysapps/printer/pack.json b/sysapps/printer/pack.json new file mode 100644 index 0000000..b1f2246 --- /dev/null +++ b/sysapps/printer/pack.json @@ -0,0 +1 @@ +{ "bin": [ "usr/bin/ipp-usb", "usr/sbin/p910nd" ], "share": [ "usr/share/ipp-usb" ] } diff --git a/tests/mock-ubus.py b/tests/mock-ubus.py index 12f46a4..879afa1 100644 --- a/tests/mock-ubus.py +++ b/tests/mock-ubus.py @@ -699,6 +699,18 @@ def call(obj, method, args): STATE['led'].update({k: v for k, v in args.items() if v}) return {'fan': STATE['fan'], 'led': STATE['led'], 'temperature': 41000, 'fan_pwm': 100, 'presets': {'quiet': {'off_temp': 40, 'full_temp': 55, 'half_temp': 50}}} + if obj == 'shareos.printer': + pr = STATE.setdefault('printer', {'installed': True, 'enabled': False, 'mode': 'auto', 'ippusb': False, 'hostname': 'skynet-nas', 'printers': [ + {'id': '03f0:5c11', 'ipp': True, 'lp': 'lp0', 'via': 'ipp-usb', 'port': None, 'maker': 'HP', 'model': 'Smart Tank 5100 series'}, + {'id': '04a9:2676', 'ipp': False, 'lp': 'lp1', 'via': 'p910nd', 'port': None, 'maker': 'Canon', 'model': 'LBP2900'}]}) + if method == 'set': + pr['enabled'] = bool(args.get('enabled')); pr['mode'] = args.get('mode') or pr['mode'] + for x in pr['printers']: + x['via'] = 'ipp-usb' if x['ipp'] and pr['mode'] == 'auto' else 'p910nd' + x['port'] = (60000 if x['via'] == 'ipp-usb' else 9100 + int(x['lp'][2:])) if pr['enabled'] else None + if method == 'test': + return {'ok': True} + return pr if obj == 'shareos.web': if method == 'set': STATE.update({k: v for k, v in args.items() if k in ('https', 'redirect')}) diff --git a/tests/test-printer.sh b/tests/test-printer.sh new file mode 100755 index 0000000..a640013 --- /dev/null +++ b/tests/test-printer.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# +# Tests shareos-printer (system app "printer") with a fake /sys and fake +# ipp-usb / p910nd: which printer gets which way, the avahi service file of a +# port-9100 printer, mode raw, unplugging, switching off, the status lines. +# +# tests/test-printer.sh [dir with uci] +# +TOP=$(cd "$(dirname "$0")/.." && pwd) +P="$TOP/src/shareos-core/usr/sbin/shareos-printer" +T=$(mktemp -d) +cleanup() { SHAREOS_ROOT=$T sh "$P" stop-all >/dev/null 2>&1; rm -rf "$T"; } +trap cleanup EXIT +[ -n "$DEBUG" ] && trap "" EXIT +export PATH="${1:+$1:}$PATH" +mkdir -p $T/etc/config $T/sys/bus/usb/devices $T/dev/usb $T/bin +# fake programs: stay running, log their arguments +for p in ipp-usb p910nd; do + printf '#!/bin/sh\necho "$0 $*" >> %s/calls.log\n[ "$1" = status ] && { echo " Num Device Vndr:Prod Port Model"; echo " 1. Bus 001 Device 003 03f0:5c11 60000 \\"HP Smart Tank 5100 series\\""; exit 0; }\nwhile :; do sleep 1; done\n' $T > $T/bin/$p + chmod 755 $T/bin/$p +done +export SHAREOS_ROOT=$T SHAREOS_PRINTER_SYS=$T/sys SHAREOS_PRINTER_IPPUSB=$T/bin/ipp-usb SHAREOS_PRINTER_P910ND=$T/bin/p910nd +run() { sh "$P" "$@"; } +pass=0; failed=0 +check() { if eval "$2"; then echo "PASS $1"; pass=$((pass+1)); else echo "FAIL $1"; failed=$((failed+1)); fi; } +conf() { printf "config printer 'main'\n" > $T/etc/config/printer; for kv; do printf "\toption %s '%s'\n" "${kv%%=*}" "${kv#*=}" >> $T/etc/config/printer; done; } +# usb device (lp node on the 7/1/2 one) +dev() { + d=$T/sys/bus/usb/devices/$1; mkdir -p $d + echo $2 > $d/idVendor; echo $3 > $d/idProduct; echo "$4" > $d/manufacturer; echo "$5" > $d/product + n=0; shift 5 + for proto; do + i=$d:1.$n; mkdir -p $i; echo 07 > $i/bInterfaceClass; echo $proto > $i/bInterfaceProtocol + [ $proto = 02 ] && { mkdir -p $i/usbmisc/lp$LP; touch $T/dev/usb/lp$LP; } + n=$((n + 1)) + done +} +running() { [ -n "$(cat $T/tmp/shareos-printer/$1.pid 2>/dev/null)" ] && kill -0 "$(cat $T/tmp/shareos-printer/$1.pid)" 2>/dev/null; } + +conf enabled=1 +LP=0 dev 1-1 03f0 5c11 HP "Smart Tank 5100 series" 02 04 04 +LP=1 dev 1-2 04a9 1234 Canon "LBP2900" 02 +run apply +check "IPP-over-USB printer: ipp-usb runs" "running ipp-usb" +check "ipp-usb: network config, ports from 60000" "grep -q 'interface = all' $T/tmp/shareos-printer/conf/ipp-usb.conf && grep -q 'http-min-port = 60000' $T/tmp/shareos-printer/conf/ipp-usb.conf" +check "ipp-usb: port state kept on the flash" "grep -q -- '-path-dev-state-dir $T/etc/shareos/printer/dev' $T/calls.log" +check "the IPP printer gets no p910nd" "! running p9100" +check "older printer: p910nd on lp1 (port 9101)" "running p9101 && grep -q 'p910nd -f $T/dev/usb/lp1 -b -d 1' $T/calls.log" +check "older printer: announced as _pdl-datastream on 9101" "grep -q '9101' $T/etc/avahi/services/shareos-printer-lp1.service && grep -q 'Canon LBP2900' $T/etc/avahi/services/shareos-printer-lp1.service" +check "status: both printers with their way and port" "run status | grep -qx 'printer=03f0:5c11|1|lp0|ipp-usb|60000|HP | Smart Tank 5100 series' && run status | grep -qx 'printer=04a9:1234|0|lp1|p910nd|9101|Canon | LBP2900'" +p=$(cat $T/tmp/shareos-printer/p9101.pid) +run apply +check "apply again: nothing restarted" "[ \"\$(cat $T/tmp/shareos-printer/p9101.pid)\" = $p ]" +rm -rf $T/sys/bus/usb/devices/1-2 $T/sys/bus/usb/devices/1-2:* +run apply +check "unplugged: its p910nd stops, its service file goes" "! running p9101 && [ ! -e $T/etc/avahi/services/shareos-printer-lp1.service ]" +conf enabled=1 mode=raw +run apply +check "mode raw: ipp-usb off, p910nd also for the IPP printer" "! running ipp-usb && running p9100" +conf enabled=0 +run apply +check "switched off: nothing runs" "! running ipp-usb && ! running p9100 && ! ls $T/etc/avahi/services/ | grep -q shareos-printer" +check "status: switched off, no ports" "run status | grep -qx 'enabled=0' && run status | grep -qx 'printer=03f0:5c11|1|lp0|ipp-usb||HP | Smart Tank 5100 series'" +check "boot while switched off: nothing" "run boot && ! running ipp-usb" +check "test page: refuses an unknown printer" "run test lp7 | grep -q 'no such printer'" + +echo "test-printer: $pass passed, $failed failed" +[ $failed = 0 ] diff --git a/tools/i18n/ar.tsv b/tools/i18n/ar.tsv index 50a39b7..17b5113 100644 --- a/tools/i18n/ar.tsv +++ b/tools/i18n/ar.tsv @@ -1636,3 +1636,27 @@ P323 إيقاف $1 E1269 الجهاز والوصول E1270 النجمة تجعل التطبيق مفضلاً – تظهر المفضلة أعلى «ShareOS الخاص بي». E1271 عرض كل التنبيهات +E1272 الطابعات +E1273 جارٍ تحميل الطابعات … +E1274 الطابعات في الشبكة تطبيق نظام: تطبع الطابعات والأجهزة متعددة الوظائف الحديثة وتمسح ضوئيًا دون برامج تشغيل (AirPrint، Mopria، Windows 10/11)، والقديمة عبر المنفذ 9100 مع برنامج التشغيل على الحاسوب. +E1275 التثبيت من المتجر (ipp-usb + p910nd) +E1276 في الشبكة +E1277 غير متاح +E1278 دون برنامج تشغيل (IPP over USB) +E1279 مع برنامج التشغيل على الحاسوب (المنفذ 9100) +E1280 المسح الضوئي +E1281 عبر الشبكة (eSCL) إذا كان للجهاز ماسح ضوئي +E1282 العنوان +E1283 صفحة الجهاز +E1284 يعثر Windows 10/11 وmacOS وiPhone/iPad وAndroid على الطابعة تلقائيًا («إضافة طابعة»). يدويًا: أدخل العنوان أعلاه كطابعة IPP. +E1285 Windows: «إضافة طابعة» ← «عنوان TCP/IP» ← +E1286 ، المنفذ +E1287 ، برنامج تشغيل الشركة المصنّعة. macOS/Linux: «HP JetDirect – Socket» أو AppSocket. +E1288 صفحة اختبار +E1289 لا توجد طابعة USB متصلة. صِلها – تظهر هنا بعد ثوانٍ. +E1290 مشاركة الطابعات في الشبكة (تبدأ أيضًا بعد إعادة التشغيل) +E1291 تلقائي – دون برنامج تشغيل حيث تستطيع الطابعة +E1292 دائمًا المنفذ 9100 (برنامج التشغيل على الحاسوب) +E1293 يستطيع أي شخص في الشبكة الطباعة دون تسجيل دخول. اختر «دائمًا المنفذ 9100» فقط إذا لم تطبع الطابعة جيدًا دون برنامج تشغيل. +E1294 تم حفظ الطابعات. +E1295 تم إرسال صفحة الاختبار. diff --git a/tools/i18n/bn.tsv b/tools/i18n/bn.tsv index a00225d..236039e 100644 --- a/tools/i18n/bn.tsv +++ b/tools/i18n/bn.tsv @@ -1636,3 +1636,27 @@ P323 বন্ধ $1 E1269 ডিভাইস ও অ্যাক্সেস E1270 তারকা দিয়ে অ্যাপ প্রিয় হয় – প্রিয়গুলি "আমার ShareOS"-এর উপরে দেখায়। E1271 সব বিজ্ঞপ্তি দেখান +E1272 প্রিন্টার +E1273 প্রিন্টার লোড হচ্ছে … +E1274 নেটওয়ার্কে প্রিন্টার একটি সিস্টেম অ্যাপ: নতুন প্রিন্টার ও মাল্টিফাংশন ডিভাইস ড্রাইভার ছাড়াই প্রিন্ট ও স্ক্যান করে (AirPrint, Mopria, Windows 10/11), পুরোনোগুলো পোর্ট 9100 দিয়ে, ড্রাইভার কম্পিউটারে। +E1275 স্টোর থেকে ইনস্টল করুন (ipp-usb + p910nd) +E1276 নেটওয়ার্কে +E1277 পৌঁছানো যাচ্ছে না +E1278 ড্রাইভার ছাড়া (IPP over USB) +E1279 কম্পিউটারে ড্রাইভারসহ (পোর্ট 9100) +E1280 স্ক্যান +E1281 নেটওয়ার্কের মাধ্যমে (eSCL), যদি ডিভাইসে স্ক্যানার থাকে +E1282 ঠিকানা +E1283 ডিভাইসের পেজ +E1284 Windows 10/11, macOS, iPhone/iPad ও Android প্রিন্টার নিজেই খুঁজে পায় (“প্রিন্টার যোগ করুন”)। নিজে করতে: উপরের ঠিকানা IPP প্রিন্টার হিসেবে দিন। +E1285 Windows: “প্রিন্টার যোগ করুন” → “TCP/IP ঠিকানা” → +E1286 , পোর্ট +E1287 , নির্মাতার ড্রাইভার। macOS/Linux: “HP JetDirect – Socket” বা AppSocket। +E1288 টেস্ট পেজ +E1289 কোনো USB প্রিন্টার যুক্ত নেই। লাগান – কয়েক সেকেন্ডে এখানে দেখা যাবে। +E1290 প্রিন্টার নেটওয়ার্কে শেয়ার করুন (রিস্টার্টের পরেও চালু হবে) +E1291 স্বয়ংক্রিয় – প্রিন্টার পারলে ড্রাইভার ছাড়া +E1292 সবসময় পোর্ট 9100 (কম্পিউটারে ড্রাইভার) +E1293 নেটওয়ার্কের যে কেউ সাইন-ইন ছাড়াই প্রিন্ট করতে পারে। “সবসময় পোর্ট 9100” কেবল তখনই, যখন প্রিন্টার ড্রাইভার ছাড়া ঠিকমতো প্রিন্ট করে না। +E1294 প্রিন্টার সংরক্ষিত। +E1295 টেস্ট পেজ পাঠানো হয়েছে। diff --git a/tools/i18n/es.tsv b/tools/i18n/es.tsv index ed0285d..743a94e 100644 --- a/tools/i18n/es.tsv +++ b/tools/i18n/es.tsv @@ -1636,3 +1636,27 @@ P323 apagado $1 E1269 EQUIPO Y ACCESO E1270 La estrella convierte una app en favorita: las favoritas aparecen arriba en «Mi ShareOS». E1271 Mostrar todos los avisos +E1272 IMPRESORAS +E1273 Cargando impresoras … +E1274 Las impresoras en red son una app del sistema: las impresoras y equipos multifunción actuales imprimen y escanean sin controladores (AirPrint, Mopria, Windows 10/11); las antiguas por el puerto 9100 con el controlador en el ordenador. +E1275 Instalar desde la tienda (ipp-usb + p910nd) +E1276 en la red +E1277 no accesible +E1278 Sin controlador (IPP over USB) +E1279 Con el controlador en el ordenador (puerto 9100) +E1280 Escaneo +E1281 por la red (eSCL), si el equipo tiene escáner +E1282 Dirección +E1283 Página del equipo +E1284 Windows 10/11, macOS, iPhone/iPad y Android encuentran la impresora solos («Agregar impresora»). A mano: introduce la dirección de arriba como impresora IPP. +E1285 Windows: «Agregar impresora» → «Dirección TCP/IP» → +E1286 , puerto +E1287 , controlador del fabricante. macOS/Linux: «HP JetDirect – Socket» o AppSocket. +E1288 Página de prueba +E1289 No hay ninguna impresora USB conectada. Conéctala – aparece aquí en unos segundos. +E1290 Compartir impresoras en la red (también arranca tras reiniciar) +E1291 Automático – sin controlador cuando la impresora lo permite +E1292 Siempre puerto 9100 (controlador en el ordenador) +E1293 Cualquiera en la red puede imprimir, sin iniciar sesión. «Siempre puerto 9100» solo si una impresora no imprime bien sin controlador. +E1294 Impresoras guardadas. +E1295 Página de prueba enviada. diff --git a/tools/i18n/fr.tsv b/tools/i18n/fr.tsv index 6b1af10..53e931f 100644 --- a/tools/i18n/fr.tsv +++ b/tools/i18n/fr.tsv @@ -1636,3 +1636,27 @@ P323 arrêt $1 E1269 APPAREIL & ACCÈS E1270 L’étoile fait d’une app un favori – les favoris apparaissent en haut de « Mon ShareOS ». E1271 Afficher toutes les notifications +E1272 IMPRIMANTES +E1273 Chargement des imprimantes … +E1274 Les imprimantes en réseau sont une application système : les imprimantes et appareils multifonctions récents impriment et numérisent alors sans pilote (AirPrint, Mopria, Windows 10/11), les plus anciens via le port 9100 avec le pilote sur l'ordinateur. +E1275 Installer depuis le store (ipp-usb + p910nd) +E1276 dans le réseau +E1277 injoignable +E1278 Sans pilote (IPP over USB) +E1279 Avec le pilote sur l'ordinateur (port 9100) +E1280 Numérisation +E1281 via le réseau (eSCL), si l'appareil a un scanner +E1282 Adresse +E1283 Page de l'appareil +E1284 Windows 10/11, macOS, iPhone/iPad et Android trouvent l'imprimante tout seuls (« Ajouter une imprimante »). À la main : saisir l'adresse ci-dessus comme imprimante IPP. +E1285 Windows : « Ajouter une imprimante » → « Adresse TCP/IP » → +E1286 , port +E1287 , pilote du fabricant. macOS/Linux : « HP JetDirect – Socket » ou AppSocket. +E1288 Page de test +E1289 Aucune imprimante USB branchée. Branchez-en une – elle apparaît ici après quelques secondes. +E1290 Partager les imprimantes sur le réseau (démarre aussi après un redémarrage) +E1291 Automatique – sans pilote quand l'imprimante le permet +E1292 Toujours le port 9100 (pilote sur l'ordinateur) +E1293 Tout le monde sur le réseau peut imprimer, sans connexion. « Toujours le port 9100 » seulement si une imprimante n'imprime pas correctement sans pilote. +E1294 Imprimantes enregistrées. +E1295 Page de test envoyée. diff --git a/tools/i18n/hi.tsv b/tools/i18n/hi.tsv index 1398ad8..1ccc7f0 100644 --- a/tools/i18n/hi.tsv +++ b/tools/i18n/hi.tsv @@ -1636,3 +1636,27 @@ P323 बंद $1 E1269 डिवाइस और पहुँच E1270 स्टार से ऐप पसंदीदा बनता है – पसंदीदा "मेरा ShareOS" के ऊपर दिखते हैं। E1271 सभी सूचनाएँ दिखाएँ +E1272 प्रिंटर +E1273 प्रिंटर लोड हो रहे हैं … +E1274 नेटवर्क में प्रिंटर एक सिस्टम ऐप हैं: नए प्रिंटर और मल्टीफ़ंक्शन डिवाइस बिना ड्राइवर के प्रिंट और स्कैन करते हैं (AirPrint, Mopria, Windows 10/11), पुराने पोर्ट 9100 से, ड्राइवर कंप्यूटर पर। +E1275 स्टोर से इंस्टॉल करें (ipp-usb + p910nd) +E1276 नेटवर्क में +E1277 पहुँच से बाहर +E1278 बिना ड्राइवर (IPP over USB) +E1279 कंप्यूटर पर ड्राइवर के साथ (पोर्ट 9100) +E1280 स्कैनिंग +E1281 नेटवर्क से (eSCL), यदि डिवाइस में स्कैनर है +E1282 पता +E1283 डिवाइस पेज +E1284 Windows 10/11, macOS, iPhone/iPad और Android प्रिंटर खुद ढूँढ लेते हैं (“प्रिंटर जोड़ें”)। हाथ से: ऊपर का पता IPP प्रिंटर के रूप में डालें। +E1285 Windows: “प्रिंटर जोड़ें” → “TCP/IP पता” → +E1286 , पोर्ट +E1287 , निर्माता का ड्राइवर। macOS/Linux: “HP JetDirect – Socket” या AppSocket। +E1288 टेस्ट पेज +E1289 कोई USB प्रिंटर जुड़ा नहीं है। लगाइए – कुछ सेकंड में यहाँ दिखेगा। +E1290 प्रिंटर नेटवर्क में साझा करें (रीस्टार्ट के बाद भी शुरू होगा) +E1291 स्वचालित – जहाँ प्रिंटर सपोर्ट करे, बिना ड्राइवर +E1292 हमेशा पोर्ट 9100 (कंप्यूटर पर ड्राइवर) +E1293 नेटवर्क में कोई भी बिना साइन-इन प्रिंट कर सकता है। “हमेशा पोर्ट 9100” तभी चुनें जब प्रिंटर बिना ड्राइवर ठीक से प्रिंट न करे। +E1294 प्रिंटर सहेजे गए। +E1295 टेस्ट पेज भेजा गया। diff --git a/tools/i18n/pt.tsv b/tools/i18n/pt.tsv index 332a435..68aaccb 100644 --- a/tools/i18n/pt.tsv +++ b/tools/i18n/pt.tsv @@ -1636,3 +1636,27 @@ P323 desliga $1 E1269 EQUIPAMENTO E ACESSO E1270 A estrela torna um app favorito – os favoritos aparecem no topo de “Meu ShareOS”. E1271 Mostrar todos os avisos +E1272 IMPRESSORAS +E1273 A carregar impressoras … +E1274 As impressoras em rede são uma app de sistema: impressoras e multifunções atuais imprimem e digitalizam sem controladores (AirPrint, Mopria, Windows 10/11); as mais antigas pela porta 9100 com o controlador no computador. +E1275 Instalar a partir da loja (ipp-usb + p910nd) +E1276 na rede +E1277 inacessível +E1278 Sem controlador (IPP over USB) +E1279 Com o controlador no computador (porta 9100) +E1280 Digitalização +E1281 pela rede (eSCL), se o dispositivo tiver scanner +E1282 Endereço +E1283 Página do dispositivo +E1284 Windows 10/11, macOS, iPhone/iPad e Android encontram a impressora sozinhos («Adicionar impressora»). À mão: introduza o endereço acima como impressora IPP. +E1285 Windows: «Adicionar impressora» → «Endereço TCP/IP» → +E1286 , porta +E1287 , controlador do fabricante. macOS/Linux: «HP JetDirect – Socket» ou AppSocket. +E1288 Página de teste +E1289 Nenhuma impressora USB ligada. Ligue uma – aparece aqui após alguns segundos. +E1290 Partilhar impressoras na rede (também arranca após reiniciar) +E1291 Automático – sem controlador quando a impressora o permite +E1292 Sempre porta 9100 (controlador no computador) +E1293 Qualquer pessoa na rede pode imprimir, sem iniciar sessão. «Sempre porta 9100» só se uma impressora não imprimir bem sem controlador. +E1294 Impressoras guardadas. +E1295 Página de teste enviada. diff --git a/tools/i18n/ru.tsv b/tools/i18n/ru.tsv index 1db7ab0..7f7b90b 100644 --- a/tools/i18n/ru.tsv +++ b/tools/i18n/ru.tsv @@ -1636,3 +1636,27 @@ P323 выкл. $1 E1269 УСТРОЙСТВО И ДОСТУП E1270 Звёздочка делает приложение избранным – избранные показаны вверху «Мой ShareOS». E1271 Показать все уведомления +E1272 ПРИНТЕРЫ +E1273 Загрузка принтеров … +E1274 Сетевые принтеры – это системное приложение: современные принтеры и МФУ печатают и сканируют без драйверов (AirPrint, Mopria, Windows 10/11), старые – через порт 9100 с драйвером на компьютере. +E1275 Установить из магазина (ipp-usb + p910nd) +E1276 в сети +E1277 недоступен +E1278 Без драйвера (IPP over USB) +E1279 С драйвером на компьютере (порт 9100) +E1280 Сканирование +E1281 по сети (eSCL), если у устройства есть сканер +E1282 Адрес +E1283 Страница устройства +E1284 Windows 10/11, macOS, iPhone/iPad и Android находят принтер сами («Добавить принтер»). Вручную: укажите адрес выше как IPP-принтер. +E1285 Windows: «Добавить принтер» → «TCP/IP-адрес» → +E1286 , порт +E1287 , драйвер производителя. macOS/Linux: «HP JetDirect – Socket» или AppSocket. +E1288 Тестовая страница +E1289 USB-принтер не подключён. Подключите – он появится здесь через несколько секунд. +E1290 Открыть доступ к принтерам по сети (запускается и после перезагрузки) +E1291 Автоматически – без драйвера, где принтер это умеет +E1292 Всегда порт 9100 (драйвер на компьютере) +E1293 Печатать может любой в сети, без входа. «Всегда порт 9100» – только если принтер без драйвера печатает неправильно. +E1294 Принтеры сохранены. +E1295 Тестовая страница отправлена. diff --git a/tools/i18n/zh.tsv b/tools/i18n/zh.tsv index b0768e1..a4c3a27 100644 --- a/tools/i18n/zh.tsv +++ b/tools/i18n/zh.tsv @@ -1636,3 +1636,27 @@ P323 关 $1 E1269 设备与访问 E1270 星标可将应用设为收藏——收藏显示在“我的 ShareOS”顶部。 E1271 显示所有提示 +E1272 打印机 +E1273 正在加载打印机…… +E1274 网络打印是一个系统应用:较新的打印机和多功能一体机可免驱动打印和扫描(AirPrint、Mopria、Windows 10/11),较旧的通过 9100 端口打印,驱动装在电脑上。 +E1275 从商店安装 (ipp-usb + p910nd) +E1276 已共享到网络 +E1277 无法访问 +E1278 免驱动 (IPP over USB) +E1279 驱动装在电脑上(9100 端口) +E1280 扫描 +E1281 通过网络 (eSCL),如果设备带扫描仪 +E1282 地址 +E1283 设备页面 +E1284 Windows 10/11、macOS、iPhone/iPad 和 Android 会自动找到打印机(“添加打印机”)。手动添加:将上面的地址作为 IPP 打印机填入。 +E1285 Windows:“添加打印机” → “TCP/IP 地址” → +E1286 ,端口 +E1287 ,使用厂商驱动。macOS/Linux:“HP JetDirect – Socket”或 AppSocket。 +E1288 测试页 +E1289 未连接 USB 打印机。插上后几秒钟即会在此显示。 +E1290 在网络中共享打印机(重启后也会启动) +E1291 自动——打印机支持时免驱动 +E1292 始终使用 9100 端口(驱动装在电脑上) +E1293 网络中的任何人都可以打印,无需登录。仅当打印机免驱动打印不正常时才选“始终使用 9100 端口”。 +E1294 打印机设置已保存。 +E1295 测试页已发送。