Last active
April 26, 2026 00:52
-
-
Save ybart/8be271ae6b61997fefbd03a1f4221573 to your computer and use it in GitHub Desktop.
HP WiFi config over USB - no HPLIP required, adaptable to other HP LaserJet/OfficeJet/Photosmart models.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| HP Printer EWS USB Proxy | |
| ======================== | |
| Exposes the HP printer's Embedded Web Server (EWS) through a local HTTP proxy, | |
| forwarding requests over USB bulk endpoints. | |
| Useful when the printer is not yet on WiFi and its web UI is unreachable | |
| over the network. | |
| Requirements: | |
| pip install pyusb (or: sudo apt install python3-usb) | |
| Usage: | |
| python3 hp_ews_proxy.py | |
| Then open: http://localhost:8080 | |
| Custom port: | |
| python3 hp_ews_proxy.py --port 9090 | |
| Adapting for other HP printer models: | |
| Find your printer's USB product ID with lsusb (Linux) or | |
| system_profiler SPUSBDataType (macOS), then update HP_PRODUCT_ID below. | |
| Also verify EWS_INTERFACE, ENDPOINT_OUT, ENDPOINT_IN match your printer | |
| (see hp-wifi-config.py --help-adapt for full instructions). | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| import time | |
| import threading | |
| from http.server import BaseHTTPRequestHandler, HTTPServer | |
| try: | |
| import usb.core | |
| import usb.util | |
| except ImportError: | |
| print("Error: pyusb not installed.") | |
| print("Install with: pip3 install pyusb or sudo apt install python3-usb") | |
| sys.exit(1) | |
| HP_VENDOR_ID = 0x03f0 | |
| HP_PRODUCT_ID = 0x102a | |
| EWS_INTERFACE = 1 | |
| ENDPOINT_OUT = 0x02 | |
| ENDPOINT_IN = 0x82 | |
| USB_TIMEOUT_MS = 10000 | |
| usb_dev = None | |
| usb_lock = threading.Lock() | |
| product_id_g = HP_PRODUCT_ID | |
| def connect_printer(product_id): | |
| """Find and claim the printer EWS interface. Returns device or None.""" | |
| dev = usb.core.find(idVendor=HP_VENDOR_ID, idProduct=product_id) | |
| if dev is None: | |
| return None | |
| try: | |
| if dev.is_kernel_driver_active(EWS_INTERFACE): | |
| dev.detach_kernel_driver(EWS_INTERFACE) | |
| usb.util.claim_interface(dev, EWS_INTERFACE) | |
| print(f"Connected: {usb.util.get_string(dev, dev.iProduct)}") | |
| return dev | |
| except usb.core.USBError: | |
| return None | |
| def get_device(): | |
| """Return live device or None if disconnected. Never blocks.""" | |
| global usb_dev | |
| if usb_dev is not None: | |
| try: | |
| usb_dev.is_kernel_driver_active(EWS_INTERFACE) | |
| except usb.core.USBError: | |
| print("Printer disconnected.") | |
| usb_dev = None | |
| if usb_dev is None: | |
| usb_dev = connect_printer(product_id_g) | |
| return usb_dev | |
| def reconnect_loop(): | |
| """Background thread that keeps trying to reconnect.""" | |
| global usb_dev | |
| while True: | |
| time.sleep(1) | |
| with usb_lock: | |
| if usb_dev is None: | |
| dev = connect_printer(product_id_g) | |
| if dev: | |
| usb_dev = dev | |
| class EWSProxyHandler(BaseHTTPRequestHandler): | |
| def log_message(self, format, *args): | |
| print(f" {self.command} {self.path} -> {format % args}") | |
| def send_503(self): | |
| crlf = b"\r\n" | |
| msg = b"Printer disconnected - replug USB and retry" | |
| resp = (b"HTTP/1.1 503 Service Unavailable" + crlf + | |
| b"Content-Type: text/plain" + crlf + | |
| b"Connection: close" + crlf + | |
| b"Content-Length: " + str(len(msg)).encode() + crlf + | |
| crlf + msg) | |
| try: | |
| self.wfile.write(resp) | |
| self.wfile.flush() | |
| except Exception: | |
| pass | |
| def do_request(self): | |
| global usb_dev | |
| content_length = int(self.headers.get("Content-Length", 0)) | |
| body = self.rfile.read(content_length) if content_length else b"" | |
| raw = ( | |
| f"{self.command} {self.path} HTTP/1.1\r\n" | |
| f"Host: localhost\r\n" | |
| f"Content-Length: {len(body)}\r\n" | |
| f"Connection: close\r\n\r\n" | |
| ).encode() + body | |
| with usb_lock: | |
| dev = get_device() | |
| if dev is None: | |
| self.send_503() | |
| return | |
| try: | |
| dev.write(ENDPOINT_OUT, raw, timeout=USB_TIMEOUT_MS) | |
| response = b"" | |
| try: | |
| while True: | |
| chunk = bytes(dev.read(ENDPOINT_IN, 65536, timeout=USB_TIMEOUT_MS)) | |
| response += chunk | |
| if len(chunk) < 65536: | |
| break | |
| except usb.core.USBTimeoutError: | |
| pass | |
| self.wfile.write(response) | |
| except usb.core.USBError as e: | |
| print(f"USB error: {e}") | |
| usb_dev = None | |
| self.send_503() | |
| do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = do_OPTIONS = do_request | |
| def handle_error(self): | |
| pass # Suppress stack trace on broken browser connections | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Proxy HP printer EWS over USB") | |
| parser.add_argument("--port", type=int, default=8080) | |
| args = parser.parse_args() | |
| global usb_dev, product_id_g | |
| product_id_g = HP_PRODUCT_ID | |
| # Try to connect but don't require it — background thread will connect when ready | |
| usb_dev = connect_printer(product_id_g) | |
| if usb_dev is None: | |
| print("Printer not found — will connect when available.") | |
| t = threading.Thread(target=reconnect_loop, daemon=True) | |
| t.start() | |
| print(f"Proxy running at http://localhost:{args.port}") | |
| print("Press Ctrl+C to stop.\n") | |
| class QuietHTTPServer(HTTPServer): | |
| def handle_error(self, request, client_address): | |
| pass # Suppress stack traces on broken connections | |
| try: | |
| QuietHTTPServer(("127.0.0.1", args.port), EWSProxyHandler).serve_forever() | |
| except KeyboardInterrupt: | |
| print("\nStopping.") | |
| finally: | |
| try: | |
| usb.util.release_interface(usb_dev, EWS_INTERFACE) | |
| usb_dev.attach_kernel_driver(EWS_INTERFACE) | |
| except Exception: | |
| pass | |
| if __name__ == "__main__": | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| HP LaserJet P1102w WiFi Configuration Tool | |
| ========================================== | |
| Pushes WiFi credentials to the printer over USB using the HP EWS/LEDM interface. | |
| No HPLIP, no CUPS, no hp-wificonfig required. | |
| Requirements: | |
| pip install pyusb (or: sudo apt install python3-usb) | |
| Usage: | |
| # Auto-detect current WiFi and push to printer (Linux/macOS): | |
| python3 hp_wifi_config.py --auto | |
| # Manually specify credentials: | |
| python3 hp_wifi_config.py --ssid "MyNetwork" --password "MyPassword" | |
| python3 hp_wifi_config.py --ssid "MyNetwork" --password "MyPassword" --security WPA_PSK | |
| # Show printer WiFi status: | |
| python3 hp_wifi_config.py --status | |
| Adapting for other HP printer models: | |
| This script can be adapted for most HP LaserJet, OfficeJet, and Photosmart | |
| printers from ~2009 onwards. See the "Adapting for another HP printer model" | |
| section below for step-by-step instructions. | |
| Supported security types: | |
| WPA_PSK (default), WPA2_PSK, WEP, none | |
| Auto-detect notes: | |
| Linux : reads SSID + password from NetworkManager via nmcli (requires sudo) | |
| macOS : reads SSID via ipconfig setverbose, password from Keychain (requires sudo) | |
| The script will automatically re-exec itself with sudo if needed. | |
| Adapting for another HP printer model: | |
| This script uses the HP EWS/LEDM protocol over USB, which is supported by most | |
| HP LaserJet, OfficeJet, and Photosmart printers from ~2009 onwards. | |
| 1. Find your printer's USB IDs: | |
| lsusb (Linux) | |
| system_profiler SPUSBDataType (macOS) | |
| Look for "Hewlett-Packard" or "HP" and note idVendor and idProduct. | |
| Example: "ID 03f0:102a" -> HP_VENDOR_ID=0x03f0, HP_PRODUCT_ID=0x102a | |
| 2. Update the constants near the top of this script: | |
| HP_VENDOR_ID = 0x03f0 # always 0x03f0 for HP | |
| HP_PRODUCT_ID = 0x102a # change this to your printer's product ID | |
| 3. Find the EWS interface number and endpoints: | |
| sudo python3 -c " | |
| import usb.core, usb.util | |
| dev = usb.core.find(idVendor=0x03f0, idProduct=0xYOUR_ID) | |
| print(dev) | |
| " | |
| Look for an interface with: | |
| bInterfaceClass : 0xff (Vendor Specific) | |
| bInterfaceSubClass : 0x02 | |
| bInterfaceProtocol : 0x10 | |
| iInterface : HP EWS (or similar) | |
| Note its interface number and the Bulk OUT / Bulk IN endpoint addresses. | |
| Update EWS_INTERFACE, ENDPOINT_OUT, ENDPOINT_IN accordingly. | |
| 4. Verify the EWS interface works: | |
| sudo python3 -c " | |
| import usb.core, usb.util | |
| dev = usb.core.find(idVendor=0x03f0, idProduct=0xYOUR_ID) | |
| if dev.is_kernel_driver_active(EWS_INTERFACE): | |
| dev.detach_kernel_driver(EWS_INTERFACE) | |
| usb.util.claim_interface(dev, EWS_INTERFACE) | |
| req = b'GET /IoMgmt/Adapters HTTP/1.1 | |
| Host: localhost | |
| Content-Length: 0 | |
| Connection: close | |
| ' | |
| dev.write(ENDPOINT_OUT, req, timeout=10000) | |
| print(bytes(dev.read(ENDPOINT_IN, 4096, timeout=10000)).decode()) | |
| " | |
| If you get a 200 OK with XML, the interface works and the rest of the | |
| script should work as-is. | |
| 5. If the WiFi adapter is not named "wifi0", check the ResourceURI values | |
| in the GET /IoMgmt/Adapters response and update the endpoint in set_wifi(): | |
| endpoint = "/IoMgmt/Adapters/wifi0/Profiles/Active" | |
| """ | |
| import argparse | |
| import binascii | |
| import os | |
| import subprocess | |
| import sys | |
| import re | |
| try: | |
| import usb.core | |
| import usb.util | |
| except ImportError: | |
| print("Error: pyusb not installed.") | |
| if sys.platform == "darwin": | |
| print("Install with: pip3 install pyusb") | |
| print(" or: python3 -m pip install pyusb") | |
| else: | |
| print("Install with: sudo apt install python3-usb") | |
| print(" or: pip3 install pyusb") | |
| sys.exit(1) | |
| # HP LaserJet P1102w USB identifiers | |
| HP_VENDOR_ID = 0x03f0 | |
| HP_PRODUCT_ID = 0x102a | |
| # USB interface and endpoints | |
| EWS_INTERFACE = 1 | |
| ENDPOINT_OUT = 0x02 # Bulk OUT | |
| ENDPOINT_IN = 0x82 # Bulk IN | |
| USB_TIMEOUT_MS = 10000 | |
| # ───────────────────────────────────────────── | |
| # WiFi credential auto-detection | |
| # ───────────────────────────────────────────── | |
| def get_wifi_linux(): | |
| """ | |
| Read current WiFi SSID and password from NetworkManager on Linux. | |
| Requires sudo (nmcli needs elevated privileges to show passwords). | |
| """ | |
| # Get current connected SSID | |
| try: | |
| result = subprocess.run( | |
| ["nmcli", "-t", "-f", "active,ssid", "dev", "wifi"], | |
| capture_output=True, text=True, check=True | |
| ) | |
| ssid = None | |
| for line in result.stdout.splitlines(): | |
| if line.startswith("yes:"): | |
| ssid = line.split(":", 1)[1].strip() | |
| break | |
| if not ssid: | |
| return None, None, None | |
| except (subprocess.CalledProcessError, FileNotFoundError): | |
| return None, None, None | |
| # Get password and security type for this SSID | |
| try: | |
| result = subprocess.run( | |
| ["nmcli", "-s", "-g", | |
| "802-11-wireless-security.key-mgmt," | |
| "802-11-wireless-security.psk," | |
| "802-11-wireless-security.wep-key0", | |
| "connection", "show", ssid], | |
| capture_output=True, text=True, check=True | |
| ) | |
| lines = result.stdout.strip().splitlines() | |
| key_mgmt = lines[0].strip() if len(lines) > 0 else "" | |
| password = lines[1].strip() if len(lines) > 1 else "" | |
| wep_key = lines[2].strip() if len(lines) > 2 else "" | |
| if not password and wep_key: | |
| password = wep_key | |
| # Map nmcli key-mgmt to printer security type | |
| security_map = { | |
| "wpa-psk": "WPA_PSK", | |
| "wpa-eap": "WPA_PSK", | |
| "none": "none", | |
| "": "none", | |
| } | |
| security = security_map.get(key_mgmt.lower(), "WPA_PSK") | |
| return ssid, password, security | |
| except (subprocess.CalledProcessError, FileNotFoundError): | |
| return ssid, None, None | |
| def get_wifi_macos(): | |
| """ | |
| Read current WiFi SSID and password on macOS. | |
| Uses ipconfig setverbose 1 to unredact SSID (requires sudo). | |
| Password is read from Keychain. | |
| """ | |
| ssid = None | |
| # Enable verbose mode to unredact SSID, then restore it | |
| try: | |
| subprocess.run(["ipconfig", "setverbose", "1"], | |
| capture_output=True, check=True) | |
| result = subprocess.run(["ifconfig", "-l"], | |
| capture_output=True, text=True) | |
| interfaces = [i for i in result.stdout.split() if i.startswith("en")] | |
| for iface in interfaces: | |
| r = subprocess.run(["ipconfig", "getsummary", iface], | |
| capture_output=True, text=True) | |
| for line in r.stdout.splitlines(): | |
| if re.match(r'^\s*SSID\s*:', line): | |
| ssid = line.split(":", 1)[1].strip() | |
| break | |
| if ssid: | |
| break | |
| except (subprocess.CalledProcessError, FileNotFoundError): | |
| pass | |
| finally: | |
| subprocess.run(["ipconfig", "setverbose", "0"], | |
| capture_output=True) | |
| if not ssid: | |
| return None, None, None | |
| # Get password from Keychain | |
| password = None | |
| for cmd in [ | |
| ["security", "find-generic-password", | |
| "-D", "AirPort network password", "-a", ssid, "-w"], | |
| ["security", "find-generic-password", "-s", ssid, "-w"], | |
| ]: | |
| try: | |
| result = subprocess.run(cmd, capture_output=True, | |
| text=True, check=True) | |
| password = result.stdout.strip() | |
| if password: | |
| break | |
| except subprocess.CalledProcessError: | |
| continue | |
| return ssid, password, "WPA_PSK" | |
| def auto_detect_wifi(): | |
| """Detect current WiFi credentials from the OS.""" | |
| if sys.platform.startswith("linux"): | |
| print("Detecting WiFi credentials from NetworkManager...") | |
| ssid, password, security = get_wifi_linux() | |
| elif sys.platform == "darwin": | |
| print("Detecting WiFi credentials from macOS Keychain...") | |
| ssid, password, security = get_wifi_macos() | |
| else: | |
| print(f"Auto-detect not supported on platform: {sys.platform}") | |
| print("Use --ssid and --password instead.") | |
| sys.exit(1) | |
| if not ssid: | |
| print("Error: could not detect current WiFi network.") | |
| print("Make sure you are connected to WiFi, or use --ssid/--password.") | |
| sys.exit(1) | |
| if not password: | |
| print(f"Detected SSID: {ssid}") | |
| print("Warning: could not retrieve password automatically.") | |
| import getpass | |
| password = getpass.getpass(f"Enter WiFi password for '{ssid}': ") | |
| return ssid, password, security or "WPA_PSK" | |
| # ───────────────────────────────────────────── | |
| # USB / printer communication | |
| # ───────────────────────────────────────────── | |
| def find_printer(): | |
| dev = usb.core.find(idVendor=HP_VENDOR_ID, idProduct=HP_PRODUCT_ID) | |
| if dev is None: | |
| print("Error: HP LaserJet P1102w not found.") | |
| print("Make sure the printer is connected via USB and powered on.") | |
| sys.exit(1) | |
| return dev | |
| def claim_ews_interface(dev): | |
| try: | |
| if dev.is_kernel_driver_active(EWS_INTERFACE): | |
| dev.detach_kernel_driver(EWS_INTERFACE) | |
| print(f"Detached kernel driver from interface {EWS_INTERFACE}") | |
| usb.util.claim_interface(dev, EWS_INTERFACE) | |
| print(f"Claimed EWS interface (interface {EWS_INTERFACE})") | |
| except usb.core.USBError as e: | |
| print(f"Error claiming EWS interface: {e}") | |
| print("Try running with sudo.") | |
| sys.exit(1) | |
| def release_ews_interface(dev): | |
| try: | |
| usb.util.release_interface(dev, EWS_INTERFACE) | |
| dev.attach_kernel_driver(EWS_INTERFACE) | |
| except Exception: | |
| pass | |
| def send_http_request(dev, method, path, body=None, content_type="text/xml; charset=utf-8"): | |
| body_bytes = body.encode("utf-8") if isinstance(body, str) else (body or b"") | |
| request = ( | |
| f"{method} {path} HTTP/1.1\r\n" | |
| f"Host: localhost\r\n" | |
| f"User-Agent: hp-wifi-config/1.0\r\n" | |
| f"Content-Type: {content_type}\r\n" | |
| f"Content-Length: {len(body_bytes)}\r\n" | |
| f"Connection: close\r\n" | |
| f"\r\n" | |
| ).encode("utf-8") + body_bytes | |
| dev.write(ENDPOINT_OUT, request, timeout=USB_TIMEOUT_MS) | |
| response = b"" | |
| try: | |
| while True: | |
| chunk = bytes(dev.read(ENDPOINT_IN, 4096, timeout=USB_TIMEOUT_MS)) | |
| response += chunk | |
| if len(chunk) < 4096: | |
| break | |
| except usb.core.USBTimeoutError: | |
| pass | |
| text = response.decode("utf-8", errors="replace") | |
| return decode_chunked(text) | |
| def decode_chunked(response): | |
| """Strip HTTP chunked transfer encoding from response body.""" | |
| sep = chr(13) + chr(10) + chr(13) + chr(10) | |
| crlf = chr(13) + chr(10) | |
| if sep not in response: | |
| return response | |
| headers, body = response.split(sep, 1) | |
| if "chunked" not in headers.lower(): | |
| return response | |
| result = [] | |
| while body: | |
| pos = body.find(crlf) | |
| if pos == -1: | |
| break | |
| size_str = body[:pos].split(";")[0].strip() | |
| try: | |
| size = int(size_str, 16) | |
| except ValueError: | |
| break | |
| if size == 0: | |
| break | |
| chunk = body[pos + 2: pos + 2 + size] | |
| result.append(chunk) | |
| body = body[pos + 2 + size + 2:] | |
| return headers + sep + "".join(result) | |
| def decode_ssid(hex_ssid): | |
| try: | |
| return binascii.unhexlify(hex_ssid.strip()).decode("utf-8") | |
| except Exception: | |
| return hex_ssid | |
| def encode_ssid(ssid): | |
| return ssid.encode("utf-8").hex().upper() | |
| def parse_adapters(response): | |
| adapters = [] | |
| for block in re.findall(r'<io:Adapter>(.*?)</io:Adapter>', response, re.DOTALL): | |
| adapter = {} | |
| for key, pattern in [ | |
| ("uri", r'<dd:ResourceURI>(.*?)</dd:ResourceURI>'), | |
| ("name", r'<dd:Name>(.*?)</dd:Name>'), | |
| ("power", r'<dd:Power>\s*(.*?)\s*</dd:Power>'), | |
| ("security", r'<wifi:EncryptionType>\s*(.*?)\s*</wifi:EncryptionType>'), | |
| ("connected", r'<dd:IsConnected>\s*(.*?)\s*</dd:IsConnected>'), | |
| ("status", r'<dd:NetworkStatus>\s*(.*?)\s*</dd:NetworkStatus>'), | |
| ]: | |
| m = re.search(pattern, block, re.DOTALL) | |
| if m: | |
| adapter[key] = m.group(1).strip() | |
| m = re.search(r'<wifi:SSID>\s*([0-9A-Fa-f\s]+?)\s*</wifi:SSID>', block, re.DOTALL) | |
| if m: | |
| raw = re.sub(r'\s+', '', m.group(1)) | |
| adapter["ssid_hex"] = raw | |
| adapter["ssid"] = decode_ssid(raw) | |
| adapters.append(adapter) | |
| return adapters | |
| def get_status(dev): | |
| print("\nQuerying printer WiFi status...") | |
| response = send_http_request(dev, "GET", "/IoMgmt/Adapters") | |
| if "200 OK" not in response: | |
| print("Error: unexpected response:") | |
| print(response[:500]) | |
| return | |
| adapters = parse_adapters(response) | |
| if not adapters: | |
| print("No adapters found.") | |
| return | |
| print(f"\nFound {len(adapters)} WiFi adapter(s):\n") | |
| for a in adapters: | |
| print(f" Adapter : {a.get('name', 'unknown')}") | |
| print(f" URI : {a.get('uri', 'unknown')}") | |
| print(f" Power : {a.get('power', 'unknown')}") | |
| if "ssid" in a: | |
| print(f" SSID : {a['ssid']}") | |
| if "security" in a: | |
| print(f" Security : {a['security']}") | |
| if "connected" in a: | |
| print(f" Connected: {a['connected']}") | |
| if "status" in a: | |
| print(f" Status : {a['status']}") | |
| print() | |
| # ───────────────────────────────────────────── | |
| # WiFi configuration | |
| # ───────────────────────────────────────────── | |
| def build_wifi_xml(ssid, password, security, communication_mode="infrastructure"): | |
| """ | |
| Build the XML payload for WiFi association PUT request. | |
| Based on HPLIP LedmWifi.py associate() function. | |
| Endpoint: /IoMgmt/Adapters/wifi0/Profiles/Active | |
| """ | |
| ssid_hex = encode_ssid(ssid) | |
| password_hex = password.encode("utf-8").hex() | |
| ns = ( | |
| ' xmlns:io="http://www.hp.com/schemas/imaging/con/ledm/iomgmt/2008/11/30"' | |
| ' xmlns:dd="http://www.hp.com/schemas/imaging/con/dictionaries/1.0/"' | |
| ' xmlns:wifi="http://www.hp.com/schemas/imaging/con/wifi/2009/06/26"' | |
| ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' | |
| ' xsi:schemaLocation="http://www.hp.com/schemas/imaging/con/ledm/iomgmt/2008/11/30' | |
| ' ../../schemas/IoMgmt.xsd' | |
| ' http://www.hp.com/schemas/imaging/con/dictionaries/1.0/' | |
| ' ../../schemas/dd/DataDictionaryMasterLEDM.xsd"' | |
| ) | |
| if security == "none": | |
| return ( | |
| f'<?xml version="1.0" encoding="UTF-8"?>' | |
| f'<io:Profile{ns}>' | |
| f'<io:AdapterProfile><io:WifiProfile>' | |
| f'<wifi:SSID>{ssid_hex}</wifi:SSID>' | |
| f'<wifi:CommunicationMode>{communication_mode}</wifi:CommunicationMode>' | |
| f'<wifi:EncryptionType>{security}</wifi:EncryptionType>' | |
| f'<wifi:AuthenticationMode>open</wifi:AuthenticationMode>' | |
| f'</io:WifiProfile></io:AdapterProfile></io:Profile>' | |
| ) | |
| else: | |
| return ( | |
| f'<?xml version="1.0" encoding="UTF-8"?>' | |
| f'<io:Profile{ns}>' | |
| f'<io:AdapterProfile><io:WifiProfile>' | |
| f'<wifi:SSID>{ssid_hex}</wifi:SSID>' | |
| f'<wifi:CommunicationMode>{communication_mode}</wifi:CommunicationMode>' | |
| f'<wifi:EncryptionType>{security}</wifi:EncryptionType>' | |
| f'<wifi:AuthenticationMode>{security}</wifi:AuthenticationMode>' | |
| f'<io:KeyInfo><io:WpaPassPhraseInfo>' | |
| f'<wifi:RsnEncryption>AESOrTKIP</wifi:RsnEncryption>' | |
| f'<wifi:RsnAuthorization>autoWPA</wifi:RsnAuthorization>' | |
| f'<wifi:PassPhrase>{password_hex}</wifi:PassPhrase>' | |
| f'</io:WpaPassPhraseInfo></io:KeyInfo>' | |
| f'</io:WifiProfile></io:AdapterProfile></io:Profile>' | |
| ) | |
| def set_wifi(dev, ssid, password, security): | |
| print(f"\nConfiguring WiFi:") | |
| print(f" SSID : {ssid}") | |
| print(f" Security: {security}") | |
| print(f" Password: {'*' * len(password)}") | |
| print("\nVerifying printer is reachable...") | |
| response = send_http_request(dev, "GET", "/IoMgmt/Adapters") | |
| if "200 OK" not in response: | |
| print("Error: could not reach printer EWS interface.") | |
| sys.exit(1) | |
| print("Printer EWS interface OK.") | |
| endpoint = "/IoMgmt/Adapters/wifi0/Profiles/Active" | |
| xml_body = build_wifi_xml(ssid, password, security) | |
| print(f"\nSending WiFi configuration to {endpoint}...") | |
| response = send_http_request(dev, "PUT", endpoint, body=xml_body) | |
| first_line = response.split("\r\n")[0] if "\r\n" in response else response[:100] | |
| print(f"Printer response: {first_line}") | |
| if any(code in response for code in ["200 OK", "204", "202"]): | |
| print("\n✓ WiFi configuration sent successfully!") | |
| print(" The printer will now attempt to connect to the new network.") | |
| print(" This may take 30-60 seconds.") | |
| print(" Print a config page (hold Go ~5 sec) to verify the new IP.") | |
| elif "400" in response: | |
| print("\n✗ Bad request — check security type.") | |
| print(" Try: --security WPA2_PSK or --security WPA_PSK") | |
| print("\nFull response:\n" + response[:1000]) | |
| elif "401" in response or "403" in response: | |
| print("\n✗ Authentication error — printer may have a password set.") | |
| elif "404" in response: | |
| print("\n✗ Endpoint not found.\nFull response:\n" + response[:500]) | |
| else: | |
| print(f"\n? Unexpected response:\n" + response[:1000]) | |
| # ───────────────────────────────────────────── | |
| # Main | |
| # ───────────────────────────────────────────── | |
| def main(): | |
| # Re-exec with sudo if not root | |
| if os.geteuid() != 0: | |
| print("Requesting sudo privileges...") | |
| os.execvp("sudo", ["sudo", sys.executable] + sys.argv) | |
| parser = argparse.ArgumentParser( | |
| description="Configure HP LaserJet P1102w WiFi over USB", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| python3 hp_wifi_config.py --auto | |
| python3 hp_wifi_config.py --status | |
| python3 hp_wifi_config.py --ssid "MyNetwork" --password "MyPassword" | |
| python3 hp_wifi_config.py --ssid "MyNetwork" --password "MyPassword" --security WPA2_PSK | |
| Adapting for other HP models: | |
| This script supports most HP LaserJet, OfficeJet, and Photosmart printers | |
| from ~2009 onwards. Run: python3 hp_wifi_config.py --help-adapt | |
| """ | |
| ) | |
| parser.add_argument("--help-adapt", action="store_true", | |
| help="Show instructions for adapting this script to other HP printer models") | |
| parser.add_argument("--auto", action="store_true", | |
| help="Auto-detect current WiFi credentials from OS and push to printer") | |
| parser.add_argument("--status", action="store_true", | |
| help="Show current printer WiFi status") | |
| parser.add_argument("--ssid", help="WiFi network name (SSID)") | |
| parser.add_argument("--password", help="WiFi password") | |
| parser.add_argument("--security", default="WPA_PSK", | |
| choices=["WPA_PSK", "WPA2_PSK", "WEP", "none"], | |
| help="Security type (default: WPA_PSK)") | |
| args = parser.parse_args() | |
| if args.help_adapt: | |
| import re as _re | |
| docstring = __doc__ | |
| m = _re.search(r'(Adapting for another HP printer model:.*)', docstring, _re.DOTALL) | |
| if m: | |
| print(m.group(1)) | |
| sys.exit(0) | |
| if not args.status and not args.auto and not (args.ssid and args.password): | |
| parser.print_help() | |
| print("\nError: provide --status, --auto, or both --ssid and --password.") | |
| sys.exit(1) | |
| print("HP LaserJet P1102w WiFi Configuration Tool") | |
| print("=" * 45) | |
| ssid = password = security = None | |
| if args.auto: | |
| ssid, password, security = auto_detect_wifi() | |
| print(f"\nDetected SSID : {ssid}") | |
| print(f"Detected security: {security}") | |
| print(f"Password : {'*' * len(password)}") | |
| confirm = input("\nPush these credentials to the printer? [y/N] ").strip().lower() | |
| if confirm != "y": | |
| print("Aborted.") | |
| sys.exit(0) | |
| elif args.ssid: | |
| ssid = args.ssid | |
| password = args.password | |
| security = args.security | |
| dev = find_printer() | |
| print(f"\nFound printer: {usb.util.get_string(dev, dev.iProduct)}") | |
| print(f"Serial number: {usb.util.get_string(dev, dev.iSerialNumber)}") | |
| claim_ews_interface(dev) | |
| try: | |
| if args.status: | |
| get_status(dev) | |
| else: | |
| set_wifi(dev, ssid, password, security) | |
| finally: | |
| release_ews_interface(dev) | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This script was born out of frustration with HP's WiFi setup tooling on Linux and macOS:
Why is this so hard?
With only HP's own tools, WPS is the only realistic option on modern macOS:
openEWS_LEDM()failure is never checked, causing an infinite block on the first HTTP read. The underlying cause ofopenEWS_LEDM()returningHPMUD_R_INVALID_STATEwas not fully identified.So unless your router has WPS, you're stuck, unless you use this proxy to access the EWS (embedded web server) over USB, enable Wireless Direct, then configure WiFi from your browser.
This script talks directly to the printer's HP EWS/LEDM HTTP interface over USB bulk endpoints using pyusb.
Tested on HP LaserJet P1102w, Ubuntu 26.04 (VM) and macOS Tahoe. Should work on most HP LaserJet/OfficeJet/Photosmart from ~2009 onwards — see
--help-adaptfor instructions.