diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c1fd4d..d031867 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: Dynamic DNS (shareos-ddns, all providers with a fake curl) run: LD_LIBRARY_PATH="$HOME/hostroot/lib" tests/test-ddns.sh "$HOME/hostroot/bin" - name: Zabbix agent controller (settings, disk/RAID items) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a48d3..b4583a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ 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). - **DynDNS, own certificate authority and Let's Encrypt** (System → Netzwerk). - *DynDNS*: DuckDNS, dynv6, Cloudflare, No-IP or any dyndns2 address (Strato, IONOS, …), IPv4 and/or IPv6 (for lines without their own IPv4 address: DS-Lite, fibre, mobile). The box reports its public address every 5 minutes when it changed, otherwise once a day; a failure becomes a message. Token and password go to curl through stdin, not the command line, and are never sent back to the browser. `shareos-ddns`, `tests/test-ddns.sh` (24 checks with a fake curl). - *Own CA*: a certificate authority on the box (EC P-256, 10 years) issues certificates for the box and other devices in the home network (825 days, server authentication, wildcards like `*.home.lan`, IP addresses); its root certificate is installed once on PCs and phones, after that there are no certificate warnings. The box's own certificate follows name and IP address and is renewed 30 days before it runs out. The CA is part of the settings backup. `shareos-ca` (mbedtls of the firmware), `tests/test-ca.sh` (18 checks, in CI with the firmware's programs in qemu). diff --git a/CLAUDE.md b/CLAUDE.md index 7944281..5291030 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, test-ddns, test-ca (mbedtls/uci of the firmware through qemu-arm-static, see build.yml), 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, test-ddns, test-ca (mbedtls/uci of the firmware through qemu-arm-static, see build.yml), 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 c8a33c0..1f2d7ce 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 d7a9ca0..62659e9 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-ddns $(TARGET_DIR)/usr/sbin/shareos-ddns $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-ca $(TARGET_DIR)/usr/sbin/shareos-ca $(INSTALL) -D -m 0755 $(@D)/usr/sbin/shareos-acme $(TARGET_DIR)/usr/sbin/shareos-acme 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 6e96d74..4fa4319 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": [ @@ -299,6 +302,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 52ab203..fc7ca3b 100644 --- a/src/shareos-core/usr/share/rpcd/ucode/shareos.uc +++ b/src/shareos-core/usr/share/rpcd/ucode/shareos.uc @@ -849,7 +849,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', '/etc/shareos/ca/ca.crt', '/etc/shareos/ca/ca.key' ]; @@ -1175,6 +1175,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'; @@ -3831,6 +3850,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 f41200f..24b8c4c 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 = {}; @@ -3285,6 +3344,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; @@ -4161,6 +4221,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 'ddns-now'() { try { await rpc('shareos.web', 'ddns_update'); toast('Wird gemeldet …'); setTimeout(async () => { await loadNetExtra(); if (S.route.sub === 'network') renderMain(); }, 5000); } catch (e) { toast('DynDNS: ' + e.message, true); } @@ -4259,6 +4324,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 d046922..bea59a5 100644 --- a/src/shareos-core/www/i18n/ar.json +++ b/src/shareos-core/www/i18n/ar.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "إيقاف Let's Encrypt؟", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "يعود الجهاز بعدها إلى شهادة موقّعة ذاتيًا.", "Abgeschaltet – die Seite verbindet sich neu.": "تم الإيقاف – تعيد الصفحة الاتصال.", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "شهادة Let's Encrypt نشطة – تعيد الصفحة الاتصال." + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "شهادة Let's Encrypt نشطة – تعيد الصفحة الاتصال.", + "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 d9ff109..e7b7a8d 100644 --- a/src/shareos-core/www/i18n/bn.json +++ b/src/shareos-core/www/i18n/bn.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "Let's Encrypt বন্ধ করবেন?", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "এরপর বক্স আবার স্ব-স্বাক্ষরিত সার্টিফিকেট ব্যবহার করবে।", "Abgeschaltet – die Seite verbindet sich neu.": "বন্ধ করা হয়েছে – পেজ আবার সংযুক্ত হচ্ছে।", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Let's Encrypt সার্টিফিকেট সক্রিয় – পেজ আবার সংযুক্ত হচ্ছে।" + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Let's Encrypt সার্টিফিকেট সক্রিয় – পেজ আবার সংযুক্ত হচ্ছে।", + "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 b020b3b..d1db9e1 100644 --- a/src/shareos-core/www/i18n/en.json +++ b/src/shareos-core/www/i18n/en.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "Switch off Let's Encrypt?", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "The box then uses a self-signed certificate again.", "Abgeschaltet – die Seite verbindet sich neu.": "Switched off – the page reconnects.", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Let's Encrypt certificate active – the page reconnects." + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Let's Encrypt certificate active – the page reconnects.", + "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 9062306..10be64f 100644 --- a/src/shareos-core/www/i18n/es.json +++ b/src/shareos-core/www/i18n/es.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "¿Desactivar Let's Encrypt?", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "La caja vuelve a usar un certificado autofirmado.", "Abgeschaltet – die Seite verbindet sich neu.": "Desactivado – la página se reconecta.", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Certificado de Let's Encrypt activo – la página se reconecta." + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Certificado de Let's Encrypt activo – la página se reconecta.", + "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 049743f..22bf0cf 100644 --- a/src/shareos-core/www/i18n/fr.json +++ b/src/shareos-core/www/i18n/fr.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "Désactiver Let's Encrypt ?", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "La box utilise ensuite de nouveau un certificat auto-signé.", "Abgeschaltet – die Seite verbindet sich neu.": "Désactivé – la page se reconnecte.", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Certificat Let's Encrypt actif – la page se reconnecte." + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Certificat Let's Encrypt actif – la page se reconnecte.", + "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 c286811..457e61b 100644 --- a/src/shareos-core/www/i18n/hi.json +++ b/src/shareos-core/www/i18n/hi.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "Let's Encrypt बंद करें?", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "इसके बाद बॉक्स फिर से स्व-हस्ताक्षरित प्रमाणपत्र उपयोग करेगा।", "Abgeschaltet – die Seite verbindet sich neu.": "बंद किया गया – पेज फिर से जुड़ रहा है।", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Let's Encrypt प्रमाणपत्र सक्रिय – पेज फिर से जुड़ रहा है।" + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Let's Encrypt प्रमाणपत्र सक्रिय – पेज फिर से जुड़ रहा है।", + "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 2aaf08e..b2dce5e 100644 --- a/src/shareos-core/www/i18n/pt.json +++ b/src/shareos-core/www/i18n/pt.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "Desligar o Let's Encrypt?", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "A caixa volta a usar um certificado autoassinado.", "Abgeschaltet – die Seite verbindet sich neu.": "Desligado – a página volta a ligar-se.", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Certificado Let's Encrypt ativo – a página volta a ligar-se." + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Certificado Let's Encrypt ativo – a página volta a ligar-se.", + "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 a263dfe..ae2dad4 100644 --- a/src/shareos-core/www/i18n/ru.json +++ b/src/shareos-core/www/i18n/ru.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "Отключить Let's Encrypt?", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "После этого устройство снова использует самоподписанный сертификат.", "Abgeschaltet – die Seite verbindet sich neu.": "Отключено – страница переподключается.", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Сертификат Let's Encrypt активен – страница переподключается." + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Сертификат Let's Encrypt активен – страница переподключается.", + "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 86b40f4..9e26fd0 100644 --- a/src/shareos-core/www/i18n/zh.json +++ b/src/shareos-core/www/i18n/zh.json @@ -1330,7 +1330,31 @@ "Let's Encrypt abschalten?": "关闭 Let's Encrypt?", "Die Box nutzt danach wieder ein selbstsigniertes Zertifikat.": "之后本设备将重新使用自签名证书。", "Abgeschaltet – die Seite verbindet sich neu.": "已关闭——页面将重新连接。", - "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Let's Encrypt 证书已启用——页面将重新连接。" + "Let's-Encrypt-Zertifikat aktiv – die Seite verbindet sich neu.": "Let's Encrypt 证书已启用——页面将重新连接。", + "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 798f265..9172c02 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' and method.startswith(('ddns', 'ca_', 'acme')): d = STATE.setdefault('ddns', {'enabled': False, 'provider': '', 'host': '', 'user': '', 'url': '', 'token_set': False, 'ipv4': True, 'ipv6': False, 'last': None}) ca = STATE.setdefault('ca', {'exists': False, 'certs': [], 'web': False}) 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 e53ce91..d3e024f 100644 --- a/tools/i18n/ar.tsv +++ b/tools/i18n/ar.tsv @@ -1693,3 +1693,27 @@ E1324 إيقاف Let's Encrypt؟ E1325 يعود الجهاز بعدها إلى شهادة موقّعة ذاتيًا. E1326 تم الإيقاف – تعيد الصفحة الاتصال. E1327 شهادة Let's Encrypt نشطة – تعيد الصفحة الاتصال. +E1328 الطابعات +E1329 جارٍ تحميل الطابعات … +E1330 الطابعات في الشبكة تطبيق نظام: تطبع الطابعات والأجهزة متعددة الوظائف الحديثة وتمسح ضوئيًا دون برامج تشغيل (AirPrint، Mopria، Windows 10/11)، والقديمة عبر المنفذ 9100 مع برنامج التشغيل على الحاسوب. +E1331 التثبيت من المتجر (ipp-usb + p910nd) +E1332 في الشبكة +E1333 غير متاح +E1334 دون برنامج تشغيل (IPP over USB) +E1335 مع برنامج التشغيل على الحاسوب (المنفذ 9100) +E1336 المسح الضوئي +E1337 عبر الشبكة (eSCL) إذا كان للجهاز ماسح ضوئي +E1338 العنوان +E1339 صفحة الجهاز +E1340 يعثر Windows 10/11 وmacOS وiPhone/iPad وAndroid على الطابعة تلقائيًا («إضافة طابعة»). يدويًا: أدخل العنوان أعلاه كطابعة IPP. +E1341 Windows: «إضافة طابعة» ← «عنوان TCP/IP» ← +E1342 ، المنفذ +E1343 ، برنامج تشغيل الشركة المصنّعة. macOS/Linux: «HP JetDirect – Socket» أو AppSocket. +E1344 صفحة اختبار +E1345 لا توجد طابعة USB متصلة. صِلها – تظهر هنا بعد ثوانٍ. +E1346 مشاركة الطابعات في الشبكة (تبدأ أيضًا بعد إعادة التشغيل) +E1347 تلقائي – دون برنامج تشغيل حيث تستطيع الطابعة +E1348 دائمًا المنفذ 9100 (برنامج التشغيل على الحاسوب) +E1349 يستطيع أي شخص في الشبكة الطباعة دون تسجيل دخول. اختر «دائمًا المنفذ 9100» فقط إذا لم تطبع الطابعة جيدًا دون برنامج تشغيل. +E1350 تم حفظ الطابعات. +E1351 تم إرسال صفحة الاختبار. diff --git a/tools/i18n/bn.tsv b/tools/i18n/bn.tsv index 84337e2..90d8193 100644 --- a/tools/i18n/bn.tsv +++ b/tools/i18n/bn.tsv @@ -1693,3 +1693,27 @@ E1324 Let's Encrypt বন্ধ করবেন? E1325 এরপর বক্স আবার স্ব-স্বাক্ষরিত সার্টিফিকেট ব্যবহার করবে। E1326 বন্ধ করা হয়েছে – পেজ আবার সংযুক্ত হচ্ছে। E1327 Let's Encrypt সার্টিফিকেট সক্রিয় – পেজ আবার সংযুক্ত হচ্ছে। +E1328 প্রিন্টার +E1329 প্রিন্টার লোড হচ্ছে … +E1330 নেটওয়ার্কে প্রিন্টার একটি সিস্টেম অ্যাপ: নতুন প্রিন্টার ও মাল্টিফাংশন ডিভাইস ড্রাইভার ছাড়াই প্রিন্ট ও স্ক্যান করে (AirPrint, Mopria, Windows 10/11), পুরোনোগুলো পোর্ট 9100 দিয়ে, ড্রাইভার কম্পিউটারে। +E1331 স্টোর থেকে ইনস্টল করুন (ipp-usb + p910nd) +E1332 নেটওয়ার্কে +E1333 পৌঁছানো যাচ্ছে না +E1334 ড্রাইভার ছাড়া (IPP over USB) +E1335 কম্পিউটারে ড্রাইভারসহ (পোর্ট 9100) +E1336 স্ক্যান +E1337 নেটওয়ার্কের মাধ্যমে (eSCL), যদি ডিভাইসে স্ক্যানার থাকে +E1338 ঠিকানা +E1339 ডিভাইসের পেজ +E1340 Windows 10/11, macOS, iPhone/iPad ও Android প্রিন্টার নিজেই খুঁজে পায় (“প্রিন্টার যোগ করুন”)। নিজে করতে: উপরের ঠিকানা IPP প্রিন্টার হিসেবে দিন। +E1341 Windows: “প্রিন্টার যোগ করুন” → “TCP/IP ঠিকানা” → +E1342 , পোর্ট +E1343 , নির্মাতার ড্রাইভার। macOS/Linux: “HP JetDirect – Socket” বা AppSocket। +E1344 টেস্ট পেজ +E1345 কোনো USB প্রিন্টার যুক্ত নেই। লাগান – কয়েক সেকেন্ডে এখানে দেখা যাবে। +E1346 প্রিন্টার নেটওয়ার্কে শেয়ার করুন (রিস্টার্টের পরেও চালু হবে) +E1347 স্বয়ংক্রিয় – প্রিন্টার পারলে ড্রাইভার ছাড়া +E1348 সবসময় পোর্ট 9100 (কম্পিউটারে ড্রাইভার) +E1349 নেটওয়ার্কের যে কেউ সাইন-ইন ছাড়াই প্রিন্ট করতে পারে। “সবসময় পোর্ট 9100” কেবল তখনই, যখন প্রিন্টার ড্রাইভার ছাড়া ঠিকমতো প্রিন্ট করে না। +E1350 প্রিন্টার সংরক্ষিত। +E1351 টেস্ট পেজ পাঠানো হয়েছে। diff --git a/tools/i18n/es.tsv b/tools/i18n/es.tsv index 0082c2d..a18e20c 100644 --- a/tools/i18n/es.tsv +++ b/tools/i18n/es.tsv @@ -1693,3 +1693,27 @@ E1324 ¿Desactivar Let's Encrypt? E1325 La caja vuelve a usar un certificado autofirmado. E1326 Desactivado – la página se reconecta. E1327 Certificado de Let's Encrypt activo – la página se reconecta. +E1328 IMPRESORAS +E1329 Cargando impresoras … +E1330 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. +E1331 Instalar desde la tienda (ipp-usb + p910nd) +E1332 en la red +E1333 no accesible +E1334 Sin controlador (IPP over USB) +E1335 Con el controlador en el ordenador (puerto 9100) +E1336 Escaneo +E1337 por la red (eSCL), si el equipo tiene escáner +E1338 Dirección +E1339 Página del equipo +E1340 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. +E1341 Windows: «Agregar impresora» → «Dirección TCP/IP» → +E1342 , puerto +E1343 , controlador del fabricante. macOS/Linux: «HP JetDirect – Socket» o AppSocket. +E1344 Página de prueba +E1345 No hay ninguna impresora USB conectada. Conéctala – aparece aquí en unos segundos. +E1346 Compartir impresoras en la red (también arranca tras reiniciar) +E1347 Automático – sin controlador cuando la impresora lo permite +E1348 Siempre puerto 9100 (controlador en el ordenador) +E1349 Cualquiera en la red puede imprimir, sin iniciar sesión. «Siempre puerto 9100» solo si una impresora no imprime bien sin controlador. +E1350 Impresoras guardadas. +E1351 Página de prueba enviada. diff --git a/tools/i18n/fr.tsv b/tools/i18n/fr.tsv index d75f346..8b758d8 100644 --- a/tools/i18n/fr.tsv +++ b/tools/i18n/fr.tsv @@ -1693,3 +1693,27 @@ E1324 Désactiver Let's Encrypt ? E1325 La box utilise ensuite de nouveau un certificat auto-signé. E1326 Désactivé – la page se reconnecte. E1327 Certificat Let's Encrypt actif – la page se reconnecte. +E1328 IMPRIMANTES +E1329 Chargement des imprimantes … +E1330 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. +E1331 Installer depuis le store (ipp-usb + p910nd) +E1332 dans le réseau +E1333 injoignable +E1334 Sans pilote (IPP over USB) +E1335 Avec le pilote sur l'ordinateur (port 9100) +E1336 Numérisation +E1337 via le réseau (eSCL), si l'appareil a un scanner +E1338 Adresse +E1339 Page de l'appareil +E1340 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. +E1341 Windows : « Ajouter une imprimante » → « Adresse TCP/IP » → +E1342 , port +E1343 , pilote du fabricant. macOS/Linux : « HP JetDirect – Socket » ou AppSocket. +E1344 Page de test +E1345 Aucune imprimante USB branchée. Branchez-en une – elle apparaît ici après quelques secondes. +E1346 Partager les imprimantes sur le réseau (démarre aussi après un redémarrage) +E1347 Automatique – sans pilote quand l'imprimante le permet +E1348 Toujours le port 9100 (pilote sur l'ordinateur) +E1349 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. +E1350 Imprimantes enregistrées. +E1351 Page de test envoyée. diff --git a/tools/i18n/hi.tsv b/tools/i18n/hi.tsv index 5793f14..b624f8d 100644 --- a/tools/i18n/hi.tsv +++ b/tools/i18n/hi.tsv @@ -1693,3 +1693,27 @@ E1324 Let's Encrypt बंद करें? E1325 इसके बाद बॉक्स फिर से स्व-हस्ताक्षरित प्रमाणपत्र उपयोग करेगा। E1326 बंद किया गया – पेज फिर से जुड़ रहा है। E1327 Let's Encrypt प्रमाणपत्र सक्रिय – पेज फिर से जुड़ रहा है। +E1328 प्रिंटर +E1329 प्रिंटर लोड हो रहे हैं … +E1330 नेटवर्क में प्रिंटर एक सिस्टम ऐप हैं: नए प्रिंटर और मल्टीफ़ंक्शन डिवाइस बिना ड्राइवर के प्रिंट और स्कैन करते हैं (AirPrint, Mopria, Windows 10/11), पुराने पोर्ट 9100 से, ड्राइवर कंप्यूटर पर। +E1331 स्टोर से इंस्टॉल करें (ipp-usb + p910nd) +E1332 नेटवर्क में +E1333 पहुँच से बाहर +E1334 बिना ड्राइवर (IPP over USB) +E1335 कंप्यूटर पर ड्राइवर के साथ (पोर्ट 9100) +E1336 स्कैनिंग +E1337 नेटवर्क से (eSCL), यदि डिवाइस में स्कैनर है +E1338 पता +E1339 डिवाइस पेज +E1340 Windows 10/11, macOS, iPhone/iPad और Android प्रिंटर खुद ढूँढ लेते हैं (“प्रिंटर जोड़ें”)। हाथ से: ऊपर का पता IPP प्रिंटर के रूप में डालें। +E1341 Windows: “प्रिंटर जोड़ें” → “TCP/IP पता” → +E1342 , पोर्ट +E1343 , निर्माता का ड्राइवर। macOS/Linux: “HP JetDirect – Socket” या AppSocket। +E1344 टेस्ट पेज +E1345 कोई USB प्रिंटर जुड़ा नहीं है। लगाइए – कुछ सेकंड में यहाँ दिखेगा। +E1346 प्रिंटर नेटवर्क में साझा करें (रीस्टार्ट के बाद भी शुरू होगा) +E1347 स्वचालित – जहाँ प्रिंटर सपोर्ट करे, बिना ड्राइवर +E1348 हमेशा पोर्ट 9100 (कंप्यूटर पर ड्राइवर) +E1349 नेटवर्क में कोई भी बिना साइन-इन प्रिंट कर सकता है। “हमेशा पोर्ट 9100” तभी चुनें जब प्रिंटर बिना ड्राइवर ठीक से प्रिंट न करे। +E1350 प्रिंटर सहेजे गए। +E1351 टेस्ट पेज भेजा गया। diff --git a/tools/i18n/pt.tsv b/tools/i18n/pt.tsv index 2f6e67a..0c1bcb6 100644 --- a/tools/i18n/pt.tsv +++ b/tools/i18n/pt.tsv @@ -1693,3 +1693,27 @@ E1324 Desligar o Let's Encrypt? E1325 A caixa volta a usar um certificado autoassinado. E1326 Desligado – a página volta a ligar-se. E1327 Certificado Let's Encrypt ativo – a página volta a ligar-se. +E1328 IMPRESSORAS +E1329 A carregar impressoras … +E1330 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. +E1331 Instalar a partir da loja (ipp-usb + p910nd) +E1332 na rede +E1333 inacessível +E1334 Sem controlador (IPP over USB) +E1335 Com o controlador no computador (porta 9100) +E1336 Digitalização +E1337 pela rede (eSCL), se o dispositivo tiver scanner +E1338 Endereço +E1339 Página do dispositivo +E1340 Windows 10/11, macOS, iPhone/iPad e Android encontram a impressora sozinhos («Adicionar impressora»). À mão: introduza o endereço acima como impressora IPP. +E1341 Windows: «Adicionar impressora» → «Endereço TCP/IP» → +E1342 , porta +E1343 , controlador do fabricante. macOS/Linux: «HP JetDirect – Socket» ou AppSocket. +E1344 Página de teste +E1345 Nenhuma impressora USB ligada. Ligue uma – aparece aqui após alguns segundos. +E1346 Partilhar impressoras na rede (também arranca após reiniciar) +E1347 Automático – sem controlador quando a impressora o permite +E1348 Sempre porta 9100 (controlador no computador) +E1349 Qualquer pessoa na rede pode imprimir, sem iniciar sessão. «Sempre porta 9100» só se uma impressora não imprimir bem sem controlador. +E1350 Impressoras guardadas. +E1351 Página de teste enviada. diff --git a/tools/i18n/ru.tsv b/tools/i18n/ru.tsv index 3305b25..e72d80a 100644 --- a/tools/i18n/ru.tsv +++ b/tools/i18n/ru.tsv @@ -1693,3 +1693,27 @@ E1324 Отключить Let's Encrypt? E1325 После этого устройство снова использует самоподписанный сертификат. E1326 Отключено – страница переподключается. E1327 Сертификат Let's Encrypt активен – страница переподключается. +E1328 ПРИНТЕРЫ +E1329 Загрузка принтеров … +E1330 Сетевые принтеры – это системное приложение: современные принтеры и МФУ печатают и сканируют без драйверов (AirPrint, Mopria, Windows 10/11), старые – через порт 9100 с драйвером на компьютере. +E1331 Установить из магазина (ipp-usb + p910nd) +E1332 в сети +E1333 недоступен +E1334 Без драйвера (IPP over USB) +E1335 С драйвером на компьютере (порт 9100) +E1336 Сканирование +E1337 по сети (eSCL), если у устройства есть сканер +E1338 Адрес +E1339 Страница устройства +E1340 Windows 10/11, macOS, iPhone/iPad и Android находят принтер сами («Добавить принтер»). Вручную: укажите адрес выше как IPP-принтер. +E1341 Windows: «Добавить принтер» → «TCP/IP-адрес» → +E1342 , порт +E1343 , драйвер производителя. macOS/Linux: «HP JetDirect – Socket» или AppSocket. +E1344 Тестовая страница +E1345 USB-принтер не подключён. Подключите – он появится здесь через несколько секунд. +E1346 Открыть доступ к принтерам по сети (запускается и после перезагрузки) +E1347 Автоматически – без драйвера, где принтер это умеет +E1348 Всегда порт 9100 (драйвер на компьютере) +E1349 Печатать может любой в сети, без входа. «Всегда порт 9100» – только если принтер без драйвера печатает неправильно. +E1350 Принтеры сохранены. +E1351 Тестовая страница отправлена. diff --git a/tools/i18n/zh.tsv b/tools/i18n/zh.tsv index 4e29f6e..025024f 100644 --- a/tools/i18n/zh.tsv +++ b/tools/i18n/zh.tsv @@ -1693,3 +1693,27 @@ E1324 关闭 Let's Encrypt? E1325 之后本设备将重新使用自签名证书。 E1326 已关闭——页面将重新连接。 E1327 Let's Encrypt 证书已启用——页面将重新连接。 +E1328 打印机 +E1329 正在加载打印机…… +E1330 网络打印是一个系统应用:较新的打印机和多功能一体机可免驱动打印和扫描(AirPrint、Mopria、Windows 10/11),较旧的通过 9100 端口打印,驱动装在电脑上。 +E1331 从商店安装 (ipp-usb + p910nd) +E1332 已共享到网络 +E1333 无法访问 +E1334 免驱动 (IPP over USB) +E1335 驱动装在电脑上(9100 端口) +E1336 扫描 +E1337 通过网络 (eSCL),如果设备带扫描仪 +E1338 地址 +E1339 设备页面 +E1340 Windows 10/11、macOS、iPhone/iPad 和 Android 会自动找到打印机(“添加打印机”)。手动添加:将上面的地址作为 IPP 打印机填入。 +E1341 Windows:“添加打印机” → “TCP/IP 地址” → +E1342 ,端口 +E1343 ,使用厂商驱动。macOS/Linux:“HP JetDirect – Socket”或 AppSocket。 +E1344 测试页 +E1345 未连接 USB 打印机。插上后几秒钟即会在此显示。 +E1346 在网络中共享打印机(重启后也会启动) +E1347 自动——打印机支持时免驱动 +E1348 始终使用 9100 端口(驱动装在电脑上) +E1349 网络中的任何人都可以打印,无需登录。仅当打印机免驱动打印不正常时才选“始终使用 9100 端口”。 +E1350 打印机设置已保存。 +E1351 测试页已发送。