From 0ebddac75877ae8868f94b678f39b5bb36124dc0 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Sat, 4 Mar 2023 20:49:37 +0100 Subject: [PATCH 1/6] Improving performance of ZRLE --- src/rmview/rfb.py | 201 ++++++++++++++++++++++++---------------------- 1 file changed, 107 insertions(+), 94 deletions(-) diff --git a/src/rmview/rfb.py b/src/rmview/rfb.py index ec8d413..3d7f1fa 100644 --- a/src/rmview/rfb.py +++ b/src/rmview/rfb.py @@ -119,47 +119,84 @@ def ord(x): return x KEY_SpaceBar= 0x0020 -# ZRLE helpers -def _zrle_next_bit(it, pixels_in_tile): - num_pixels = 0 - while True: - b = ord(next(it)) - - for n in range(8): - value = b >> (7 - n) - yield value & 1 - - num_pixels += 1 - if num_pixels == pixels_in_tile: - return - - -def _zrle_next_dibit(it, pixels_in_tile): - num_pixels = 0 - while True: - b = ord(next(it)) - - for n in range(0, 8, 2): - value = b >> (6 - n) - yield value & 3 - - num_pixels += 1 - if num_pixels == pixels_in_tile: - return - - -def _zrle_next_nibble(it, pixels_in_tile): - num_pixels = 0 - while True: - b = ord(next(it)) - - for n in range(0, 8, 4): - value = b >> (4 - n) - yield value & 15 +class ZRLEDataStream(): + + def __init__(self, data, bytes_per_pixel=1): + self._data = data + self._offset = 0 + self._bypp = bytes_per_pixel + + def __len__(self): + return len(self._data) - self._offset + + def nextByte(self): + b = ord(self._data[self._offset]) + self._offset += 1 + return b + + def nextChunk(self, length): + r = memoryview(self._data)[self._offset:self._offset+length] + self._offset += length + return r + + def nextPixel(self): + return self.nextChunk(self._bypp) + + def nextPixels(self, n=1): + return self.nextChunk(n*self._bypp) + + def nextRunLength(self): + run_length_next = self.nextByte() + run_length = run_length_next + while run_length_next == 255: + run_length_next = self.nextByte() + run_length += run_length_next + return run_length + 1 + + def nextPaletteIndices(self, palette_size, length=1): + if palette_size == 2: + return self._zrle_next_bit(length) + if palette_size == 3 or palette_size == 4: + return self._zrle_next_dibit(length) + else: + return self._zrle_next_nibble(length) + + def _zrle_next_bit(self, tot_pixels): + num_pixels = 0 + while num_pixels < tot_pixels: + b = self.nextByte() + for n in range(8): + value = b >> (7 - n) + yield value & 1 + + num_pixels += 1 + if num_pixels == tot_pixels: + return + + def _zrle_next_dibit(self, tot_pixels): + num_pixels = 0 + while num_pixels < tot_pixels: + b = self.nextByte() + for n in range(0, 8, 2): + value = b >> (6 - n) + yield value & 3 + + num_pixels += 1 + if num_pixels == tot_pixels: + return + + def _zrle_next_nibble(self, tot_pixels): + num_pixels = 0 + while num_pixels < tot_pixels: + b = self.nextByte() + for n in range(0, 8, 4): + value = b >> (4 - n) + yield value & 15 + + num_pixels += 1 + if num_pixels == tot_pixels: + return - num_pixels += 1 - if num_pixels == pixels_in_tile: - return class RFBClient(Protocol): @@ -586,22 +623,10 @@ def _handleDecodeZRLEdata(self, block, x, y, width, height): tx = x ty = y - data = self._zlib_stream.decompress(block) - it = iter(data) + data = ZRLEDataStream(self._zlib_stream.decompress(block), self.bypp) - def cpixel(i): - for j in range(self.bypp): - yield next(i) - # yield next(i) - # yield next(i) - # Alpha channel - # yield 0xff - - while True: - try: - subencoding = ord(next(it)) - except StopIteration: - break + while len(data) > 0: + subencoding = data.nextByte() # calc tile size tw = th = 64 @@ -613,71 +638,59 @@ def cpixel(i): pixels_in_tile = tw * th # decode next tile - num_pixels = 0 - pixel_data = bytearray() palette_size = subencoding & 127 if subencoding & 0x80: # RLE - - def do_rle(pixel): - run_length_next = ord(next(it)) - run_length = run_length_next - while run_length_next == 255: - run_length_next = ord(next(it)) - run_length += run_length_next - pixel_data.extend(pixel * (run_length + 1)) - return run_length + 1 + pixel_data = bytearray() if palette_size == 0: # plain RLE - while num_pixels < pixels_in_tile: - color = bytearray(cpixel(it)) - num_pixels += do_rle(color) - if num_pixels != pixels_in_tile: - raise ValueError("too many pixels") + while len(pixel_data) < pixels_in_tile: + color = bytes(data.nextPixel()) + run_len = data.nextRunLength() + pixel_data += color * run_len + if len(pixel_data) > pixels_in_tile: + raise ValueError("too many pixels") else: - palette = [bytearray(cpixel(it)) for p in range(palette_size)] + # Palette RLE + palette = [data.nextPixel() for _ in range(palette_size)] - while num_pixels < pixels_in_tile: - palette_index = ord(next(it)) + while len(pixel_data) < pixels_in_tile: + palette_index = data.nextByte() if palette_index & 0x80: palette_index &= 0x7F # run of length > 1, more bytes follow to determine run length - num_pixels += do_rle(palette[palette_index]) + run_len = data.nextRunLength() + pixel_data += palette[palette_index] * run_len else: # run of length 1 - pixel_data.extend(palette[palette_index]) - num_pixels += 1 - if num_pixels != pixels_in_tile: + pixel_data += palette[palette_index] + + if len(pixel_data) != pixels_in_tile: raise ValueError("too many pixels") - self.updateRectangle(tx, ty, tw, th, bytes(pixel_data)) + self.updateRectangle(tx, ty, tw, th, pixel_data) else: # No RLE if palette_size == 0: # Raw pixel data - pixel_data = b''.join(bytes(cpixel(it)) for _ in range(pixels_in_tile)) - self.updateRectangle(tx, ty, tw, th, bytes(pixel_data)) + pixel_data = data.nextPixels(pixels_in_tile) + self.updateRectangle(tx, ty, tw, th, pixel_data) elif palette_size == 1: # Fill tile with plain color - color = bytearray(cpixel(it)) - self.fillRectangle(tx, ty, tw, th, bytes(color)) + color = data.nextPixel() + self.fillRectangle(tx, ty, tw, th, color) else: if palette_size > 16: raise ValueError( "Palette of size {0} is not allowed".format(palette_size)) - palette = [bytearray(cpixel(it)) for _ in range(palette_size)] - if palette_size == 2: - next_index = _zrle_next_bit(it, pixels_in_tile) - elif palette_size == 3 or palette_size == 4: - next_index = _zrle_next_dibit(it, pixels_in_tile) - else: - next_index = _zrle_next_nibble(it, pixels_in_tile) - - for palette_index in next_index: - pixel_data.extend(palette[palette_index]) - self.updateRectangle(tx, ty, tw, th, bytes(pixel_data)) + palette = [data.nextPixel() for _ in range(palette_size)] + + pixel_data = bytearray() + for palette_index in data.nextPaletteIndices(palette_size, pixels_in_tile): + pixel_data += palette[palette_index] + self.updateRectangle(tx, ty, tw, th, pixel_data) # Next tile tx = tx + 64 From 23ef92fa0f88382ffc1ec8f44beff4ecf26fd992 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Wed, 5 Apr 2023 00:01:53 +0200 Subject: [PATCH 2/6] Avoid crash when triggering emulated keypresses before initialization --- src/rmview/rmview.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/rmview/rmview.py b/src/rmview/rmview.py index 2643c1e..fd1cb24 100644 --- a/src/rmview/rmview.py +++ b/src/rmview/rmview.py @@ -119,17 +119,17 @@ def __init__(self, args): ### self.leftAction = QAction('Emulate Left Button', self) self.leftAction.setShortcut('Ctrl+Left') - self.leftAction.triggered.connect(lambda: self.fbworker.keyEvent(KEY_Left)) + self.leftAction.triggered.connect(lambda: self.emulateKeyEvent(KEY_Left)) self.viewer.addAction(self.leftAction) ### self.rightAction = QAction('Emulate Right Button', self) self.rightAction.setShortcut('Ctrl+Right') - self.rightAction.triggered.connect(lambda: self.fbworker.keyEvent(KEY_Right)) + self.rightAction.triggered.connect(lambda: self.emulateKeyEvent(KEY_Right)) self.viewer.addAction(self.rightAction) ### self.homeAction = QAction('Emulate Central Button', self) - self.homeAction.setShortcut(QKeySequence.Cancel) - self.homeAction.triggered.connect(lambda: self.fbworker.keyEvent(KEY_Escape)) + # self.homeAction.setShortcut(QKeySequence.Cancel) + self.homeAction.triggered.connect(lambda: self.emulateKeyEvent(KEY_Escape)) self.viewer.addAction(self.homeAction) @@ -185,6 +185,10 @@ def __init__(self, args): self.aboutToQuit.connect(self.joinWorkers) self.requestConnect() + def emulateKeyEvent(self, key): + if self.fbworker: + self.fbworker.keyEvent(key) + def disableAutoOrientation(self): self.orient = 0 From 4a810267e7f9559ec1e0f85e552b0461e7a30ca6 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Wed, 5 Apr 2023 00:03:24 +0200 Subject: [PATCH 3/6] Exit fullscreen before cloning frames --- src/rmview/rmview.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/rmview/rmview.py b/src/rmview/rmview.py index fd1cb24..9a5a188 100644 --- a/src/rmview/rmview.py +++ b/src/rmview/rmview.py @@ -453,6 +453,7 @@ def movePen(self, x, y): @pyqtSlot() def cloneViewer(self): + self.viewer.showNormal() img = self.viewer.image() img.detach() v = QtImageViewer() @@ -461,6 +462,7 @@ def cloneViewer(self): v.show() v.rotate(self.viewer._rotation) self.cloned_frames.add(v) + v.setWindowTitle("Cloned frame %d" % len(self.cloned_frames)) v.destroyed.connect(lambda: self.cloned_frames.discard(v)) @pyqtSlot() From cb555f9371fd1999a276135f235ddbc446163c39 Mon Sep 17 00:00:00 2001 From: Kolton Yager Date: Mon, 20 Nov 2023 14:22:12 -0800 Subject: [PATCH 4/6] Fix for issue #146 The issue arises due to the reMarkable software version no longer being accurately reflected in `/etc/version`. This causes rmview to think it is connecting to an older version. I've fixed this by switching rmview's SW version logic over to the semantic versioning scheme used by recent releases. The SW version is taken from the `REMARKABLE_RELEASE_VERSION` value in `/usr/share/remarkable/update.conf`, as suggested by @Eeems. Support for older versions is to be maintained by mapping the older timestamp version numbers back to the corresponding semantic versions. --- src/rmview/connection.py | 18 +++++++++++++++++- src/rmview/rmparams.py | 12 ++++++++++++ src/rmview/rmview.py | 6 +++--- src/rmview/screenstream/screenshare.py | 2 +- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/rmview/connection.py b/src/rmview/connection.py index 7c92473..31b1a5b 100644 --- a/src/rmview/connection.py +++ b/src/rmview/connection.py @@ -2,6 +2,8 @@ from PyQt5.QtCore import * from PyQt5.QtWidgets import * +from .rmparams import timestamp_to_version + import paramiko import struct @@ -161,7 +163,20 @@ def _getVersion(self): def _getSwVersion(self): _, out, _ = self.client.exec_command("cat /etc/version") - return int(out.read().decode("utf-8")) + version_ts = int(out.read().decode("utf-8")) + version = timestamp_to_version(version_ts) + + try: + _, out, err = self.client.exec_command(r"grep 'REMARKABLE_RELEASE_VERSION=' /usr/share/remarkable/update.conf " + r"| sed -r 's/(REMARKABLE_RELEASE_VERSION=)([0-9a-zA-Z.]+)/\2/'") + config_version = tuple(int(v) for v in out.read().decode("utf-8").strip().split('.')) + if (len(config_version) < 4): config_version += (0,)*(len(config_version) - 4); + version = config_version + log.debug("Using update.conf as SW version authority") + except Exception as e: + log.debug("Using /etc/version as SW version authority") + + return version @pyqtSlot() @@ -179,6 +194,7 @@ def run(self): self.client.hostname = self.address self.client.deviceVersion, self.client.fullDeviceVersion = self._getVersion() self.client.softwareVersion = self._getSwVersion() + log.info("Detected SW version: {}".format('.'.join(str(v) for v in self.client.softwareVersion))) self.signals.onConnect.emit(self.client) except Exception as e: log.error("Could not connect to %s: %s", self.address, e) diff --git a/src/rmview/rmparams.py b/src/rmview/rmparams.py index 3bb972a..4fcd5e5 100644 --- a/src/rmview/rmparams.py +++ b/src/rmview/rmparams.py @@ -9,6 +9,18 @@ '2.9.1.236': 20210820111232 } +# This mapping was adapted from the above `SW_VER_TIMESTAMPS` dictionary. +# Newer versions do not use timestamp based versioning, and are likewise not represented here. +def timestamp_to_version(ts): + if ts < SW_VER_TIMESTAMPS["2.7"]: + return (2, 6, 0, 0) + elif ts < SW_VER_TIMESTAMPS["2.9"]: + return (2, 7, 0, 0) + elif ts < SW_VER_TIMESTAMPS["2.9.1.236"]: + return (2, 9, 0, 0) + else: + return (2, 9, 1, 236) + # evtype_sync = 0 e_type_key = 1 diff --git a/src/rmview/rmview.py b/src/rmview/rmview.py index 9a5a188..97d7353 100644 --- a/src/rmview/rmview.py +++ b/src/rmview/rmview.py @@ -306,7 +306,7 @@ def connected(self, ssh): self.ssh = ssh self.viewer.setWindowTitle("rMview - " + ssh.hostname) - log.info("Detected %s", ssh.fullDeviceVersion) + log.info("Detected device: %s", ssh.fullDeviceVersion) version = ssh.deviceVersion if version not in [1, 2]: log.error("Device is unsupported: '%s' [%s]", ssh.fullDeviceVersion, version or "unknown device") @@ -316,11 +316,11 @@ def connected(self, ssh): backend = self.config.get('backend', 'auto') if backend == 'auto': - if ssh.softwareVersion >= SW_VER_TIMESTAMPS['2.9']: + if ssh.softwareVersion >= (2, 9, 0, 0): backend = 'screenshare' else: backend = 'vncserver' - if ssh.softwareVersion >= SW_VER_TIMESTAMPS['2.7']: + if ssh.softwareVersion >= (2, 7, 0, 0): log.warning("Detected version 2.7 or 2.8. The server might not work with these versions.") log.info("Using backend '%s'", backend) diff --git a/src/rmview/screenstream/screenshare.py b/src/rmview/screenstream/screenshare.py index 404a180..e00d657 100644 --- a/src/rmview/screenstream/screenshare.py +++ b/src/rmview/screenstream/screenshare.py @@ -134,7 +134,7 @@ def startVncClient(self, challenge=None): def run(self): log.info("Connecting to ScreenShare, make sure you enable it") try: - if self.ssh.softwareVersion > SW_VER_TIMESTAMPS['2.9.1.236']: + if self.ssh.softwareVersion > (2, 9, 1, 236): log.warning("Authenticating, please wait...") challengeReader = ChallengeReaderProtocol(self.runVnc) reactor.listenUDP(5901, challengeReader) From 01a21650ddc266f8d5539eb4ab0bbd44c8309698 Mon Sep 17 00:00:00 2001 From: Kolton Yager Date: Mon, 20 Nov 2023 14:54:03 -0800 Subject: [PATCH 5/6] Added additional catch-all case for timestamp based versioning Prior commit reported version 2.9.1.236 for all timestamps greater-than or equal to the 2.9.1.236 timestamp. I realized this might break the versioning logic for later 2.9 SW versions, so I added an additional version tuple of (2.9.1.9999) to represent those versions instead. --- src/rmview/rmparams.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rmview/rmparams.py b/src/rmview/rmparams.py index 4fcd5e5..f81c5ba 100644 --- a/src/rmview/rmparams.py +++ b/src/rmview/rmparams.py @@ -18,8 +18,10 @@ def timestamp_to_version(ts): return (2, 7, 0, 0) elif ts < SW_VER_TIMESTAMPS["2.9.1.236"]: return (2, 9, 0, 0) - else: + elif ts == SW_VER_TIMESTAMPS["2.9.1.236"]: return (2, 9, 1, 236) + else: + return (2, 9, 1, 9999) # Phony version number. Just needs to compare > 2.9.1.236 # evtype_sync = 0 From c254ab17dc261bd8ae859759e0b5344f3fcf4415 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Mon, 11 Dec 2023 13:29:11 +0100 Subject: [PATCH 6/6] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3b0df23..8b81c4d 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def run(self): setup( name='rmview', - version='3.0', + version='3.1.3', url='https://github.com/bordaigorl/rmview', description='rMview: a fast live viewer for reMarkable', author='bordaigorl',