mirror of
https://github.com/NaitLee/Cat-Printer.git
synced 2025-05-18 00:00:18 -07:00
Take IPP support back, update doc, few other thing
This commit is contained in:
parent
2f4e111419
commit
1768c5055e
25
README.md
25
README.md
@ -1,4 +1,4 @@
|
|||||||
English | [Deutsch](./README.de_DE.md) | [简体中文](./README.zh_CN.md)
|
English | [Deutsch](./readme.i18n/README.de_DE.md) | [简体中文](./readme.i18n/README.zh_CN.md)
|
||||||
|
|
||||||
# Cat-Printer
|
# Cat-Printer
|
||||||
|
|
||||||
@ -16,22 +16,37 @@ Currently:
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
*Currently it's in development. More will be here soon!*
|
||||||
|
|
||||||
|
| Available | Partial | Planned |
|
||||||
|
|-----------------|-----------|---------------|
|
||||||
|
| Web Interface | CUPS/IPP* | Visual Editor |
|
||||||
|
| Print a Picture | | Help/Manual |
|
||||||
|
| Command-line | | Text Printing |
|
||||||
|
|
||||||
|
<!-- May comment the line below if there are no * -->
|
||||||
|
\* In development code. Will be released in a short period.
|
||||||
|
|
||||||
|
*Along with...*
|
||||||
|
|
||||||
- Simple!
|
- Simple!
|
||||||
- Operate via a Web UI just in browser,
|
- Operate via a Web UI just in browser,
|
||||||
- or get the Android release!
|
- or get the Android release!
|
||||||
- ~~Feature-rich~~
|
|
||||||
- Currently it's in development. More will be there soon!
|
|
||||||
- Friendly!
|
- Friendly!
|
||||||
- Language support! You can participate in translation!
|
- Language support! You can participate in translation!
|
||||||
- Good user interface, adaptive to PC/mobile and light/dark theme!
|
- Good user interface, adaptive to PC/mobile and light/dark theme!
|
||||||
|
|
||||||
- Cross platform!
|
- Cross platform!
|
||||||
- Newer Windows 10 and above
|
- Newer Windows 10 and above
|
||||||
- GNU/Linux
|
- GNU/Linux
|
||||||
- MacOS *(Needs testing)*
|
- MacOS *(Needs testing)*
|
||||||
- and a lot of extra efforts for Android!
|
- and a lot of extra efforts for Android!
|
||||||
|
|
||||||
- Free, as in [freedom](https://www.gnu.org/philosophy/free-sw.html)!
|
- Free, as in [freedom](https://www.gnu.org/philosophy/free-sw.html)!
|
||||||
- Unlike the "original" proprietary app,
|
- Unlike the "original" proprietary app,
|
||||||
this project is for everyone that concerns *open-mind and freedom*!
|
this project is for everyone that concerns *open-mind and freedom*!
|
||||||
|
|
||||||
- and Fun!
|
- and Fun!
|
||||||
- Do whatever you like!
|
- Do whatever you like!
|
||||||
|
|
||||||
@ -99,7 +114,7 @@ See file `COPYING`, `LICENSE`, and detail of used JavaScript in file `www/jslice
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
You may interested in language support, anyway. See the translation files in directory `www/lang`!
|
You may interested in language support, anyway. See the translation files in directory `www/lang` and `readme.i18n`!
|
||||||
Note: you can correct some mistakes in them, if there are any. Also feel free to make it (truly) better!
|
Note: you can correct some mistakes in them, if there are any. Also feel free to make it (truly) better!
|
||||||
|
|
||||||
Also interested in code development? See [development.md](development.md)!
|
Also interested in code development? See [development.md](development.md)!
|
||||||
@ -111,5 +126,5 @@ Also interested in code development? See [development.md](development.md)!
|
|||||||
- [roddeh-i18n](https://github.com/roddeh/i18njs), good work!
|
- [roddeh-i18n](https://github.com/roddeh/i18njs), good work!
|
||||||
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/), though there are some painful troubles
|
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/), though there are some painful troubles
|
||||||
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) for saving my life from Java
|
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) for saving my life from Java
|
||||||
- Stack Overflow & the whole Internet, you let me know Android `Activity` all from empty
|
- Stack Overflow & the whole Internet, you let me know Android `Activity` all from beginning
|
||||||
- ... Everyone is Awesome!
|
- ... Everyone is Awesome!
|
||||||
|
12
TODO
12
TODO
@ -1,8 +1,16 @@
|
|||||||
|
|
||||||
|
Note: not ordered. do whatever I/you want
|
||||||
|
|
||||||
|
+ Check GB03, again
|
||||||
+ Hacky text printing, typewriter-like, with PF2 font
|
+ Hacky text printing, typewriter-like, with PF2 font
|
||||||
+ Better MTU/freq moderation
|
ok I won't forget frontend, but it's different
|
||||||
+ Better CLI, e.g. invoke imagemagick if input is not PBM
|
+ Better CLI, e.g. invoke imagemagick if input is not PBM
|
||||||
+ (Re-)support CUPS & IPP
|
+ Consider better MTU adaption
|
||||||
|
+ Consider better BLE traffic regulation, with BLE notification
|
||||||
|
+ Consider the printer 'text' mode, it can make things faster
|
||||||
|
+ Consider more control to something like 'energy'
|
||||||
|
+ Consider keeping server backend (more) secure to be used by IPP
|
||||||
|
+ Clean up CUPS/IPP code
|
||||||
+ Make a build guide for android:
|
+ Make a build guide for android:
|
||||||
Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
|
Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
|
||||||
+ More frontend usability, more functions
|
+ More frontend usability, more functions
|
||||||
|
0
additional/__init__.py
Normal file
0
additional/__init__.py
Normal file
@ -26,10 +26,12 @@ class I18n():
|
|||||||
self.load_file(os.path.join(search_path, name))
|
self.load_file(os.path.join(search_path, name))
|
||||||
|
|
||||||
def load_file(self, name):
|
def load_file(self, name):
|
||||||
|
'Load an i18n json file'
|
||||||
with open(name, 'r', encoding='utf-8') as file:
|
with open(name, 'r', encoding='utf-8') as file:
|
||||||
self.load_data(file.read())
|
self.load_data(file.read())
|
||||||
|
|
||||||
def load_data(self, raw_json):
|
def load_data(self, raw_json):
|
||||||
|
'Load i18n json data (from str)'
|
||||||
data = json.loads(raw_json)
|
data = json.loads(raw_json)
|
||||||
for key in data['values']:
|
for key in data['values']:
|
||||||
self.data['values'][key] = data['values'][key]
|
self.data['values'][key] = data['values'][key]
|
||||||
@ -60,5 +62,5 @@ class I18n():
|
|||||||
if string is None:
|
if string is None:
|
||||||
string = data
|
string = data
|
||||||
for j in i:
|
for j in i:
|
||||||
string = string.replace('%%{%s}' % j, i[j])
|
string = string.replace(f'%%{j}', i[j])
|
||||||
return string
|
return string
|
100
additional/ipp.py
Normal file
100
additional/ipp.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
''' Provide *very* basic CUPS/IPP support
|
||||||
|
Extracted from version 0.0.2, do more cleaning later...
|
||||||
|
'''
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
class IPP():
|
||||||
|
'https://datatracker.ietf.org/doc/html/rfc8010'
|
||||||
|
server = None
|
||||||
|
printer = None
|
||||||
|
def __init__(self, server, printer):
|
||||||
|
self.server = server
|
||||||
|
self.printer = printer
|
||||||
|
async def handle_ipp(self, data):
|
||||||
|
'Handle an IPP protocol request'
|
||||||
|
server = self.server
|
||||||
|
# len_data = len(data)
|
||||||
|
# ipp_version_number = data[0:2]
|
||||||
|
# ipp_operation_id = data[2:4]
|
||||||
|
# ipp_request_id = data[4:8]
|
||||||
|
ipp_operation_attributes_tag = data[8]
|
||||||
|
attributes = {}
|
||||||
|
data_to_print = b''
|
||||||
|
# this is silly. i want to use io.BytesIO
|
||||||
|
if ipp_operation_attributes_tag == 0x01:
|
||||||
|
pointer = 9
|
||||||
|
next_name_length_at = 10
|
||||||
|
next_value_length_at = 10
|
||||||
|
name = b''
|
||||||
|
value = b''
|
||||||
|
while data[pointer] != 0x03:
|
||||||
|
tag = data[pointer:pointer + 1]
|
||||||
|
pointer += 1
|
||||||
|
if tag[0] < 0x10: # delimiter-tag
|
||||||
|
continue
|
||||||
|
next_name_length_at = pointer + data[pointer] * 0x0100 + data[pointer + 1] + 2
|
||||||
|
pointer += 2
|
||||||
|
while pointer < next_name_length_at:
|
||||||
|
name = name + data[pointer:pointer + 1]
|
||||||
|
pointer += 1
|
||||||
|
next_value_length_at = pointer + data[pointer] * 0x0100 + data[pointer + 1] + 2
|
||||||
|
pointer += 2
|
||||||
|
while pointer < next_value_length_at:
|
||||||
|
value = value + data[pointer:pointer + 1]
|
||||||
|
pointer += 1
|
||||||
|
attributes[name] = (tag, value)
|
||||||
|
name = b''
|
||||||
|
value = b''
|
||||||
|
pointer += 1
|
||||||
|
data_to_print = data[pointer:]
|
||||||
|
# there are hard coded minimal response. this "just works" on cups
|
||||||
|
if data_to_print == b'':
|
||||||
|
try:
|
||||||
|
server.send_response(200)
|
||||||
|
server.send_header('Content-Type', 'application/ipp')
|
||||||
|
server.end_headers()
|
||||||
|
server.wfile.write(
|
||||||
|
b'\x01\x01\x00\x00\x00\x00\x00\x01\x01\x03'
|
||||||
|
)
|
||||||
|
except BrokenPipeError:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
platform_system = platform.system()
|
||||||
|
# https://ghostscript.com/doc/9.54.0/Use.htm#Output_device
|
||||||
|
if platform_system == 'Windows':
|
||||||
|
gsexe = 'gswin32c.exe'
|
||||||
|
elif platform_system == 'OS/2':
|
||||||
|
gsexe = 'gsos2'
|
||||||
|
else:
|
||||||
|
gsexe = 'gs'
|
||||||
|
gsproc = subprocess.Popen([
|
||||||
|
gsexe,
|
||||||
|
'-q', '-sDEVICE=pbmraw', '-dNOPAUSE', '-dBATCH', '-dSAFER',
|
||||||
|
'-dFIXEDMEDIA', '-g384x543', '-r46.4441219158x46.4441219158',
|
||||||
|
'-dFitPage', '-dFitPage',
|
||||||
|
'-sOutputFile=-', '-'
|
||||||
|
], executable=gsexe, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
pbm_data, _ = gsproc.communicate(data_to_print)
|
||||||
|
try:
|
||||||
|
if gsproc.wait() == 0:
|
||||||
|
info = server.path[1:]
|
||||||
|
is_found = await server.printer.filter_device(info, server.settings.scan_time)
|
||||||
|
if not is_found:
|
||||||
|
... # TODO: Make IPP can report some errors
|
||||||
|
raise Exception(f'No printer found with info: {info}')
|
||||||
|
await server.printer.print_data(pbm_data)
|
||||||
|
else:
|
||||||
|
raise Exception('Error on invoking Ghostscript')
|
||||||
|
server.send_response(200)
|
||||||
|
server.send_header('Content-Type', 'application/ipp')
|
||||||
|
server.end_headers()
|
||||||
|
server.wfile.write(
|
||||||
|
b'\x01\x01\x00\x00\x00\x00\x00\x01\x01\x03'
|
||||||
|
)
|
||||||
|
except Exception as _:
|
||||||
|
server.send_response(500)
|
||||||
|
server.send_header('Content-Type', 'application/ipp')
|
||||||
|
server.end_headers()
|
||||||
|
server.wfile.write(b'')
|
104
printer.py
104
printer.py
@ -9,11 +9,14 @@ from bleak import BleakClient, BleakScanner
|
|||||||
from bleak.exc import BleakError, BleakDBusError
|
from bleak.exc import BleakError, BleakDBusError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from i18n import I18n
|
from additional.i18n import I18n
|
||||||
except ImportError:
|
except ImportError:
|
||||||
class I18n():
|
class I18n():
|
||||||
|
'Dummy i18n in case "full" version is missing'
|
||||||
|
|
||||||
def __init__(self, _search_path=None, _lang=None, _fallback=None):
|
def __init__(self, _search_path=None, _lang=None, _fallback=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __getitem__(self, keys):
|
def __getitem__(self, keys):
|
||||||
if not isinstance(keys, tuple):
|
if not isinstance(keys, tuple):
|
||||||
keys = (keys, )
|
keys = (keys, )
|
||||||
@ -21,9 +24,11 @@ except ImportError:
|
|||||||
|
|
||||||
i18n = I18n('www/lang')
|
i18n = I18n('www/lang')
|
||||||
|
|
||||||
|
|
||||||
class PrinterError(Exception):
|
class PrinterError(Exception):
|
||||||
'Error of Printer driver'
|
'Error of Printer driver'
|
||||||
|
|
||||||
|
|
||||||
models = ('GT01', 'GB01', 'GB02', 'GB03')
|
models = ('GT01', 'GB01', 'GB02', 'GB03')
|
||||||
|
|
||||||
crc8_table = [
|
crc8_table = [
|
||||||
@ -75,7 +80,10 @@ def reverse_binary(value: int):
|
|||||||
return int(f"{bin(value)[2:]:0>8}"[::-1], 2)
|
return int(f"{bin(value)[2:]:0>8}"[::-1], 2)
|
||||||
|
|
||||||
|
|
||||||
def make_command(command: int, payload: Union[bytes, bytearray], *, prefix: List[int]=None) -> bytearray:
|
def make_command(
|
||||||
|
command: int, payload: Union[bytes, bytearray], *,
|
||||||
|
prefix: List[int] = None
|
||||||
|
) -> bytearray:
|
||||||
'Make a `bytearray` with command data, which can be sent to printer directly to operate'
|
'Make a `bytearray` with command data, which can be sent to printer directly to operate'
|
||||||
if len(payload) > 0x100:
|
if len(payload) > 0x100:
|
||||||
raise Exception('Too large payload')
|
raise Exception('Too large payload')
|
||||||
@ -164,40 +172,48 @@ class PrinterDriver():
|
|||||||
def _read_pbm(self, path: str = None, data: bytes = None):
|
def _read_pbm(self, path: str = None, data: bytes = None):
|
||||||
if path is not None and path != '-':
|
if path is not None and path != '-':
|
||||||
file = open(path, 'rb')
|
file = open(path, 'rb')
|
||||||
elif data is not None:
|
data = file.read()
|
||||||
file = io.BytesIO(data)
|
|
||||||
else:
|
|
||||||
file = sys.stdin.buffer
|
|
||||||
signature = file.readline()
|
|
||||||
if signature != b'P4\n':
|
|
||||||
raise Exception('Specified file is not a PBM image')
|
|
||||||
width, height = self.standard_width, 0
|
|
||||||
while True:
|
|
||||||
# There can be comments. Skip them
|
|
||||||
line = file.readline()[0:-1]
|
|
||||||
if line[0:1] != b'#':
|
|
||||||
break
|
|
||||||
width, height = [int(x) for x in line.split(b' ')[0:2]]
|
|
||||||
if width != self.standard_width:
|
|
||||||
raise Exception('PBM image width is not 384px')
|
|
||||||
expected_data_size = self.pbm_data_per_line * height
|
|
||||||
data = file.read()
|
|
||||||
if path is not None and path != '-':
|
|
||||||
file.close()
|
file.close()
|
||||||
data_size = len(data)
|
elif data is not None:
|
||||||
if data_size != expected_data_size:
|
pass
|
||||||
raise Exception('Broken PBM file data')
|
else:
|
||||||
if self.dry_run:
|
data = sys.stdin.buffer.read()
|
||||||
# Dry run: put blank data
|
if data[0:3] != b'P4\n':
|
||||||
data = b'\x00' * expected_data_size
|
raise Exception('Specified file is not a PBM image')
|
||||||
return PBMData(width, height, data)
|
# there can be several "chunks", by e.g. cat-ing several files, or ghostscript output
|
||||||
|
chunks = data.split(b'P4\n')[1:]
|
||||||
|
result = b''
|
||||||
|
total_height = 0
|
||||||
|
for chunk in chunks:
|
||||||
|
page = io.BytesIO(chunk)
|
||||||
|
while True:
|
||||||
|
# There can be comments. Skip them
|
||||||
|
line = page.readline()[0:-1]
|
||||||
|
if line[0:1] != b'#':
|
||||||
|
break
|
||||||
|
width, height = [int(x) for x in line.split(b' ')[0:2]]
|
||||||
|
if width != self.standard_width:
|
||||||
|
raise Exception('PBM image width is not 384px')
|
||||||
|
total_height += height
|
||||||
|
expected_data_size = self.pbm_data_per_line * height
|
||||||
|
raw_data = page.read()
|
||||||
|
data_size = len(raw_data)
|
||||||
|
if data_size != expected_data_size:
|
||||||
|
raise Exception('Broken PBM file data')
|
||||||
|
if self.dry_run:
|
||||||
|
# Dry run: put blank data
|
||||||
|
result += b'\x00' * expected_data_size
|
||||||
|
else:
|
||||||
|
result += raw_data
|
||||||
|
return PBMData(self.standard_width, total_height, result)
|
||||||
|
|
||||||
def _pbm_data_to_raw(self, data: PBMData):
|
def _pbm_data_to_raw(self, data: PBMData):
|
||||||
buffer = bytearray()
|
buffer = bytearray()
|
||||||
# new/old print command
|
# new/old print command
|
||||||
if self.name == 'GB03':
|
if self.name == 'GB03':
|
||||||
buffer.append(0x12)
|
buffer.append(0x12)
|
||||||
buffer += bytearray([0x51, 0x78, 0xa3, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff])
|
buffer += bytearray([0x51, 0x78, 0xa3, 0x00,
|
||||||
|
0x01, 0x00, 0x00, 0x00, 0xff])
|
||||||
for key in data.args:
|
for key in data.args:
|
||||||
buffer += make_command(key, data.args[key])
|
buffer += make_command(key, data.args[key])
|
||||||
buffer += make_command(
|
buffer += make_command(
|
||||||
@ -236,7 +252,7 @@ class PrinterDriver():
|
|||||||
)
|
)
|
||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
async def send_buffer(self, buffer: bytearray, address: str=None):
|
async def send_buffer(self, buffer: bytearray, address: str = None):
|
||||||
'Send manipulation data (buffer) to the printer via bluetooth'
|
'Send manipulation data (buffer) to the printer via bluetooth'
|
||||||
address = address or self.address
|
address = address or self.address
|
||||||
client = BleakClient(address, timeout=5.0)
|
client = BleakClient(address, timeout=5.0)
|
||||||
@ -267,6 +283,7 @@ class PrinterDriver():
|
|||||||
if device.name in models:
|
if device.name in models:
|
||||||
result.append(device)
|
result.append(device)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def search_printer(self, timeout: int):
|
async def search_printer(self, timeout: int):
|
||||||
'Search for a printer, returns `None` if not found'
|
'Search for a printer, returns `None` if not found'
|
||||||
timeout = timeout or 3
|
timeout = timeout or 3
|
||||||
@ -275,20 +292,40 @@ class PrinterDriver():
|
|||||||
return devices[0]
|
return devices[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def print_file(self, path: str, address: str=None):
|
async def print_file(self, path: str, address: str = None):
|
||||||
'Method to print the specified PBM image at `path` with printer at specified MAC `address`'
|
'Method to print the specified PBM image at `path` with printer at specified MAC `address`'
|
||||||
address = address or self.address
|
address = address or self.address
|
||||||
pbm_data = self._read_pbm(path)
|
pbm_data = self._read_pbm(path)
|
||||||
buffer = self._pbm_data_to_raw(pbm_data)
|
buffer = self._pbm_data_to_raw(pbm_data)
|
||||||
await self.send_buffer(buffer, address)
|
await self.send_buffer(buffer, address)
|
||||||
|
|
||||||
async def print_data(self, data: bytes, address: str=None):
|
async def print_data(self, data: bytes, address: str = None):
|
||||||
'Method to print the specified PBM image `data` with printer at specified MAC `address`'
|
'Method to print the specified PBM image `data` with printer at specified MAC `address`'
|
||||||
address = address or self.address
|
address = address or self.address
|
||||||
pbm_data = self._read_pbm(None, data)
|
pbm_data = self._read_pbm(None, data)
|
||||||
buffer = self._pbm_data_to_raw(pbm_data)
|
buffer = self._pbm_data_to_raw(pbm_data)
|
||||||
await self.send_buffer(buffer, address)
|
await self.send_buffer(buffer, address)
|
||||||
|
|
||||||
|
async def filter_device(self, info: str, timeout: float = 5.0) -> bool:
|
||||||
|
'Find a suitable device with `info`: Bluetooth name or MAC address, or empty string'
|
||||||
|
devices = await self.search_all_printers(timeout)
|
||||||
|
if len(devices) == 0:
|
||||||
|
return False
|
||||||
|
if info in models:
|
||||||
|
for device in devices:
|
||||||
|
if device.name == info:
|
||||||
|
self.name, self.address = device.name, device.address
|
||||||
|
break
|
||||||
|
elif info[2::3] == ':::::':
|
||||||
|
for device in devices:
|
||||||
|
if device.address.lower() == info.lower():
|
||||||
|
self.name, self.address = device.name, device.address
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
device = devices[0]
|
||||||
|
self.name, self.address = device.name, device.address
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _main():
|
async def _main():
|
||||||
'Main routine for direct command line execution'
|
'Main routine for direct command line execution'
|
||||||
@ -335,6 +372,7 @@ async def _main():
|
|||||||
await printer.print_file(cmdargs.file)
|
await printer.print_file(cmdargs.file)
|
||||||
print(i18n['finished'])
|
print(i18n['finished'])
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
'Run the `_main` routine while catching exceptions'
|
'Run the `_main` routine while catching exceptions'
|
||||||
try:
|
try:
|
||||||
@ -344,7 +382,7 @@ async def main():
|
|||||||
if (
|
if (
|
||||||
'not turned on' in error_message or
|
'not turned on' in error_message or
|
||||||
(isinstance(e, BleakDBusError) and
|
(isinstance(e, BleakDBusError) and
|
||||||
getattr(e, 'dbus_error') == 'org.bluez.Error.NotReady')
|
getattr(e, 'dbus_error') == 'org.bluez.Error.NotReady')
|
||||||
):
|
):
|
||||||
print(i18n['please-enable-bluetooth'])
|
print(i18n['please-enable-bluetooth'])
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -15,21 +15,36 @@ Gegenwärtig:
|
|||||||
|
|
||||||
## Funktionen
|
## Funktionen
|
||||||
|
|
||||||
|
*Derzeit befindet sich die Software im Alpha-Stadium. Mehr wird es bald geben!*
|
||||||
|
|
||||||
|
| Available | Partial | Planned |
|
||||||
|
|-----------------|-----------|---------------|
|
||||||
|
| Web Interface | CUPS/IPP* | Visual Editor |
|
||||||
|
| Print a Picture | | Help/Manual |
|
||||||
|
| Command-line | | Text Printing |
|
||||||
|
|
||||||
|
<!-- May comment the line below if there are none -->
|
||||||
|
\* In development code. Will be released in a short period.
|
||||||
|
|
||||||
|
*Along with…*
|
||||||
|
|
||||||
- Simple!
|
- Simple!
|
||||||
- Bedienung über eine Web-UI direkt im Browser,
|
- Bedienung über eine Web-UI direkt im Browser,
|
||||||
- oder besorgen Sie sich die Android-Version!
|
- oder besorgen Sie sich die Android-Version!
|
||||||
- ~~Umfangreiche Funktionen~~
|
|
||||||
- Derzeit befindet sich die Software im Alpha-Stadium. Mehr wird es bald geben!
|
|
||||||
- Friendly!
|
- Friendly!
|
||||||
- Sprachunterstützung! Sie können sich an der Übersetzung beteiligen!
|
- Sprachunterstützung! Sie können sich an der Übersetzung beteiligen!
|
||||||
- Gute Benutzeroberfläche, mit PC-/Mobil-/Licht-/Dunkelmodus-Varianten! (Systemkonfiguration adaptiv)
|
- Gute Benutzeroberfläche, mit PC-/Mobil-/Licht-/Dunkelmodus-Varianten! (Systemkonfiguration adaptiv)
|
||||||
|
|
||||||
- Plattformübergreifend!
|
- Plattformübergreifend!
|
||||||
- Neuere Windows 10 und darüber
|
- Neuere Windows 10 und darüber
|
||||||
- GNU/Linux
|
- GNU/Linux
|
||||||
- MacOS *(muss getestet werden*
|
- MacOS *(muss getestet werden*
|
||||||
- und eine Menge zusätzlicher Anstrengungen für Android!
|
- und eine Menge zusätzlicher Anstrengungen für Android!
|
||||||
|
|
||||||
- Frei, wie in [freedom](https://www.gnu.org/philosophy/free-sw.html)!
|
- Frei, wie in [freedom](https://www.gnu.org/philosophy/free-sw.html)!
|
||||||
- Anders als die "offizielle" proprietäre App, ist dieses Projekt für alle, denen *offener Geist und Freiheit* wichtig sind!
|
- Anders als die "offizielle" proprietäre App, ist dieses Projekt für alle, denen *offener Geist und Freiheit* wichtig sind!
|
||||||
|
|
||||||
- und Fun!
|
- und Fun!
|
||||||
- Mach, was du willst!
|
- Mach, was du willst!
|
||||||
|
|
||||||
@ -92,7 +107,7 @@ Siehe Datei `COPYING`, `LICENSE` und Details zum verwendeten JavaScript in der D
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Vielleicht sind Sie ohnehin an der Sprachunterstützung interessiert. Siehe die Übersetzungsdateien im Verzeichnis `www/lang`!
|
Vielleicht sind Sie ohnehin an der Sprachunterstützung interessiert. Siehe die Übersetzungsdateien im Verzeichnis `www/lang` und `readme.i18n`!
|
||||||
|
|
||||||
Interessieren Sie sich auch für Code-Entwicklung? Siehe [development.md](development.md)!
|
Interessieren Sie sich auch für Code-Entwicklung? Siehe [development.md](development.md)!
|
||||||
|
|
@ -15,21 +15,36 @@
|
|||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
|
*当前仍在继续开发。以后会有更多!*
|
||||||
|
|
||||||
|
| 可用 | 部分 | 计划 |
|
||||||
|
|-----------|-----------|---------------|
|
||||||
|
| 网页界面 | CUPS/IPP* | 可视化编辑器 |
|
||||||
|
| 打印图片 | | 帮助/文档 |
|
||||||
|
| 命令行 | | 文本打印 |
|
||||||
|
|
||||||
|
<!-- 若没有 *,可注释下一句 -->
|
||||||
|
\* 存在于开发代码中。将在短时间内发布。
|
||||||
|
|
||||||
|
*当然还有……*
|
||||||
|
|
||||||
- 简易!
|
- 简易!
|
||||||
- 在网页界面进行操作,
|
- 在网页界面进行操作,
|
||||||
- 或者获取安卓应用!
|
- 或者获取安卓应用!
|
||||||
- ~~功能丰富~~
|
|
||||||
- 当前仍在继续开发。以后会有更多!
|
|
||||||
- 友好!
|
- 友好!
|
||||||
- 语言支持!您可参与翻译!
|
- 语言支持!您可参与翻译!
|
||||||
- 良好的用户界面,可适应桌面/移动端/明暗主题!
|
- 良好的用户界面,可适应桌面/移动端/明暗主题!
|
||||||
|
|
||||||
- 跨平台!
|
- 跨平台!
|
||||||
- 较新的 Windows 10 及以上
|
- 较新的 Windows 10 及以上
|
||||||
- GNU/Linux
|
- GNU/Linux
|
||||||
- MacOS *(需要测试)*
|
- MacOS *(需要测试)*
|
||||||
- 在安卓上也花了些功夫呢!
|
- 在安卓上也花了些功夫呢!
|
||||||
|
|
||||||
- 是[自由软件](https://www.gnu.org/philosophy/free-sw.html)!
|
- 是[自由软件](https://www.gnu.org/philosophy/free-sw.html)!
|
||||||
- 不像“原版”专有应用,此作品为在乎*开放思想与计算自由*的人而生!
|
- 不像“原版”专有应用,此作品为在乎*开放思想与计算自由*的人而生!
|
||||||
|
|
||||||
- 有意思!
|
- 有意思!
|
||||||
- 做什么都可以!
|
- 做什么都可以!
|
||||||
|
|
||||||
@ -97,19 +112,20 @@ Copyright © 2022 NaitLee Soft. 保留一些权利。
|
|||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
您可能对翻译工作感兴趣。可于目录 `www/lang` 中查看翻译文件!
|
您可能对翻译工作感兴趣。可于目录 `www/lang` 和 `readme.i18n` 中查看翻译文件!
|
||||||
|
|
||||||
注:
|
注:
|
||||||
1. 通常英语与简体中文同时更新。请考虑其他,如繁体中文(需注意在繁体中与简体的用字、技术术语差别)。
|
1. 通常英语与简体中文同时更新。请考虑其他,如繁体中文(需注意在繁体中与简体的用字、技术术语差别)。
|
||||||
2. 如果您有(真的)能力,您也可以纠正/改善某些翻译!
|
2. 如果(真的)有能力,您也可以纠正/改善某些翻译!
|
||||||
|
|
||||||
还想写代码?看看 [development.md](development.md)!(英文)
|
还想写代码?看看 [development.md](development.md)!(英文)
|
||||||
|
|
||||||
### 鸣谢
|
### 鸣谢
|
||||||
|
|
||||||
- 当然不能没有 Python 和 Web 技术!
|
- 当然不能没有 Python 和 Web 技术!
|
||||||
- [Bleak](https://bleak.readthedocs.io/en/latest/) 蓝牙低功耗库,牛!
|
- [Bleak](https://bleak.readthedocs.io/en/latest/) 跨平台蓝牙低功耗库,牛!
|
||||||
- [roddeh-i18n](https://github.com/roddeh/i18njs),好活!
|
- [roddeh-i18n](https://github.com/roddeh/i18njs),很好!
|
||||||
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/),虽然有些麻烦的地方
|
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/),虽然有些麻烦的地方
|
||||||
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) 从 Java 拯救了我的生命
|
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) 从 Java 拯救了我的生命
|
||||||
- Stack Overflow 和互联网,你们让我无中生有地了解了安卓“活动” `Activity`
|
- Stack Overflow 和整个互联网,你们让我从零开始了解了安卓“活动” `Activity`
|
||||||
- ……每个人都是好样的!
|
- ……每个人都是好样的!
|
40
server.py
40
server.py
@ -34,9 +34,6 @@ class PrinterServerError(Exception):
|
|||||||
if len_args > 1:
|
if len_args > 1:
|
||||||
self.details = args[1]
|
self.details = args[1]
|
||||||
|
|
||||||
Printer = PrinterDriver()
|
|
||||||
server = None
|
|
||||||
|
|
||||||
def log(message):
|
def log(message):
|
||||||
'For logging a message'
|
'For logging a message'
|
||||||
print(message)
|
print(message)
|
||||||
@ -66,6 +63,8 @@ class PrinterServer(BaseHTTPRequestHandler):
|
|||||||
'frequency': 0.8,
|
'frequency': 0.8,
|
||||||
'dry_run': False
|
'dry_run': False
|
||||||
})
|
})
|
||||||
|
printer = PrinterDriver()
|
||||||
|
ipp = None
|
||||||
def log_request(self, _code=200, _size=0):
|
def log_request(self, _code=200, _size=0):
|
||||||
pass
|
pass
|
||||||
def log_error(self, *_args):
|
def log_error(self, *_args):
|
||||||
@ -126,22 +125,23 @@ class PrinterServer(BaseHTTPRequestHandler):
|
|||||||
'Save config file'
|
'Save config file'
|
||||||
with open(self.settings.config_path, 'w', encoding='utf-8') as file:
|
with open(self.settings.config_path, 'w', encoding='utf-8') as file:
|
||||||
json.dump(self.settings, file, indent=4)
|
json.dump(self.settings, file, indent=4)
|
||||||
|
def update_printer(self):
|
||||||
|
'Update `PrinterDriver` state/config'
|
||||||
|
self.printer.dry_run = self.settings.dry_run
|
||||||
|
self.printer.frequency = float(self.settings.frequency)
|
||||||
|
if self.settings.printer is not None:
|
||||||
|
self.printer.name, self.printer.address = self.settings.printer.split(',')
|
||||||
def handle_api(self):
|
def handle_api(self):
|
||||||
'Handle API request from POST'
|
'Handle API request from POST'
|
||||||
content_length = int(self.headers.get('Content-Length'))
|
content_length = int(self.headers.get('Content-Length'))
|
||||||
body = self.rfile.read(content_length)
|
body = self.rfile.read(content_length)
|
||||||
api = self.path[1:]
|
api = self.path[1:]
|
||||||
if api == 'print':
|
if api == 'print':
|
||||||
if self.settings.printer is None:
|
self.update_printer()
|
||||||
# usually can't encounter, though
|
|
||||||
raise PrinterServerError('No printer address specified')
|
|
||||||
Printer.dry_run = self.settings.dry_run
|
|
||||||
Printer.frequency = float(self.settings.frequency)
|
|
||||||
Printer.name, Printer.address = self.settings.printer.split(',')
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
try:
|
try:
|
||||||
devices = loop.run_until_complete(
|
devices = loop.run_until_complete(
|
||||||
Printer.print_data(body)
|
self.printer.print_data(body)
|
||||||
)
|
)
|
||||||
self.api_success()
|
self.api_success()
|
||||||
finally:
|
finally:
|
||||||
@ -152,7 +152,7 @@ class PrinterServer(BaseHTTPRequestHandler):
|
|||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
try:
|
try:
|
||||||
devices = loop.run_until_complete(
|
devices = loop.run_until_complete(
|
||||||
Printer.search_all_printers(float(self.settings.scan_time))
|
self.printer.search_all_printers(float(self.settings.scan_time))
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
loop.close()
|
loop.close()
|
||||||
@ -196,6 +196,23 @@ class PrinterServer(BaseHTTPRequestHandler):
|
|||||||
self.send_header('Content-Type', mime('txt'))
|
self.send_header('Content-Type', mime('txt'))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
return
|
return
|
||||||
|
if self.headers.get('Content-Type') == 'application/ipp':
|
||||||
|
if self.ipp is None:
|
||||||
|
try:
|
||||||
|
from additional.ipp import IPP
|
||||||
|
self.load_config()
|
||||||
|
except ImportError:
|
||||||
|
# TODO: Better response?
|
||||||
|
return
|
||||||
|
self.ipp = IPP(self, self.printer)
|
||||||
|
self.update_printer()
|
||||||
|
body = self.rfile.read(content_length)
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(self.ipp.handle_ipp(body))
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
self.handle_api()
|
self.handle_api()
|
||||||
return
|
return
|
||||||
@ -231,7 +248,6 @@ def serve():
|
|||||||
if '-a' in sys.argv:
|
if '-a' in sys.argv:
|
||||||
print('Will listen on ALL addresses')
|
print('Will listen on ALL addresses')
|
||||||
listen_all = True
|
listen_all = True
|
||||||
global server
|
|
||||||
# Again, Don't use ThreadingHTTPServer if you're going to use pyjnius!
|
# Again, Don't use ThreadingHTTPServer if you're going to use pyjnius!
|
||||||
server = HTTPServer(('' if listen_all else address, port), PrinterServer)
|
server = HTTPServer(('' if listen_all else address, port), PrinterServer)
|
||||||
service_url = f'http://{address}:{port}/'
|
service_url = f'http://{address}:{port}/'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user