Compare commits

...

51 Commits
0.3 ... master

Author SHA1 Message Date
Gerard Braad 17f7c3e53f Update meson setup command 2023-03-19 18:21:00 +10:00
Peter Hutterer 02991b7fd9 CI: update some actions to v3
Node 12 is deprecated, so let's bump to a newer version
https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2022-12-07 09:00:38 +10:00
Peter Hutterer e44e982717 tuhi 0.6 2022-04-28 10:12:08 +10:00
Peter Hutterer 38e6fc29a7 flatpak: update the gnome runtime and to the latest pyxdg and svgwrite
pyparsing has new releases too but someone needs to check that...
2022-04-28 10:11:33 +10:00
Peter Hutterer 576d9d9fd2 Update translations
A few things were removed, let's keep the pot/po files up-to-date

$ meson compile -C translation-build tuhi-pot
$ meson compile -C translation-build tuhi-update-po
2022-04-28 10:06:45 +10:00
Peter Hutterer 008f65ca6b data: DrawingPerspective - don't translate the internal name page0
Same as 4facceebc4
2022-04-28 10:06:45 +10:00
albanobattistella e59fa5258d Update it.po 2022-04-28 09:13:54 +10:00
Peter Hutterer 57a2e9e7b7 CI: run apt-get update before apt-get install
Make sure our packageset is up-to-date

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2022-04-26 09:34:40 +10:00
Martin Owens 0b02f5f9c2 Change SVG export to use paths instead of line segments 2022-02-01 16:42:11 +10:00
Peter Hutterer 1362b89394 tools: add a basic SVG/PNG exporter tool
A commandline tool to convert the JSON files the GUI stores in
$XDG/tuhi/*.json. Makes testing SVG changes a bit easier.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2022-01-31 12:30:14 +10:00
Peter Hutterer 4ff7da5d41 protocol: handle the pen id packet correctly
We were re-using the header byte of the stroke header to count the bits.
Where that header is anything but 0xff we got out of sync and raised an
error. Fix this by renaming things so we don't accidentally use the
wrong fields.

Fixes #283

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2022-01-31 12:29:31 +10:00
Gündüzhan Gündüz f05fc2d80f
po: tr translation fixes (#277) 2021-02-08 13:57:36 +10:00
Gündüzhan Gündüz df7b92643e
Add turkish language translation (#275) 2021-02-04 15:58:19 +10:00
Peter Hutterer 7141412d14 Fix a typo in the ErrorPerspective
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-02-03 10:29:27 +10:00
Peter Hutterer c59730cf1f Replace Circle Ci with github workflows
It has a better integration to github (to no-ones suprise), even though its
use of docker containers isn't as convenient.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-25 14:02:12 +10:00
Peter Hutterer 16b0f9d2d5 meson.build: fall back to flake8 where flake8-3 does not exist
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-25 14:02:12 +10:00
Peter Hutterer 8ab43bf0b7 meson.build: explicitly check for gettext to be present
There's a meson issue where loading i18n produces a warning about missing
gettext but then proceeds to fail with confusing error messages where the i18n
module is used, see https://github.com/mesonbuild/meson/issues/6165

Work around this by explicitly checking for gettext after loading i18n.

Fixes #270

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-25 13:08:18 +10:00
Peter Hutterer 643099b858 Fix flake8 errors - ambiguous variable name 'l'
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-22 17:09:27 +10:00
Peter Hutterer b54ac6e99e Fix flake8 errors - module import not at top of the file
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-22 17:09:27 +10:00
Peter Hutterer 27164aba52 Fix flake8 errors - f-string without placeholders
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-22 17:09:27 +10:00
albanobattistella 9d36e3bad0
Add Italian translation (#263) 2020-09-09 07:14:06 +10:00
Peter Hutterer 4facceebc4 data: don't make the internal page0, page1 etc names translatable
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-09-04 09:43:15 +10:00
Peter Hutterer 4ce8d011d1 tuhi 0.5
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-09-02 10:24:42 +10:00
Peter Hutterer 9b4c0ca5ef Update the copyright year in the about dialog
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-09-02 10:24:42 +10:00
Peter Hutterer 02252b68b9 Update flatpak dependencies to latest versions
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-09-02 10:12:08 +10:00
Peter Hutterer eef877d6ce tuhi: use a Gtk.FileChooserNative dialog
This one hooks into the correct portal APIs, hopefully fixing the issue with
the Flatpak version not saving files correctly.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 11:03:26 +10:00
Peter Hutterer ea5177027c Update flatpak GNOME version to 3.36
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 11:03:26 +10:00
Peter Hutterer ba0424c6df gui: fix a flake8 complaint
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 11:03:26 +10:00
Peter Hutterer 59dfce5b8d gui: add an expanding label to the end, but don't expand flowboxes
pack_end, introduced in 29761204a9 means they
align to the bottom of the window. Where there aren't enough drawings to fill
the window, everything is bottom aligned. Hack around this by adding an empty
label to the bottom that expands to the maximum available size and stop
expanding the rest. This new label thus pushes everything up to the top
position.

Fixes 29761204a9

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 10:33:07 +10:00
Peter Hutterer 29761204a9 gui: pack the flowboxes with pack_end
The first drawings we load are from disk. If we sync a drawing from the device
with a different key (i.e. created in a different month), the new flowbox for
that drawing was appended to the list and in last place. This caused
out-of-order sorting (though a restart of Tuhi would fix it).

So simply change the behavior to sort by oldest timestamp first and pack_end()
for all of them. If we pack all our flowboxes with pack_end, they are
effectively in reverse order, i.e. last one added is first in the list.

Fixes #244

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 09:34:08 +10:00
Peter Hutterer 927fc0216b tuhi 0.4
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-07-21 16:54:52 +10:00
Taihsiang Ho (tai271828) 3c1a84e1ab protocol: rm unused class WacomPacket 2020-04-21 15:23:44 +10:00
Robert Schütz 72d9dee37a live: keep $PATH and $DISPLAY 2020-04-06 15:38:17 +02:00
Peter Hutterer 7a881de9bb test: switch the stroke tests to use pytest
We're already running the meson test through pytest anyway and pytest is more
powerful than unittest. So let's switch, it's just a search/replace away.

Plus, this way the approach to dynamically create the tests based on the test
logs in the user's home directory is a lot saner.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:39:45 +01:00
Peter Hutterer fcdacb7187 test: switch the message tests from unittest to pytest
We're already running the meson test through pytest anyway and pytest is more
powerfull than unittest. So let's switch, it's just a search/replace away.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:39:45 +01:00
Peter Hutterer acc459f003 meson.build: check for 3.30 as minimum pygobject version
Fixes #238

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:12:24 +10:00
Peter Hutterer 53e2cebaf8 protocol: fix a flake8 issue
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:08:36 +10:00
Peter Hutterer 785c1e5f58 test: append the required \0xa to the spark messages
f5ea08f171 added this effective requirement for
the Spark and older generations, so let's make sure the test passes.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:08:36 +10:00
Niclas Hoyer f5ea08f171 Add support for long device names
If the device name is longer than one reply, the registration will crash.
This adds support for a variable number of replies until the reply ends
with `0x0a`.
2020-01-07 11:05:13 +10:00
Niclas Hoyer a0566e0dc0 Workaround for Spark register problems
This is a workaround for registering problems with Spark.
The registration hangs at "Connecting to device...".

Internally the `GENERAL_ERROR` occurs, which is expected
when registering a Spark device, but this should also
raise an AuthorizeException, but it instead reaches the
handler as DeviceError.

It is currently unclear why the `GENERAL_ERROR` passes to
this handler, so this catches any `GENERAL_ERROR` while
registering a Spark device.
2020-01-07 11:00:11 +10:00
Peter Hutterer 820f168f43 meson.build: drop python-devel dependency
This isn't needed. We need python but if we can run meson we can rely on
python being available anyway. And we don't actually use the python header
files here.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-03 10:35:15 +10:00
Peter Hutterer 110c77745c flatpak: allow access to $HOME
The whole purpose of Tuhi is to save the files after downloading and the
flatpak sandbox quietly dropped any file we saved in the target directory.

We could use xdg-download or something as allowed directory but let's open up
home itself since we don't want to limit where the files can be saved.

Fixes #230

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-03 10:34:50 +10:00
Niclas Hoyer 67eb54c908 Add company ID for Augmented Paper 2020-01-03 10:34:25 +10:00
Peter Hutterer 170b2c0d8a meson.build: add a custom message for the python3 dependency.
It's confusing to users because they don't get any indication that it's the
development package we need, not the normal python package.

This requires bumping meson's minimum version to 0.50 but hey, we can live
with that.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-11-04 09:43:08 +01:00
Piotr Drąg a11d51c42c Update Polish translation 2019-10-24 16:54:46 +02:00
Peter Hutterer f6fdf86649 meson.build: drop yaml from the required python module list
This isn't required for the flatpak, the yaml module is only needed in some of
the debugging/recovery tools but not in Tuhi itself.
2019-09-30 11:36:52 +02:00
Peter Hutterer d5cb1930e5 export: drop unused import
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-30 18:05:39 +10:00
Ishak BELAHMAR ddd896b1f6 tools: Add choices to fetch.format argument 2019-09-30 09:50:03 +02:00
Ishak BELAHMAR ea1dc798b1 Remove unused svg module 2019-09-30 09:50:03 +02:00
Ishak BELAHMAR bbff179a4d Add png export feature 2019-09-30 09:50:03 +02:00
Piotr Drąg 538480f022 Add Polish translation 2019-09-16 08:32:02 +10:00
31 changed files with 1250 additions and 568 deletions

View File

@ -1,18 +0,0 @@
version: 2
jobs:
build:
working_directory: ~/tuhi
docker:
- image: fedora:latest
steps:
- run:
command: |
dnf install -y meson gettext python3-devel pygobject3-devel python3-flake8 desktop-file-utils libappstream-glib python3-pytest python3-pyxdg python3-pyyaml python3-svgwrite
- checkout
- run:
command: |
meson builddir
ninja -C builddir test
- store_artifacts:
path: ~/tuhi/builddir/meson-logs

26
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,26 @@
on: [ push, pull_request ]
env:
CFLAGS: "-Werror -Wall -Wextra -Wno-error=sign-compare -Wno-error=unused-parameter -Wno-error=missing-field-initializers"
UBUNTU_PACKAGES: meson gettext python3-dev python-gi-dev flake8 desktop-file-utils libappstream-glib-dev appstream-util python3-pytest python3-xdg python3-yaml python3-svgwrite python3-cairo
jobs:
meson_test:
runs-on: ubuntu-20.04
steps:
- name: Install dependencies
run: |
sudo apt-get update -yq
sudo apt-get install -yq --no-install-suggests --no-install-recommends $UBUNTU_PACKAGES
- uses: actions/checkout@v3
- name: meson
run: meson builddir
- name: ninja
run: ninja -C builddir test
- name: capture build logs
uses: actions/upload-artifact@v3
if: ${{ always() }} # even if we fail
with:
name: meson logs
path: |
builddir/meson-logs

View File

@ -26,7 +26,7 @@ To build and run Tuhi from the repository directly:
```
$> git clone http://github.com/tuhiproject/tuhi
$> cd tuhi
$> meson builddir
$> meson setup builddir
$> ninja -C builddir
$> ./builddir/tuhi.devel
```
@ -41,7 +41,7 @@ To install and run Tuhi:
```
$> git clone http://github.com/tuhiproject/tuhi
$> cd tuhi
$> meson builddir
$> meson setup builddir
$> ninja -C builddir install
```

View File

@ -8,7 +8,7 @@
<property name="type_hint">normal</property>
<property name="program_name">Tuhi</property>
<property name="version">@version@</property>
<property name="copyright">Copyright © 2019 Tuhi Developers</property>
<property name="copyright">Copyright © 2020 Tuhi Developers</property>
<property name="website">@url@</property>
<property name="website_label" translatable="yes">Visit Tuhis website</property>
<property name="logo_icon_name">org.freedesktop.Tuhi</property>

View File

@ -140,7 +140,7 @@
</object>
<packing>
<property name="name">page0</property>
<property name="title" translatable="yes">page0</property>
<property name="title">page0</property>
</packing>
</child>
</template>

View File

@ -22,7 +22,7 @@
Tuhi connects to tablets of the Wacom Ink range. It allows you to download the drawings stored on those devices as SVGs for processing later.
Tuhi is a DBus server that needs to be running for the Tuhi GUI to connect to it. Connecting to the DBus server should take less than a second. If you read this far, your Tuhi DBus server is not running or responsing and needs to be restarted.</property>
Tuhi is a DBus server that needs to be running for the Tuhi GUI to connect to it. Connecting to the DBus server should take less than a second. If you read this far, your Tuhi DBus server is not running or responding and needs to be restarted.</property>
<property name="wrap">True</property>
<property name="max_width_chars">55</property>
</object>

View File

@ -118,7 +118,7 @@
</object>
<packing>
<property name="name">page0</property>
<property name="title" translatable="yes">page0</property>
<property name="title">page0</property>
</packing>
</child>
<child>
@ -185,7 +185,7 @@
</object>
<packing>
<property name="name">page1</property>
<property name="title" translatable="yes">page1</property>
<property name="title">page1</property>
<property name="position">1</property>
</packing>
</child>
@ -255,7 +255,7 @@
</object>
<packing>
<property name="name">page2</property>
<property name="title" translatable="yes">page2</property>
<property name="title">page2</property>
<property name="position">2</property>
</packing>
</child>

View File

@ -1,13 +1,12 @@
project('tuhi',
version: '0.3',
version: '0.6',
license: 'GPLv2',
meson_version: '>= 0.48.0')
meson_version: '>= 0.50.0')
# The tag date of the project_version(), update when the version bumps.
version_date='2019-09-12'
version_date='2022-04-28'
# Dependencies
dependency('python3', required: true)
dependency('pygobject-3.0', required: true)
dependency('pygobject-3.0', version: '>= 3.30', required: true)
prefix = get_option('prefix')
datadir = join_paths(prefix, get_option('datadir'))
@ -24,6 +23,8 @@ libexecdir = join_paths(get_option('prefix'), get_option('libexecdir'), 'tuhi')
i18n = import('i18n')
# Workaround for https://github.com/mesonbuild/meson/issues/6165
find_program('gettext')
subdir('po')
subdir('data')
@ -35,7 +36,7 @@ python_modules = [
'svgwrite',
'xdg',
'gi',
'yaml',
'cairo',
]
if meson.version().version_compare('>=0.51')
py3 = pymod.find_installation(modules: python_modules)
@ -138,7 +139,7 @@ appdata = i18n.merge_file(input: appdata_intl,
install_data('data/org.freedesktop.Tuhi.svg', install_dir: icondir)
flake8 = find_program('flake8-3', required: false)
flake8 = find_program('flake8-3', 'flake8', required: false)
if flake8.found()
test('flake8', flake8,
args: ['--ignore=E501,W504',

View File

@ -1,7 +1,7 @@
{
"app-id": "org.freedesktop.Tuhi",
"runtime": "org.gnome.Platform",
"runtime-version": "3.32",
"runtime-version": "42",
"sdk": "org.gnome.Sdk",
"command": "tuhi",
"finish-args": [
@ -10,7 +10,8 @@
"--socket=wayland",
"--talk-name=org.freedesktop.tuhi1",
"--own-name=org.freedesktop.tuhi1",
"--system-talk-name=org.bluez"
"--system-talk-name=org.bluez",
"--filesystem=home"
],
"modules": [
{
@ -20,8 +21,8 @@
{
"type": "git",
"url": "https://gitlab.freedesktop.org/xdg/pyxdg.git",
"tag": "rel-0.26",
"commit": "7db14dcf4c4305c3859a2d9fcf9f5da2db328330"
"tag": "rel-0.27",
"commit": "f097a66923a65e93640c48da83e6e9cfbddd86ba"
}
],
"build-commands": [
@ -34,8 +35,8 @@
"sources": [
{
"type": "archive",
"url": "https://github.com/pyparsing/pyparsing/releases/download/pyparsing_2.4.2/pyparsing-2.4.2.tar.gz",
"sha512": "27e5959eb1cf0c4d899746d2d32f5f000c3753278bdbbb670d24a077053e5c08caf8429f684186c502f6d9bf358702e0a8b3fea40cd2b50807cf02ea38c750dd"
"url": "https://github.com/pyparsing/pyparsing/releases/download/pyparsing_2.4.7/pyparsing-2.4.7.tar.gz",
"sha512": "0b9f8f18907f65cb3af1b48ed57989e183f28d71646f2b2f820e772476f596ca15ee1a689f3042f18458206457f4683d10daa6e73dfd3ae82d5e4405882f9dd2"
}
],
"build-commands": [
@ -49,8 +50,8 @@
{
"type": "git",
"url": "https://github.com/mozman/svgwrite.git",
"tag": "v1.3.1",
"commit": "13633ad13d7a4b3253d1304d31db8fc2c8d1dd9e"
"tag": "v1.4.2",
"commit": "e2617741ab018956e638e18aa21827405bd8edd1"
}
],
"build-commands": [

View File

@ -1 +1,4 @@
# Language list must be in alphabetical order
it
pl
tr

193
po/it.po Normal file
View File

@ -0,0 +1,193 @@
# Italian translation for tuhi.
# Copyright © 2019 the tuhi authors.
# This file is distributed under the same license as the tuhi package.
# ALBANO BATTISTELLA <albano_battistella@hotmail.com>, 2020,2022.
#
msgid ""
msgstr ""
"Project-Id-Version: tuhi\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-28 09:34+1000\n"
"PO-Revision-Date: 2022-04-25 18:23+0100\n"
"Last-Translator: Albano Battistella <piotrd>\n"
"Language-Team: Italian <>\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:7
#: data/org.freedesktop.Tuhi.desktop.in:3
msgid "Tuhi"
msgstr "Tuhi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:8
#: data/org.freedesktop.Tuhi.desktop.in:4
msgid "Utility to download drawings from the Wacom Ink range of devices"
msgstr ""
"Utilità per scaricare i disegni dalla gamma Inchiostro di dispositivi Wacom"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:10
msgid ""
"Tuhi is a graphical user interface to download drawings stored on tablet "
"devices from the Wacom Ink range, e.g. Intuos Pro Paper or Bamboo Slate."
msgstr ""
"Tuhi è un'interfaccia utente grafica per scaricare disegni archiviati su "
"tablet dispositivi della gamma di Inchiostro Wacom, ad esempio Intuos Pro "
"Paper o Bamboo Slate."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:15
#, fuzzy
msgid ""
"Tuhi requires Tuhi, the daemon to actually communicate with the devices. "
"ThiGui is merely a front end to Tuhi, Tuhi must be installed and running "
"when Tuhi is launched."
msgstr ""
"Tuhi richiede Tuhi, il demone per comunicare effettivamente con i "
"dispositivi.ThiGui è semplicemente un front-end di Tuhi, Tuhi deve essere "
"installato e funzionante quando Tuhi viene lanciato."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:32
msgid "Tuhi's main window"
msgstr "Finestra principale di Tuhi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:36
msgid "Tuhi's main window (zoomed)"
msgstr "Finestra principale di Tuhi (ingrandita)"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: data/org.freedesktop.Tuhi.desktop.in:12
msgid "tablet;wacom;ink;"
msgstr "tablet;wacom;inchiostro;"
#: data/ui/AboutDialog.ui.in:13
msgid "Visit Tuhis website"
msgstr "Visita il sito web di Tuhi"
#: data/ui/AppMenu.ui:5
msgid "Portrait"
msgstr "Ritratto"
#: data/ui/AppMenu.ui:10
msgid "Landscape"
msgstr "Paesaggio"
#: data/ui/AppMenu.ui:17
msgid "Help"
msgstr "Aiuto"
#: data/ui/AppMenu.ui:21
msgid "About"
msgstr "Informazioni"
#: data/ui/DrawingPerspective.ui:68
msgid "Undo delete drawing"
msgstr "Annulla cancellazione del disegno"
#: data/ui/DrawingPerspective.ui:132
msgid "Press the button on the device to synchronize drawings"
msgstr "Premere il pulsante sul dispositivo per sincronizzare i disegni"
#: data/ui/ErrorPerspective.ui:21
#, fuzzy
msgid ""
"TuhiGUI is an interactive GUI to download data from Tuhi.\n"
"\n"
"Tuhi connects to tablets of the Wacom Ink range. It allows you to download "
"the drawings stored on those devices as SVGs for processing later.\n"
"\n"
"Tuhi is a DBus server that needs to be running for the Tuhi GUI to connect "
"to it. Connecting to the DBus server should take less than a second. If you "
"read this far, your Tuhi DBus server is not running or responding and needs "
"to be restarted."
msgstr ""
"TuhiGUI è una GUI interattiva per scaricare dati da Tuhi.\n"
"\n"
"Tuhi si collega ai tablet della gamma Wacom. Ti permette di scaricare i "
"disegni memorizzati su tali dispositivi come SVG per l'elaborazione "
"successiva.\n"
"\n"
"Tuhi è un server DBus che deve essere in esecuzione perché la GUI Tuhi possa "
"connettersi ad esso. La connessione al server DBus dovrebbe richiedere meno "
"di un secondo. Se hai letto fin qui, il tuo server Tuhi DBus non è in "
"esecuzione o non risponde e ha bisogno di essere riavviato."
#: data/ui/ErrorPerspective.ui:69
msgid "Connecting to Tuhi"
msgstr "Connessione a Tuhi"
#: data/ui/ErrorPerspective.ui:96
msgid ""
"This should take less than a second. Make sure the Tuhi DBus server is "
"running."
msgstr ""
"Questo dovrebbe richiedere meno di un secondo. Assicurati che il server Tuhi "
"DBus sia in esecuzione."
#: data/ui/MainWindow.ui:166
msgid "Authorization error while connecting to the device "
msgstr "Errore di autorizzazione durante la connessione al dispositivo "
#: data/ui/MainWindow.ui:176
msgid "Register"
msgstr "Registro"
#: data/ui/SetupPerspective.ui:7
msgid "Initial Device Setup"
msgstr "Configurazione iniziale del dispositivo"
#: data/ui/SetupPerspective.ui:30
msgid "Quit"
msgstr "Esci"
#: data/ui/SetupPerspective.ui:70
msgid "Hold the button on the device until the blue light is flashing."
msgstr ""
"Tieni premuto il pulsante sul dispositivo finché la luce blu non lampeggia."
#: data/ui/SetupPerspective.ui:103
msgid "Searching for device"
msgstr "Ricerca del dispositivo"
#: data/ui/SetupPerspective.ui:137
msgid "Connecting to LE Paper"
msgstr "Collegamento a LE Paper"
#: data/ui/SetupPerspective.ui:170
msgid "Connecting to device..."
msgstr "Connessione al dispositivo..."
#: data/ui/SetupPerspective.ui:206
msgid "Press the button on the device now!"
msgstr "Premi ora il pulsante sul dispositivo!"
#: data/ui/SetupPerspective.ui:240
msgid "waiting for reply"
msgstr "in attesa di risposta"
#. Translators: the default filename to save to
#: tuhi/gui/drawing.py:121
msgid "untitled.svg"
msgstr "senza titolo.svg"
#. Translators: filter name to show all/any files
#: tuhi/gui/drawing.py:125
msgid "Any files"
msgstr "Qualsiasi file"
#. Translators: filter to show svg files only
#: tuhi/gui/drawing.py:129
msgid "SVG files"
msgstr "File SVG"
#. Translators: filter to show png files only
#: tuhi/gui/drawing.py:133
msgid "PNG files"
msgstr "File PNG"
#: tuhi/gui/window.py:68
#, python-brace-format
msgid "Connecting to {device.name}"
msgstr "Connessione a {device.name}"

193
po/pl.po Normal file
View File

@ -0,0 +1,193 @@
# Polish translation for tuhi.
# Copyright © 2019 the tuhi authors.
# This file is distributed under the same license as the tuhi package.
# Piotr Drąg <piotrdrag@gmail.com>, 2019.
# Aviary.pl <community-poland@mozilla.org>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: tuhi\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-28 09:34+1000\n"
"PO-Revision-Date: 2019-10-24 16:23+0200\n"
"Last-Translator: Piotr Drąg <piotrdrag@gmail.com>\n"
"Language-Team: Polish <community-poland@mozilla.org>\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:7
#: data/org.freedesktop.Tuhi.desktop.in:3
msgid "Tuhi"
msgstr "Tuhi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:8
#: data/org.freedesktop.Tuhi.desktop.in:4
msgid "Utility to download drawings from the Wacom Ink range of devices"
msgstr "Narzędzie do pobierania rysunków z rodziny urządzeń Wacom Ink"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:10
msgid ""
"Tuhi is a graphical user interface to download drawings stored on tablet "
"devices from the Wacom Ink range, e.g. Intuos Pro Paper or Bamboo Slate."
msgstr ""
"Tuhi to graficzny interfejs użytkownika do pobierania rysunków "
"przechowywanych na tabletach z rodziny Wacom Ink, np. Intuos Pro Paper "
"i Bamboo Slate."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:15
msgid ""
"Tuhi requires Tuhi, the daemon to actually communicate with the devices. "
"ThiGui is merely a front end to Tuhi, Tuhi must be installed and running "
"when Tuhi is launched."
msgstr ""
"TuhiGUI wymaga Tuhi, usługi komunikującej się z urządzeniem. TuhiGUI to "
"interfejs dla usługi Tuhi, która musi być zainstalowana i uruchomiona, aby "
"narzędzie Tuhi mogło działać."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:32
msgid "Tuhi's main window"
msgstr "Główne okno Tuhi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:36
msgid "Tuhi's main window (zoomed)"
msgstr "Główne okno Tuhi (powiększone)"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: data/org.freedesktop.Tuhi.desktop.in:12
msgid "tablet;wacom;ink;"
msgstr "tablet;wacom;ink;"
#: data/ui/AboutDialog.ui.in:13
msgid "Visit Tuhis website"
msgstr "Witryna programu Tuhi"
#: data/ui/AppMenu.ui:5
msgid "Portrait"
msgstr "Pionowo"
#: data/ui/AppMenu.ui:10
msgid "Landscape"
msgstr "Poziomo"
#: data/ui/AppMenu.ui:17
msgid "Help"
msgstr "Pomoc"
#: data/ui/AppMenu.ui:21
msgid "About"
msgstr "O programie"
#: data/ui/DrawingPerspective.ui:68
msgid "Undo delete drawing"
msgstr "Cofnij usunięcie rysunku"
#: data/ui/DrawingPerspective.ui:132
msgid "Press the button on the device to synchronize drawings"
msgstr "Proszę nacisnąć przycisk na urządzeniu, aby zsynchronizować rysunki"
#: data/ui/ErrorPerspective.ui:21
#, fuzzy
msgid ""
"TuhiGUI is an interactive GUI to download data from Tuhi.\n"
"\n"
"Tuhi connects to tablets of the Wacom Ink range. It allows you to download "
"the drawings stored on those devices as SVGs for processing later.\n"
"\n"
"Tuhi is a DBus server that needs to be running for the Tuhi GUI to connect "
"to it. Connecting to the DBus server should take less than a second. If you "
"read this far, your Tuhi DBus server is not running or responding and needs "
"to be restarted."
msgstr ""
"TuhiGUI to interaktywny interfejs użytkownika do pobierania danych z usługi "
"Tuhi.\n"
"\n"
"Tuhi łączy się z tabletami z rodziny Wacom Ink. Umożliwia pobieranie obrazów "
"przechowywanych na tych urządzeniach jako pliki SVG do przetwarzania "
"w późniejszym czasie.\n"
"\n"
"Tuhi to serwer D-Bus, który musi być uruchomiony, aby interfejs Tuhi mógł "
"się z nim połączyć. Połączenie z serwerem D-Bus powinno zająć mniej niż "
"sekundę. Jeśli jeszcze czytasz ten tekst, to znaczy że serwer D-Bus Tuhi nie "
"jest uruchomiony lub nie odpowiada i wymaga ponownego uruchomienia."
#: data/ui/ErrorPerspective.ui:69
msgid "Connecting to Tuhi"
msgstr "Łączenie z usługą Tuhi"
#: data/ui/ErrorPerspective.ui:96
msgid ""
"This should take less than a second. Make sure the Tuhi DBus server is "
"running."
msgstr ""
"To powinno zająć mniej niż sekundę. Proszę się upewnić, że serwer D-Bus Tuhi "
"jest uruchomiony."
#: data/ui/MainWindow.ui:166
msgid "Authorization error while connecting to the device "
msgstr "Błąd upoważnienia podczas łączenia z urządzeniem "
#: data/ui/MainWindow.ui:176
msgid "Register"
msgstr "Zarejestruj"
#: data/ui/SetupPerspective.ui:7
msgid "Initial Device Setup"
msgstr "Pierwsza konfiguracja urządzenia"
#: data/ui/SetupPerspective.ui:30
msgid "Quit"
msgstr "Zakończ"
#: data/ui/SetupPerspective.ui:70
msgid "Hold the button on the device until the blue light is flashing."
msgstr ""
"Proszę przytrzymać przycisk na urządzeniu, aż niebieska dioda zacznie migać."
#: data/ui/SetupPerspective.ui:103
msgid "Searching for device"
msgstr "Wyszukiwanie urządzenia"
#: data/ui/SetupPerspective.ui:137
msgid "Connecting to LE Paper"
msgstr "Łączenie z LE Paper"
#: data/ui/SetupPerspective.ui:170
msgid "Connecting to device..."
msgstr "Łączenie z urządzeniem…"
#: data/ui/SetupPerspective.ui:206
msgid "Press the button on the device now!"
msgstr "Proszę teraz nacisnąć przycisk na urządzeniu."
#: data/ui/SetupPerspective.ui:240
msgid "waiting for reply"
msgstr "oczekiwanie na odpowiedź"
#. Translators: the default filename to save to
#: tuhi/gui/drawing.py:121
msgid "untitled.svg"
msgstr "bez tytułu.svg"
#. Translators: filter name to show all/any files
#: tuhi/gui/drawing.py:125
msgid "Any files"
msgstr "Wszystkie pliki"
#. Translators: filter to show svg files only
#: tuhi/gui/drawing.py:129
msgid "SVG files"
msgstr "Pliki SVG"
#. Translators: filter to show png files only
#: tuhi/gui/drawing.py:133
msgid "PNG files"
msgstr "Pliki PNG"
#: tuhi/gui/window.py:68
#, python-brace-format
msgid "Connecting to {device.name}"
msgstr "Łączenie z urządzeniem {device.name}"

190
po/tr.po Normal file
View File

@ -0,0 +1,190 @@
# Turkish translation for tuhi.
# Copyright © 2019 the tuhi authors.
# This file is distributed under the same license as the tuhi package.
# Gündüzhan Gündüz <gunduzhan@gmail.com>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: tuhi\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-28 09:34+1000\n"
"PO-Revision-Date: \n"
"Last-Translator: Gündüzhan Gündüz <gunduzhan@gmail.com>\n"
"Language-Team: Turkish <gnome-turk@gnome.org>\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 2.4.1\n"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:7
#: data/org.freedesktop.Tuhi.desktop.in:3
msgid "Tuhi"
msgstr "Tuhi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:8
#: data/org.freedesktop.Tuhi.desktop.in:4
msgid "Utility to download drawings from the Wacom Ink range of devices"
msgstr "Wacom Ink cihazlarınızdan çizimlerinizi indirmek için bir araç"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:10
msgid ""
"Tuhi is a graphical user interface to download drawings stored on tablet "
"devices from the Wacom Ink range, e.g. Intuos Pro Paper or Bamboo Slate."
msgstr ""
"Tuhi, Wacom Ink serisi cihazlarda depolanan çizimlerinizi indirmenize olanak "
"sağlayan bir grafik kullanıcı arayüzüdür. Ör. Intuos Pro Paper veya Bamboo "
"Slate."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:15
#, fuzzy
msgid ""
"Tuhi requires Tuhi, the daemon to actually communicate with the devices. "
"ThiGui is merely a front end to Tuhi, Tuhi must be installed and running "
"when Tuhi is launched."
msgstr ""
"Tuhi cihazınız ile iletişim kurmak için arka plan programı Tuhi'ye ihtiyaç "
"duyar. ThiGui, Tuhi için sadece bir başlangıç aşamasıdır. Tuhi "
"başlatıldığında kurulmalı ve çalıştırılmalıdır."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:32
msgid "Tuhi's main window"
msgstr "Tuhi ana penceresi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:36
msgid "Tuhi's main window (zoomed)"
msgstr "Tuhi ana penceresi (yakın)"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: data/org.freedesktop.Tuhi.desktop.in:12
msgid "tablet;wacom;ink;"
msgstr "tablet;wacom;ink;"
#: data/ui/AboutDialog.ui.in:13
msgid "Visit Tuhis website"
msgstr "Tuhi'nin sitesini ziyaret edin"
#: data/ui/AppMenu.ui:5
msgid "Portrait"
msgstr "Portre"
#: data/ui/AppMenu.ui:10
msgid "Landscape"
msgstr "Yatay"
#: data/ui/AppMenu.ui:17
msgid "Help"
msgstr "Yardım"
#: data/ui/AppMenu.ui:21
msgid "About"
msgstr "Hakkında"
#: data/ui/DrawingPerspective.ui:68
msgid "Undo delete drawing"
msgstr "Silinen çizimi geri al"
#: data/ui/DrawingPerspective.ui:132
msgid "Press the button on the device to synchronize drawings"
msgstr "Çizimlerinizi senkron etmek için cihazdaki düğmeye basın"
#: data/ui/ErrorPerspective.ui:21
#, fuzzy
msgid ""
"TuhiGUI is an interactive GUI to download data from Tuhi.\n"
"\n"
"Tuhi connects to tablets of the Wacom Ink range. It allows you to download "
"the drawings stored on those devices as SVGs for processing later.\n"
"\n"
"Tuhi is a DBus server that needs to be running for the Tuhi GUI to connect "
"to it. Connecting to the DBus server should take less than a second. If you "
"read this far, your Tuhi DBus server is not running or responding and needs "
"to be restarted."
msgstr ""
"TuhiGUI, Tuhi'den çizimlerinizi indirmek için bir araçtır.\n"
"\n"
"Tuhi Wacom Ink serisi cihazlara bağlanır. Ayrıca cihazlarda depolanan "
"çizimlerinizi işlemek üzere SVG olarak indirmenizi sağlar.\n"
"\n"
"Tuhi, TuhiGUI'nin bağlanabilmesi için çalışan bir DBus sunucusudur. DBus "
"sunucusuna bağlanmak saniyeden çok daha kısa sürede gerçekleşir. Eğer buraya "
"kadar okuduysanız, Tuhi DBus sunucusu çalışmıyor, yanıt vermiyor ve yeniden "
"başlatılması gerekiyordur."
#: data/ui/ErrorPerspective.ui:69
msgid "Connecting to Tuhi"
msgstr "Tuhi'ye bağlanılıyor"
#: data/ui/ErrorPerspective.ui:96
msgid ""
"This should take less than a second. Make sure the Tuhi DBus server is "
"running."
msgstr ""
"Bir saniyeden daha kısa sürer. Tuhi DBus sunucusunun çalıştığından emin olun."
#: data/ui/MainWindow.ui:166
msgid "Authorization error while connecting to the device "
msgstr "Cihaza bağlanırken yetkilendirme hatası oluştu "
#: data/ui/MainWindow.ui:176
msgid "Register"
msgstr "Kayıt"
#: data/ui/SetupPerspective.ui:7
msgid "Initial Device Setup"
msgstr "İlk Cihaz Kurulumu"
#: data/ui/SetupPerspective.ui:30
msgid "Quit"
msgstr "Çıkış"
#: data/ui/SetupPerspective.ui:70
msgid "Hold the button on the device until the blue light is flashing."
msgstr "Mavı ışık yanıp sönmeye başlayana kadar düğmeye basılı tutun."
#: data/ui/SetupPerspective.ui:103
msgid "Searching for device"
msgstr "Cihaz aranıyor"
#: data/ui/SetupPerspective.ui:137
msgid "Connecting to LE Paper"
msgstr "LE Paper'a bağlanıyor"
#: data/ui/SetupPerspective.ui:170
msgid "Connecting to device..."
msgstr "Cihaza bağlanıyor ..."
#: data/ui/SetupPerspective.ui:206
msgid "Press the button on the device now!"
msgstr "Şimdi cihazdaki düğmeye basın!"
#: data/ui/SetupPerspective.ui:240
msgid "waiting for reply"
msgstr "yanıt bekleniyor"
#. Translators: the default filename to save to
#: tuhi/gui/drawing.py:121
msgid "untitled.svg"
msgstr "yenibelge.svg"
#. Translators: filter name to show all/any files
#: tuhi/gui/drawing.py:125
msgid "Any files"
msgstr "Herhangi bir dosya"
#. Translators: filter to show svg files only
#: tuhi/gui/drawing.py:129
msgid "SVG files"
msgstr "SVG dosyaları"
#. Translators: filter to show png files only
#: tuhi/gui/drawing.py:133
msgid "PNG files"
msgstr "PNG dosyaları"
#: tuhi/gui/window.py:68
#, python-brace-format
msgid "Connecting to {device.name}"
msgstr "{device.name} cihaz bağlantısı kuruluyor"

262
test/test_messages.py Normal file → Executable file
View File

@ -19,19 +19,19 @@
import calendar
import os
import pytest
import sys
import unittest
import time
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.protocol import *
from tuhi.protocol import * # noqa
SUCCESS = NordicData([0xb3, 0x1, 0x00])
class TestUtils(unittest.TestCase):
class TestUtils(object):
def test_hex_string(self):
values = [
([0x00, 0x12], '00 12'),
@ -44,12 +44,12 @@ class TestUtils(unittest.TestCase):
]
for v in values:
self.assertEqual(as_hex_string(v[0]), v[1])
assert as_hex_string(v[0]) == v[1]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
as_hex_string(1)
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
as_hex_string('0x00')
def test_protocol_version(self):
@ -64,14 +64,14 @@ class TestUtils(unittest.TestCase):
]
for v in values:
self.assertEqual(ProtocolVersion.from_string(v[0]), v[1])
assert ProtocolVersion.from_string(v[0]) == v[1]
# No real reason we couldn't support those but right now they
# aren't, so let's test for it.
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
ProtocolVersion.from_string('Slate')
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
ProtocolVersion.from_string('IntuosPro')
def test_little_u16(self):
@ -81,12 +81,12 @@ class TestUtils(unittest.TestCase):
]
for v in values:
self.assertEqual(little_u16(v[0]), bytes(v[1]))
self.assertEqual(little_u16(v[1]), v[0])
assert little_u16(v[0]) == bytes(v[1])
assert little_u16(v[1]) == v[0]
invalid = [0x10000, -1, [0x00, 0x00, 0x00]]
for v in invalid:
with self.assertRaises(AssertionError):
with pytest.raises(AssertionError):
little_u16(v)
def test_little_u32(self):
@ -98,20 +98,20 @@ class TestUtils(unittest.TestCase):
]
for v in values:
self.assertEqual(little_u32(v[0]), bytes(v[1]))
self.assertEqual(little_u32(v[1]), v[0])
assert little_u32(v[0]) == bytes(v[1])
assert little_u32(v[1]) == v[0]
invalid = [0x100000000, -1, [0x00, 0x00, 0x00, 0x00, 0x00]]
for v in invalid:
with self.assertRaises(AssertionError):
with pytest.raises(AssertionError):
little_u32(v)
class TestProtocolAny(unittest.TestCase):
class TestProtocolAny(object):
protocol_version = ProtocolVersion.ANY
def test_get_protocol(self):
self.assertIsNotNone(Protocol(self.protocol_version, callback=None))
assert Protocol(self.protocol_version, callback=None) is not None
def test_has_all_messages(self):
p = Protocol(self.protocol_version, callback=None)
@ -136,47 +136,47 @@ class TestProtocolAny(unittest.TestCase):
def test_connect(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe6)
self.assertEqual(request.length, 6)
assert request.opcode == 0xe6
assert request.length == 6
return SUCCESS
if cb is None:
cb = _cb
p = Protocol(self.protocol_version, callback=cb)
with self.assertRaises(TypeError):
with pytest.raises(TypeError):
p.execute(Interactions.CONNECT) # missing argument
uuid = 'abcdef123456'
msg = p.execute(Interactions.CONNECT, uuid)
self.assertEqual(msg.uuid, uuid)
assert msg.uuid == uuid
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
p.execute(Interactions.CONNECT, 'too-long-an-id')
with self.assertRaises(binascii.Error):
with pytest.raises(binascii.Error):
uuid = 'uvwxyz123456'
p.execute(Interactions.CONNECT, uuid)
def test_get_name(self, cb=None, name='test dev name'):
def test_get_name(self, cb=None, name='test dev name\x0a'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xbb)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xbb
assert request.length == 1
assert request[0] == 0x00
return NordicData([0xbc, len(name)] + list(bytes(name, encoding='ascii')))
cb = cb or _cb
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_NAME)
self.assertEqual(msg.name, name)
assert msg.name == name
def test_set_name(self, cb=None, name='test dev name'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xbb)
self.assertEqual(request.length, len(name) + 1)
self.assertEqual(request[-1], 0xa) # spark needs a trailing linebreak
self.assertEqual(bytes(request[:-1]).decode('utf-8'), name)
assert request.opcode == 0xbb
assert request.length == len(name) + 1
assert request[-1] == 0xa # spark needs a trailing linebreak
assert bytes(request[:-1]).decode('utf-8') == name
return SUCCESS
cb = cb or _cb
@ -186,8 +186,8 @@ class TestProtocolAny(unittest.TestCase):
def test_get_time(self, cb=None, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb6)
self.assertEqual(request.length, 1)
assert request.opcode == 0xb6
assert request.length == 1
t = time.strftime('%y%m%d%H%M%S', time.gmtime(ts))
t = [int(i) for i in binascii.unhexlify(t)]
return NordicData([0xbd, len(t)] + t)
@ -196,15 +196,15 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_TIME)
self.assertEqual(msg.timestamp, int(ts))
assert msg.timestamp == int(ts)
def test_set_time(self, cb=None, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb6)
self.assertEqual(request.length, 6)
assert request.opcode == 0xb6
assert request.length == 6
str_timestamp = ''.join([f'{b:02x}' for b in request])
t = calendar.timegm(time.strptime(str_timestamp, '%y%m%d%H%M%S'))
self.assertEqual(int(t), int(ts))
assert int(t) == int(ts)
return SUCCESS
cb = cb or _cb
@ -214,8 +214,8 @@ class TestProtocolAny(unittest.TestCase):
def test_get_fw(self, cb=None, fw='abcdef-123456'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb7)
self.assertEqual(request.length, 1)
assert request.opcode == 0xb7
assert request.length == 1
data = [int(c, 16) for c in fw.split('-')[request[0]]]
return NordicData([0xb8, len(data) + 1, 0x00] + data)
@ -223,43 +223,43 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_FIRMWARE)
self.assertEqual(msg.firmware, fw)
assert msg.firmware == fw
def test_get_battery(self, cb=None, battery=(1, 78)):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb9)
self.assertEqual(request.length, 1)
assert request.opcode == 0xb9
assert request.length == 1
return NordicData([0xba, 2, battery[1], battery[0]])
cb = cb or _cb
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_BATTERY)
self.assertEqual(msg.battery_is_charging, battery[0])
self.assertEqual(msg.battery_percent, battery[1])
assert msg.battery_is_charging == battery[0]
assert msg.battery_percent == battery[1]
def test_get_width(self, cb=None):
# this is hardcoded for the spark
p = Protocol(self.protocol_version, callback=None)
msg = p.execute(Interactions.GET_WIDTH)
self.assertEqual(msg.width, 21000)
assert msg.width == 21000
def test_get_height(self, cb=None):
# this is hardcoded for the spark
p = Protocol(self.protocol_version, callback=None)
msg = p.execute(Interactions.GET_HEIGHT)
self.assertEqual(msg.height, 14800)
assert msg.height == 14800
def test_get_point_size(self, cb=None):
# this is hardcoded for the spark
p = Protocol(self.protocol_version, callback=None)
msg = p.execute(Interactions.GET_POINT_SIZE)
self.assertEqual(msg.point_size, 10)
assert msg.point_size == 10
def test_unknown_e3(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe3)
self.assertEqual(request.length, 1)
assert request.opcode == 0xe3
assert request.length == 1
return SUCCESS
cb = cb or _cb
@ -269,9 +269,9 @@ class TestProtocolAny(unittest.TestCase):
def test_filetransfer_reporting_type(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xec)
self.assertEqual(request.length, 6)
self.assertEqual(request, [0x06, 0x00, 0x00, 0x00, 0x00, 0x00])
assert request.opcode == 0xec
assert request.length == 6
assert request, [0x06, 0x00, 0x00, 0x00, 0x00 == 0x00]
return SUCCESS
cb = cb or _cb
@ -284,9 +284,9 @@ class TestProtocolAny(unittest.TestCase):
mode = Mode.LIVE
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb1)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], mode)
assert request.opcode == 0xb1
assert request.length == 1
assert request[0] == mode
return SUCCESS
cb = cb or _cb
@ -299,9 +299,9 @@ class TestProtocolAny(unittest.TestCase):
# this is a weird double call, see the protocol
# We reply 0xc7 first, and then 0xcd
if request is not None:
self.assertEqual(request.opcode, 0xc5)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xc5
assert request.length == 1
assert request[0] == 0x00
data = list(count.to_bytes(4, byteorder='big'))
return NordicData([0xc7, len(data)] + data)
else:
@ -313,14 +313,14 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_STROKES)
self.assertEqual(msg.count, count)
self.assertEqual(msg.timestamp, int(ts))
assert msg.count == count
assert msg.timestamp == int(ts)
def test_available_files_count(self, cb=None, ndata=1234):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xc1)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xc1
assert request.length == 1
assert request[0] == 0x00
data = list(ndata.to_bytes(2, byteorder='big'))
return NordicData([0xc2, len(data)] + data)
@ -328,13 +328,13 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.AVAILABLE_FILES_COUNT)
self.assertEqual(msg.count, ndata)
assert msg.count == ndata
def test_download_oldest_file(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xc3)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xc3
assert request.length == 1
assert request[0] == 0x00
return NordicData([0xc8, 1, 0xbe])
cb = cb or _cb
@ -344,9 +344,9 @@ class TestProtocolAny(unittest.TestCase):
def test_delete_oldest_file(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xca)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xca
assert request.length == 1
assert request[0] == 0x00
# no reply
cb = cb or _cb
@ -356,9 +356,9 @@ class TestProtocolAny(unittest.TestCase):
def test_register_complete(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe5)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xe5
assert request.length == 1
assert request[0] == 0x00
return SUCCESS
cb = cb or _cb
@ -368,16 +368,16 @@ class TestProtocolAny(unittest.TestCase):
def test_register_press_button(self, cb=None, uuid=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe3)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x01)
assert request.opcode == 0xe3
assert request.length == 1
assert request[0] == 0x01
# no reply
cb = cb or _cb
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.REGISTER_PRESS_BUTTON, uuid=uuid)
self.assertEqual(msg.uuid, uuid)
assert msg.uuid == uuid
def test_error_invalid_state(self):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
@ -386,17 +386,17 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=_cb)
# a "random" collection of requests that we want to check for
with self.assertRaises(DeviceError) as cm:
with pytest.raises(DeviceError) as cm:
p.execute(Interactions.CONNECT, uuid='abcdef123456')
self.assertEqual(cm.exception.errorcode, DeviceError.ErrorCode.GENERAL_ERROR)
assert cm.value.errorcode == DeviceError.ErrorCode.GENERAL_ERROR
with self.assertRaises(DeviceError) as cm:
with pytest.raises(DeviceError) as cm:
p.execute(Interactions.GET_STROKES)
self.assertEqual(cm.exception.errorcode, DeviceError.ErrorCode.GENERAL_ERROR)
assert cm.value.errorcode == DeviceError.ErrorCode.GENERAL_ERROR
with self.assertRaises(DeviceError) as cm:
with pytest.raises(DeviceError) as cm:
p.execute(Interactions.SET_MODE, Mode.PAPER)
self.assertEqual(cm.exception.errorcode, DeviceError.ErrorCode.GENERAL_ERROR)
assert cm.value.errorcode == DeviceError.ErrorCode.GENERAL_ERROR
class TestProtocolSpark(TestProtocolAny):
@ -404,14 +404,14 @@ class TestProtocolSpark(TestProtocolAny):
def test_register_wait_for_button(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertIsNone(request)
assert request is None
return NordicData([0xe4, 0x00])
cb = cb or _cb
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.REGISTER_WAIT_FOR_BUTTON)
self.assertEqual(msg.protocol_version, self.protocol_version)
assert msg.protocol_version == self.protocol_version
class TestProtocolSlate(TestProtocolSpark):
@ -419,9 +419,9 @@ class TestProtocolSlate(TestProtocolSpark):
def test_get_width(self, cb=None, width=1234):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xea)
self.assertEqual(request.length, 2)
self.assertEqual(request[0], 3)
assert request.opcode == 0xea
assert request.length == 2
assert request[0] == 3
data = [0x03, 0x00] + list(width.to_bytes(4, byteorder='little'))
return NordicData([0xeb, len(data)] + data)
@ -430,13 +430,13 @@ class TestProtocolSlate(TestProtocolSpark):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_WIDTH)
self.assertEqual(msg.width, width)
assert msg.width == width
def test_get_height(self, cb=None, height=4321):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xea)
self.assertEqual(request.length, 2)
self.assertEqual(request[0], 4)
assert request.opcode == 0xea
assert request.length == 2
assert request[0] == 4
data = [0x04, 0x00] + list(height.to_bytes(4, byteorder='little'))
return NordicData([0xeb, len(data)] + data)
@ -445,13 +445,13 @@ class TestProtocolSlate(TestProtocolSpark):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_HEIGHT)
self.assertEqual(msg.height, height)
assert msg.height == height
def test_get_strokes(self, cb=None, count=1024, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xcc)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xcc
assert request.length == 1
assert request[0] == 0x00
c = list(count.to_bytes(4, byteorder='little'))
t = time.strftime('%y%m%d%H%M%S', time.gmtime(ts))
t = [int(i) for i in binascii.unhexlify(t)]
@ -462,9 +462,9 @@ class TestProtocolSlate(TestProtocolSpark):
def test_available_files_count(self, cb=None, ndata=1234):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xc1)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xc1
assert request.length == 1
assert request[0] == 0x00
data = list(ndata.to_bytes(2, byteorder='little'))
return NordicData([0xc2, len(data)] + data)
@ -472,24 +472,24 @@ class TestProtocolSlate(TestProtocolSpark):
def test_delete_oldest_file(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xca)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xca
assert request.length == 1
assert request[0] == 0x00
return SUCCESS
super().test_delete_oldest_file(cb or _cb)
def test_register_press_button(self, cb=None, uuid='abcdef123456'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe7)
self.assertEqual(request.length, 6)
assert request.opcode == 0xe7
assert request.length == 6
# no reply
super().test_register_press_button(cb or _cb, uuid)
def test_register_wait_for_button(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertIsNone(request)
assert request is None
return NordicData([0xe4, 0x00])
super().test_register_wait_for_button(cb or _cb)
@ -500,34 +500,34 @@ class TestProtocolIntuosPro(TestProtocolSlate):
def test_connect(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe6)
self.assertEqual(request.length, 6)
assert request.opcode == 0xe6
assert request.length == 6
return NordicData([0x50, 0x06] + request) # replies with the uuid
super().test_connect(cb or _cb)
def test_get_name(self, cb=None, name='test dev name'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xdb)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xdb
assert request.length == 1
assert request[0] == 0x00
return NordicData([0xbc, len(name)] + list(bytes(name, encoding='ascii')))
super().test_get_name(cb or _cb, name=name)
def test_set_name(self, cb=None, name='test dev name'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xdb)
self.assertEqual(request.length, len(name))
self.assertEqual(bytes(request).decode('utf-8'), name)
assert request.opcode == 0xdb
assert request.length == len(name)
assert bytes(request).decode('utf-8') == name
return SUCCESS
super().test_set_name(cb or _cb, name=name)
def test_get_time(self, cb=None, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xd6)
self.assertEqual(request.length, 1)
assert request.opcode == 0xd6
assert request.length == 1
t = list(int(ts).to_bytes(length=4, byteorder='little')) + [0x00, 0x00]
return NordicData([0xbd, len(t)] + t)
@ -535,18 +535,18 @@ class TestProtocolIntuosPro(TestProtocolSlate):
def test_set_time(self, cb=None, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb6)
self.assertEqual(request.length, 6)
assert request.opcode == 0xb6
assert request.length == 6
t = int.from_bytes(request[0:4], byteorder='little')
self.assertEqual(int(t), int(ts))
assert int(t) == int(ts)
return SUCCESS
super().test_set_time(cb or _cb, ts=ts)
def test_get_fw(self, cb=None, fw='anything-string'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb7)
self.assertEqual(request.length, 1)
assert request.opcode == 0xb7
assert request.length == 1
data = bytes(fw.split('-')[request[0]].encode('utf8'))
return NordicData([0xb8, len(data) + 1, 0x00] + list(data))
@ -554,9 +554,9 @@ class TestProtocolIntuosPro(TestProtocolSlate):
def test_get_strokes(self, cb=None, count=1024, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xcc)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xcc
assert request.length == 1
assert request[0] == 0x00
c = list(count.to_bytes(4, byteorder='little'))
t = list(int(ts).to_bytes(4, byteorder='little'))
data = c + t
@ -566,16 +566,16 @@ class TestProtocolIntuosPro(TestProtocolSlate):
def test_register_wait_for_button(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertIsNone(request)
assert request is None
return NordicData([0x53, 0x00])
super().test_register_wait_for_button(cb or _cb)
def test_get_point_size(self, cb=None, pointsize=12):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xea)
self.assertEqual(request.length, 2)
self.assertEqual(request[0], 0x14)
assert request.opcode == 0xea
assert request.length == 2
assert request[0] == 0x14
ps = little_u32(pointsize)
return NordicData([0xeb, 6, 0x14, 0x00] + list(ps))
@ -583,8 +583,4 @@ class TestProtocolIntuosPro(TestProtocolSlate):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_POINT_SIZE)
self.assertEqual(msg.point_size, pointsize - 1)
if __name__ == "__main__":
unittest.main(sys.argv[1:])
assert msg.point_size == pointsize - 1

View File

@ -18,8 +18,8 @@
#
import os
import pytest
import sys
import unittest
import xdg.BaseDirectory
from pathlib import Path
import yaml
@ -27,39 +27,54 @@ import logging
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.protocol import *
from tuhi.util import flatten
from tuhi.protocol import * # noqa
from tuhi.util import flatten # noqa
logger = logging.getLogger('tuhi') # piggyback the debug messages
logger.setLevel(logging.DEBUG)
class TestLogFiles(unittest.TestCase):
'''
Special test class that loads a yaml file created by tuhi compiles
the StrokeData from it. This class autogenerates its own tests, see
the main() handling.
'''
def load_pen_data(self, filename):
def pytest_generate_tests(metafunc):
# for any test function that takes a "logfile" argument return the list
# of all current logfiles in XDG_DATA_HOME/tuhi
# This means the test gets better the more logfiles are present on the
# user's machine.
if 'logfile' in metafunc.fixturenames:
basedir = Path(xdg.BaseDirectory.xdg_data_home) / 'tuhi'
def loads_and_has_data(filename):
with open(filename) as fd:
try:
yml = yaml.load(fd, Loader=yaml.Loader)
return yml is not None
except Exception as e:
logger.error(f'Exception triggered by file {filename}')
raise e
logfiles = [f for f in basedir.glob('**/raw/log-*.yaml') if loads_and_has_data(f)]
metafunc.parametrize('logfile', logfiles)
def test_log_files(logfile):
def load_pen_data(filename):
with open(filename) as fd:
yml = yaml.load(fd, Loader=yaml.Loader)
# all recv lists that have source PEN
pendata = [d['recv'] for d in yml['data'] if 'recv' in d and 'source' in d and d['source'] == 'PEN']
return list(flatten(pendata))
def _test_file(self, fname):
data = self.load_pen_data(fname)
if not data: # Recordings without Pen data can be skipped
raise unittest.SkipTest()
StrokeFile(data)
data = load_pen_data(logfile)
if not data: # Recordings without Pen data can be skipped
pytest.skip('Recording without pen data')
StrokeFile(data)
class TestStrokeParsers(unittest.TestCase):
class TestStrokeParsers(object):
def test_identify_file_header(self):
data = [0x67, 0x82, 0x69, 0x65]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.FILE_HEADER)
assert StrokeDataType.identify(data) == StrokeDataType.FILE_HEADER
data = [0x62, 0x38, 0x62, 0x74]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.FILE_HEADER)
assert StrokeDataType.identify(data) == StrokeDataType.FILE_HEADER
others = [
# with header
@ -77,54 +92,54 @@ class TestStrokeParsers(unittest.TestCase):
[0x62, 0x38, 0x62, 0x73],
]
for data in others:
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.FILE_HEADER, msg=data)
assert StrokeDataType.identify(data) != StrokeDataType.FILE_HEADER, data
def test_identify_stroke_header(self):
data = [0xff, 0xfa] # two bytes are enough to identify
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.STROKE_HEADER)
assert StrokeDataType.identify(data) == StrokeDataType.STROKE_HEADER
data = [0x3, 0xfa] # lowest bits set, not a correct packet but identify doesn't care
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.STROKE_HEADER)
assert StrokeDataType.identify(data) == StrokeDataType.STROKE_HEADER
data = [0xfc, 0xfa] # lowest bits unset, must be something else
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.STROKE_HEADER)
assert StrokeDataType.identify(data) != StrokeDataType.STROKE_HEADER
def test_identify_stroke_point(self):
data = [0xff, 0xff, 0xff] # three bytes are enough to identify
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) == StrokeDataType.POINT
data = [0xff, 0xff, 0xff, 1, 2, 3, 4, 5, 6]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) == StrokeDataType.POINT
# wrong header, but observed in the wild
data = [0xbf, 0xff, 0xff, 1, 2, 3, 4, 5, 6]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) == StrokeDataType.POINT
data = [0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] # stroke end
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) != StrokeDataType.POINT
data = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] # EOF
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) != StrokeDataType.POINT
def test_identify_stroke_lost_point(self):
data = [0xff, 0xdd, 0xdd]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.LOST_POINT)
assert StrokeDataType.identify(data) == StrokeDataType.LOST_POINT
def test_identify_eof(self):
data = [0xff] * 9
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.EOF)
assert StrokeDataType.identify(data) == StrokeDataType.EOF
def test_identify_stroke_end(self):
data = [0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.STROKE_END)
assert StrokeDataType.identify(data) == StrokeDataType.STROKE_END
def test_identify_delta(self):
for i in range(256):
data = [i, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
if i & 0x3 == 0:
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.DELTA, f'packet: {data}')
assert StrokeDataType.identify(data), StrokeDataType.DELTA == f'packet: {data}'
else:
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.DELTA, f'packet: {data}')
assert StrokeDataType.identify(data), StrokeDataType.DELTA != f'packet: {data}'
def test_parse_stroke_header(self):
F_NEW_LAYER = 0x40
@ -134,81 +149,81 @@ class TestStrokeParsers(unittest.TestCase):
data = [0xff, 0xfa, flags, 0x1f, 0x73, 0x53, 0x5d, 0x2e, 0x01]
packet = StrokeHeader(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.is_new_layer, 1)
self.assertEqual(packet.pen_id, 0)
self.assertEqual(packet.pen_type, pen_type)
self.assertEqual(packet.timestamp, 1565750047)
assert packet.size == 9
assert packet.is_new_layer == 1
assert packet.pen_id == 0
assert packet.pen_type == pen_type
assert packet.timestamp == 1565750047
# new layer off
flags = pen_type
data = [0xff, 0xfa, flags, 0x1f, 0x73, 0x53, 0x5d, 0x2e, 0x01]
packet = StrokeHeader(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.is_new_layer, 0)
self.assertEqual(packet.pen_id, 0)
self.assertEqual(packet.pen_type, pen_type)
self.assertEqual(packet.timestamp, 1565750047)
assert packet.size == 9
assert packet.is_new_layer == 0
assert packet.pen_id == 0
assert packet.pen_type == pen_type
assert packet.timestamp == 1565750047
# pen type change
pen_type = 1
flags = F_NEW_LAYER | pen_type
data = [0xff, 0xfa, flags, 0x1f, 0x73, 0x53, 0x5d, 0x2e, 0x01]
packet = StrokeHeader(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.is_new_layer, 1)
self.assertEqual(packet.pen_id, 0)
self.assertEqual(packet.pen_type, pen_type)
self.assertEqual(packet.timestamp, 1565750047)
assert packet.size == 9
assert packet.is_new_layer == 1
assert packet.pen_id == 0
assert packet.pen_type == pen_type
assert packet.timestamp == 1565750047
# with pen id
flags = F_NEW_LAYER | F_PEN_ID | pen_type
pen_id = [0xff, 0x0a, 0x87, 0x75, 0x80, 0x28, 0x42, 0x00, 0x10]
data = [0xff, 0xfa, flags, 0x1f, 0x73, 0x53, 0x5d, 0x2e, 0x01] + pen_id
packet = StrokeHeader(data)
self.assertEqual(packet.size, 18)
self.assertEqual(packet.is_new_layer, 1)
self.assertEqual(packet.pen_id, 0x100042288075870a)
self.assertEqual(packet.pen_type, pen_type)
self.assertEqual(packet.timestamp, 1565750047)
assert packet.size == 18
assert packet.is_new_layer == 1
assert packet.pen_id == 0x100042288075870a
assert packet.pen_type == pen_type
assert packet.timestamp == 1565750047
def test_parse_stroke_point(self):
# 0xff means 2 bytes each for abs coords
data = [0xff, 0xff, 0xff, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
packet = StrokePoint(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.x, 0x0201)
self.assertEqual(packet.y, 0x0403)
self.assertEqual(packet.p, 0x0605)
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertIsNone(packet.dp)
assert packet.size == 9
assert packet.x == 0x0201
assert packet.y == 0x0403
assert packet.p == 0x0605
assert packet.dx is None
assert packet.dy is None
assert packet.dp is None
# 0xbf means: 1 byte for pressure delta, i.e. the 0x6 is skipped
data = [0xbf, 0xff, 0xff, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
packet = StrokePoint(data)
self.assertEqual(packet.size, 8)
self.assertEqual(packet.x, 0x0201)
self.assertEqual(packet.y, 0x0403)
self.assertIsNone(packet.p)
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertEqual(packet.dp, 0x5)
assert packet.size == 8
assert packet.x == 0x0201
assert packet.y == 0x0403
assert packet.p is None
assert packet.dx is None
assert packet.dy is None
assert packet.dp == 0x5
def test_parse_lost_point(self):
data = [0xff, 0xdd, 0xdd, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
packet = StrokeLostPoint(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.nlost, 0x0201)
assert packet.size == 9
assert packet.nlost == 0x0201
def test_parse_eof(self):
data = [0xff] * 9
packet = StrokeEOF(data)
self.assertEqual(packet.size, 9)
assert packet.size == 9
data = [0xfc] + [0xff] * 6
packet = StrokeEOF(data)
self.assertEqual(packet.size, 7)
assert packet.size == 7
def test_parse_delta(self):
x_delta = 0b00001000 # noqa
@ -221,93 +236,93 @@ class TestStrokeParsers(unittest.TestCase):
flags = x_delta
data = [flags, 1]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.dx, 1)
self.assertIsNone(packet.dy)
self.assertIsNone(packet.dp)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx == 1
assert packet.dy is None
assert packet.dp is None
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = y_delta
data = [flags, 2]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertIsNone(packet.dx)
self.assertEqual(packet.dy, 2)
self.assertIsNone(packet.dp)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx is None
assert packet.dy == 2
assert packet.dp is None
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = p_delta
data = [flags, 3]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertEqual(packet.dp, 3)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx is None
assert packet.dy is None
assert packet.dp == 3
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = x_delta | p_delta
data = [flags, 3, 5]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.dx, 3)
self.assertIsNone(packet.dy)
self.assertEqual(packet.dp, 5)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx == 3
assert packet.dy is None
assert packet.dp == 5
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = x_delta | y_delta | p_delta
data = [flags, 3, 5, 7]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.dx, 3)
self.assertEqual(packet.dy, 5)
self.assertEqual(packet.dp, 7)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx == 3
assert packet.dy == 5
assert packet.dp == 7
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = x_abs | y_abs | p_abs
data = [flags, 1, 2, 3, 4, 5, 6]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.x, 0x0201)
self.assertEqual(packet.y, 0x0403)
self.assertEqual(packet.p, 0x0605)
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertIsNone(packet.dp)
assert packet.size == len(data)
assert packet.x == 0x0201
assert packet.y == 0x0403
assert packet.p == 0x0605
assert packet.dx is None
assert packet.dy is None
assert packet.dp is None
flags = y_abs
data = [flags, 2, 3]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertIsNone(packet.x)
self.assertEqual(packet.y, 0x0302)
self.assertIsNone(packet.p)
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertIsNone(packet.dp)
assert packet.size == len(data)
assert packet.x is None
assert packet.y == 0x0302
assert packet.p is None
assert packet.dx is None
assert packet.dy is None
assert packet.dp is None
flags = x_abs | y_delta | p_delta
data = [flags, 2, 3, 4, 5]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.x, 0x0302)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
self.assertIsNone(packet.dx)
self.assertEqual(packet.dy, 4)
self.assertEqual(packet.dp, 5)
assert packet.size == len(data)
assert packet.x == 0x0302
assert packet.y is None
assert packet.p is None
assert packet.dx is None
assert packet.dy == 4
assert packet.dp == 5
class TestStrokes(unittest.TestCase):
class TestStrokes(object):
def test_single_stroke(self):
data = '''
67 82 69 65 22 73 53 5d 00 00 02 00 00 00 00 00 ff fa c3 1f
@ -428,39 +443,3 @@ class TestStrokes(unittest.TestCase):
p = Protocol(ProtocolVersion.INTUOS_PRO, None, None)
p.parse_pen_data(b)
# How does this work?
# The test generater spits out a simple test function that just calls the
# real test function
#
# Then we search for all yaml files with logs we have and generate a unique
# test name (based on the timestamp) for that file. Result: we have
# a unittests function for each log file found in the directory.
def generator(logfile):
def test(self):
self._test_file(logfile)
return test
def search_for_tests():
basedir = Path(xdg.BaseDirectory.xdg_data_home) / 'tuhi'
for logfile in basedir.glob('**/raw/log-*.yaml'):
with open(logfile) as fd:
try:
yml = yaml.load(fd, Loader=yaml.Loader)
if not yml:
continue
timestamp = yml['time']
test_name = f'test_log_{timestamp}'
test = generator(logfile)
setattr(TestLogFiles, test_name, test)
except Exception as e:
logger.error(f'Exception triggered by file {logfile}')
raise e
search_for_tests()
if __name__ == '__main__':
unittest.main()

49
tools/exporter.py Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
from pathlib import Path
import argparse
import json
import os
import sys
# This tool isn't installed, so we can assume that the tuhi module is always
# in the parent directory
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.export import JsonSvg, JsonPng
parser = argparse.ArgumentParser(description='Converter tool from Tuhi JSON files to SVG or PNG.')
parser.add_argument('filename', help='The JSON file to export ($HOME/.local/share/tuhi/*.json)')
parser.add_argument('--format',
help='The format to generate. Default: svg',
default='svg',
choices=['svg', 'png'])
parser.add_argument('--output',
type=str,
help='The output file name. Default: "$PWD/inputfile.suffix"',
default=None)
parser.add_argument('--orientation',
help='The orientation of the image',
default='landscape',
choices=['landscape', 'portrait', 'reverse-landscape', 'reverse-portrait'])
ns = parser.parse_args()
if ns.output is None:
ns.output = f"{Path(ns.filename).stem}.{ns.format}"
js = json.load(open(ns.filename))
if ns.format == 'svg':
JsonSvg(js, ns.orientation, ns.output)
elif ns.format == 'png':
JsonPng(js, ns.orientation, ns.output)

View File

@ -29,14 +29,14 @@ import configparser
from pathlib import Path
try:
from tuhi.svg import JsonSvg
from tuhi.export import JsonSvg, JsonPng
import tuhi.dbusclient
except ModuleNotFoundError:
# If PYTHONPATH isn't set up or we never installed Tuhi, the module
# isn't available. And since we don't install kete, we can assume that
# we're still in the git repo, so messing with the path is "fine".
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.svg import JsonSvg
from tuhi.export import JsonSvg, JsonPng
import tuhi.dbusclient
@ -208,7 +208,7 @@ class Searcher(Worker):
logger.error('Another client is already searching')
return
logger.debug(f'Starting searching')
logger.debug('Starting searching')
self.manager.start_search()
def stop(self):
@ -289,6 +289,7 @@ class Fetcher(Worker):
super(Fetcher, self).__init__(manager)
self.device = None
self.timestamps = None
self.format = args.format
address = args.address
index = args.index
@ -326,8 +327,12 @@ class Fetcher(Worker):
data = json.loads(jsondata)
t = time.localtime(data['timestamp'])
t = time.strftime('%Y-%m-%d-%H-%M', t)
path = f'{data["devicename"]}-{t}.svg'
JsonSvg(data, self.orientation, filename=path)
if self.format == 'png':
path = f'{data["devicename"]}-{t}.png'
JsonPng(data, self.orientation, filename=path)
else:
path = f'{data["devicename"]}-{t}.svg'
JsonSvg(data, self.orientation, filename=path)
logger.info(f'{data["devicename"]}: saved file "{path}"')
@ -699,6 +704,10 @@ class TuhiKeteShell(cmd.Cmd):
type=is_index_or_all,
const='all', nargs='?', default='all',
help='the index of the drawing to fetch or a literal "all"')
parser.add_argument('--format', metavar='{svg|png}',
default='svg',
choices=['svg', 'png'],
help='output file format')
try:
parsed_args = parser.parse_args(args.split())

View File

@ -28,11 +28,11 @@ import logging
# This tool isn't installed, so we can assume that the tuhi module is always
# in the parent directory
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.util import flatten
from tuhi.drawing import Drawing
from tuhi.protocol import StrokeFile
from tuhi.svg import JsonSvg
from tuhi.wacom import WacomProtocolSpark, WacomProtocolIntuosPro, WacomProtocolSlate
from tuhi.util import flatten # noqa
from tuhi.drawing import Drawing # noqa
from tuhi.protocol import StrokeFile # noqa
from tuhi.export import JsonSvg, JsonPng # noqa
from tuhi.wacom import WacomProtocolSpark, WacomProtocolIntuosPro, WacomProtocolSlate # noqa
logging.basicConfig(format='%(asctime)s %(levelname)s: %(name)s: %(message)s',
level=logging.INFO,
@ -68,6 +68,7 @@ def parse_file(filename, file_format, tablet_model, orientation):
# gotta convert to Drawings, then to json string, then to json, then
# to svg. ffs.
svgname = f'{stem}.svg'
pngname = f'{stem}.png'
jsonname = f'{stem}.json'
d = Drawing(svgname, (width * point_size, height * point_size), timestamp)
@ -85,10 +86,13 @@ def parse_file(filename, file_format, tablet_model, orientation):
with open(jsonname, 'w') as fd:
fd.write(d.to_json())
return
from io import StringIO
js = json.load(StringIO(d.to_json()))
JsonSvg(js, orientation, d.name)
else:
from io import StringIO
js = json.load(StringIO(d.to_json()))
if file_format == 'svg':
JsonSvg(js, orientation, d.name)
elif file_format == 'png':
JsonPng(js, orientation, pngname)
def fetch_files():
@ -142,7 +146,7 @@ def main(args=sys.argv):
parser.add_argument('--format',
help='The format to generate. Default: svg',
default='svg',
choices=['svg', 'json'])
choices=['svg', 'png', 'json'])
ns = parser.parse_args(args[1:])
if ns.verbose:

View File

@ -183,11 +183,15 @@ def drop_privileges():
os.setresuid(uid, uid, uid)
pw = pwd.getpwuid(uid)
path = os.environ['PATH']
display = os.environ['DISPLAY']
# we completely clear the environment and start a new and controlled one
os.environ.clear()
os.environ['XDG_RUNTIME_DIR'] = f'/run/user/{uid}'
os.environ['HOME'] = pw.pw_dir
os.environ['PATH'] = path
os.environ['DISPLAY'] = display
def parse(args):

View File

@ -21,11 +21,11 @@ from pathlib import Path
try:
from gi.repository import GObject, GLib
except Exception as e:
print(f'************ Importing gi.repository failed **********')
print(f'* This is an issue with the gi module, not with tuhi *')
print(f'******************************************************')
print(f'The full exception is below:')
print(f'')
print('************ Importing gi.repository failed **********')
print('* This is an issue with the gi module, not with tuhi *')
print('******************************************************')
print('The full exception is below:')
print('')
raise e
from tuhi.dbusserver import TuhiDBusServer
@ -37,7 +37,7 @@ DEFAULT_CONFIG_PATH = Path(xdg.BaseDirectory.xdg_data_home, 'tuhi')
logger = logging.getLogger('tuhi')
WACOM_COMPANY_IDS = [0x4755, 0x4157]
WACOM_COMPANY_IDS = [0x4755, 0x4157, 0x424d]
class TuhiDevice(GObject.Object):

View File

@ -206,11 +206,11 @@ class BlueZDevice(GObject.Object):
'''
i = self.obj.get_interface(ORG_BLUEZ_DEVICE1)
if self.connected:
self.logger.info(f'Device is already connected')
self.logger.info('Device is already connected')
self.emit('connected')
return
self.logger.debug(f'Connecting')
self.logger.debug('Connecting')
i.Connect(result_handler=self._on_connect_result)
def _on_connect_result(self, obj, result, user_data):
@ -219,7 +219,7 @@ class BlueZDevice(GObject.Object):
result.code == Gio.IOErrorEnum.DBUS_ERROR and
Gio.dbus_error_get_remote_error(result) == 'org.bluez.Error.Failed' and
'Operation already in progress' in result.message):
self.logger.debug(f'Already connecting')
self.logger.debug('Already connecting')
elif isinstance(result, Exception):
self.logger.error(f'Connection failed: {result}')
@ -230,11 +230,11 @@ class BlueZDevice(GObject.Object):
'''
i = self.obj.get_interface(ORG_BLUEZ_DEVICE1)
if not i.get_cached_property('Connected').get_boolean():
self.logger.info(f'Device is already disconnected')
self.logger.info('Device is already disconnected')
self.emit('disconnected')
return
self.logger.debug(f'Disconnecting')
self.logger.debug('Disconnecting')
i.Disconnect(result_handler=self._on_disconnect_result)
def _on_disconnect_result(self, obj, result, user_data):

View File

@ -426,7 +426,7 @@ class TuhiDBusDevice(_TuhiDBus):
fds_list = message.get_unix_fd_list()
if fds_list is None or fds_list.get_length() != 1:
logger.error(f'uhid fds not provided')
logger.error('uhid fds not provided')
result = GLib.Variant.new_int32(-errno.EINVAL)
invocation.return_value(GLib.Variant.new_tuple(result))
return

160
tuhi/export.py Normal file
View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
from gi.repository import GObject
import svgwrite
from svgwrite import mm
import cairo
class ImageExportBase(GObject.Object):
def __init__(self, json, orientation, filename, *args, **kwargs):
super().__init__(*args, **kwargs)
self.json = json
self.timestamp = json['timestamp']
self.filename = filename
self.orientation = orientation.lower()
self._convert()
@property
def output_dimensions(self):
dimensions = self.json['dimensions']
if dimensions == [0, 0]:
width, height = 100, 100
else:
# Original dimensions are too big for most Standards
# so we scale them down
width = dimensions[0] / self._output_scaling_factor
height = dimensions[1] / self._output_scaling_factor
if self.orientation in ['portrait', 'reverse-portrait']:
return height, width
else:
return width, height
@property
def output_strokes(self):
width, height = self.output_dimensions
strokes = []
for s in self.json['strokes']:
points_with_sk_width = []
for p in s['points']:
x, y = p['position']
# Scaling coordinates
x = x / self._output_scaling_factor
y = y / self._output_scaling_factor
if self.orientation == 'reverse-portrait':
x, y = y, height - x
elif self.orientation == 'portrait':
x, y = width - y, x
elif self.orientation == 'reverse-landscape':
x, y = width - x, height - y
# Pressure normalized range is [0, 0xffff]
delta = (p['pressure'] - 0x8000) / 0x8000
stroke_width = self._base_pen_width + self._pen_pressure_width_factor * delta
points_with_sk_width.append((x, y, stroke_width))
strokes.append(points_with_sk_width)
return strokes
class JsonSvg(ImageExportBase):
_output_scaling_factor = 1000
_base_pen_width = 0.4
_pen_pressure_width_factor = 0.2
# Change this value down to reduce size, change it up to improve accuracy. measured in px
_width_precision = 10
def _convert(self):
width, height = self.output_dimensions
size = width * mm, height * mm
# Make sure to set viewBox here so mm doesn't have to be specified in all later parts
svg = svgwrite.Drawing(filename=self.filename, size=size, viewBox=(f'0 0 {width} {height}'))
g = svgwrite.container.Group(id='layer0')
for sk_num, stroke_points in enumerate(self.output_strokes):
path = None
stroke_width_p = None
for i, (x, y, stroke_width) in enumerate(stroke_points):
if not x or not y:
continue
# Reduce precision of the width
stroke_width = int(stroke_width * self._width_precision) / self._width_precision
# Create a new path per object and per unique width
if stroke_width_p != stroke_width:
if path:
g.add(path)
# Reduce width by mm to px at 96dpi (see SVG/CSS specification)
width_px = stroke_width * 0.26458
path = svg.path(id=f'sk_{sk_num}_{i}', style=f'fill:none;stroke:black;stroke-width:{width_px}')
stroke_width_p = stroke_width
path.push("M", f'{x:.2f}', f'{y:.2f}')
else:
# Continue writing segment line with next coords
path.push("L", f'{x:.2f}', f'{y:.2f}')
if path:
g.add(path)
svg.add(g)
svg.save()
class JsonPng(ImageExportBase):
_output_scaling_factor = 100
_base_pen_width = 3
_pen_pressure_width_factor = 1
def _convert(self):
width, height = self.output_dimensions
width, height = int(width), int(height)
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx = cairo.Context(surface)
# Paint a transparent background
ctx.set_source_rgba(0, 0, 0, 0)
ctx.paint()
ctx.set_antialias(cairo.Antialias.DEFAULT)
ctx.set_line_join(cairo.LINE_JOIN_ROUND)
ctx.set_source_rgb(0, 0, 0)
for sk_num, stroke_points in enumerate(self.output_strokes):
for i, (x, y, stroke_width) in enumerate(stroke_points):
ctx.set_line_width(stroke_width)
if i == 0:
ctx.move_to(x, y)
else:
ctx.line_to(x, y)
ctx.stroke()
surface.write_to_png(self.filename)

View File

@ -46,7 +46,7 @@ class Config(GObject.Object):
if not self.path.exists():
return
logger.debug(f'configuration found')
logger.debug('configuration found')
self.config.read(self.path)
def _load_cached_drawings(self):

View File

@ -17,14 +17,16 @@ import xdg.BaseDirectory
import os
from pathlib import Path
from .config import Config
from tuhi.svg import JsonSvg
from tuhi.export import JsonSvg, JsonPng
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GObject, Gtk, GdkPixbuf, Gdk # NOQA
DATA_PATH = Path(xdg.BaseDirectory.xdg_cache_home, 'tuhi', 'svg')
DATA_PATH = Path(xdg.BaseDirectory.xdg_cache_home, 'tuhi')
SVG_DATA_PATH = Path(DATA_PATH, 'svg')
PNG_DATA_PATH = Path(DATA_PATH, 'png')
@Gtk.Template(resource_path='/org/freedesktop/Tuhi/ui/Drawing.ui')
@ -40,7 +42,8 @@ class Drawing(Gtk.EventBox):
super().__init__()
self.orientation = Config().orientation
Config().connect('notify::orientation', self._on_orientation_changed)
DATA_PATH.mkdir(parents=True, exist_ok=True)
SVG_DATA_PATH.mkdir(parents=True, exist_ok=True)
PNG_DATA_PATH.mkdir(parents=True, exist_ok=True)
self.json_data = json_data
self._zoom = zoom
@ -56,8 +59,12 @@ class Drawing(Gtk.EventBox):
self.redraw()
def process_svg(self):
path = os.fspath(Path(DATA_PATH, f'{self.json_data["timestamp"]}.svg'))
self.svg = JsonSvg(self.json_data, self.orientation, path)
path = os.fspath(Path(SVG_DATA_PATH, f'{self.json_data["timestamp"]}.svg'))
self.svg = JsonSvg(
self.json_data,
self.orientation,
path
)
width, height = -1, -1
if 'portrait' in self.orientation:
height = 1000
@ -68,6 +75,14 @@ class Drawing(Gtk.EventBox):
height=height,
preserve_aspect_ratio=True)
def process_png(self):
path = os.fspath(Path(PNG_DATA_PATH, f'{self.json_data["timestamp"]}.png'))
self.png = JsonPng(
self.json_data,
self.orientation,
path
)
def redraw(self):
ratio = self.pixbuf.get_height() / self.pixbuf.get_width()
base = 250 + self.zoom * 50
@ -97,11 +112,9 @@ class Drawing(Gtk.EventBox):
@Gtk.Template.Callback('_on_download_button_clicked')
def _on_download_button_clicked(self, button):
dialog = Gtk.FileChooserDialog(_('Please choose a file'),
None,
Gtk.FileChooserAction.SAVE,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
dialog = Gtk.FileChooserNative()
dialog.set_action(Gtk.FileChooserAction.SAVE)
dialog.set_transient_for(self.get_toplevel())
dialog.set_do_overwrite_confirmation(True)
# Translators: the default filename to save to
@ -115,21 +128,33 @@ class Drawing(Gtk.EventBox):
# Translators: filter to show svg files only
filter_svg.set_name(_('SVG files'))
filter_svg.add_pattern('*.svg')
filter_png = Gtk.FileFilter()
# Translators: filter to show png files only
filter_png.set_name(_('PNG files'))
filter_png.add_pattern('*.png')
dialog.add_filter(filter_svg)
dialog.add_filter(filter_png)
dialog.add_filter(filter_any)
response = dialog.run()
if response == Gtk.ResponseType.OK:
if response == Gtk.ResponseType.ACCEPT:
import shutil
# regenerate the SVG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_svg()
file = dialog.get_filename()
shutil.copyfile(self.svg.filename, file)
# FIXME: error handling
if file.lower().endswith('.png'):
# regenerate the PNG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_png()
shutil.move(self.png.filename, file)
else:
# regenerate the SVG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_svg()
shutil.copyfile(self.svg.filename, file)
# FIXME: error handling
dialog.destroy()

View File

@ -74,6 +74,11 @@ class DrawingPerspective(Gtk.Stack):
super().__init__(*args, **kwargs)
self.known_drawings = {} # type {timestamp: Drawing()}
self.flowboxes = {}
# Add an expanding emtpy label to the bottom - this pushes all the
# real stuff up to the top, forcing a nice alignment
fake_label = Gtk.Label("")
fake_label.show()
self.box_all_drawings.pack_end(fake_label, expand=True, fill=True, padding=100)
self._zoom = 0
self._want_listen = True
@ -87,7 +92,7 @@ class DrawingPerspective(Gtk.Stack):
def _hash(drawing):
return time.strftime('%Y%m', time.gmtime(drawing.timestamp))
for js in sorted(config.drawings, reverse=True, key=lambda j: j['timestamp']):
for js in sorted(config.drawings, key=lambda j: j['timestamp']):
ts = js['timestamp']
if ts in self.known_drawings:
continue
@ -102,7 +107,7 @@ class DrawingPerspective(Gtk.Stack):
except KeyError:
fb = Flowbox(time.gmtime(drawing.timestamp))
self.flowboxes[key] = fb
self.box_all_drawings.add(fb)
self.box_all_drawings.pack_end(fb, expand=False, fill=True, padding=0)
finally:
fb.insert(drawing)

View File

@ -137,7 +137,7 @@ class MainWindow(Gtk.ApplicationWindow):
dp = DrawingPerspective()
self._add_perspective(dp)
self.headerbar.set_title(f'Tuhi')
self.headerbar.set_title('Tuhi')
self.stack_perspectives.set_visible_child_name(dp.name)
if not self._tuhi.devices:

View File

@ -651,10 +651,18 @@ class MsgGetName(Msg):
opcode = 0xbb
protocol = ProtocolVersion.ANY
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = ""
def _handle_reply(self, reply):
if reply.opcode != 0xbc:
raise UnexpectedReply(f'Unknown reply: {reply.opcode}')
self.name = bytes(reply).decode('utf-8')
self.name += bytes(reply).decode('utf-8')
if bytes(reply)[-1] != 0x0a:
self.requires_request = False
self.execute()
self.requires_request = True
class MsgGetNameIntuosPro(Msg):
@ -1608,7 +1616,7 @@ class StrokeFile(object):
points.append(last_point)
else:
# should never get here
raise StrokeParsingError(f'Failed to parse', data[:16])
raise StrokeParsingError('Failed to parse', data[:16])
logger.debug(f'Offset {consumed}: {packet}')
consumed += packet.size
@ -1669,7 +1677,7 @@ class StrokeFileHeader(StrokePacket):
func = file_formats[key]
func(data)
except KeyError:
raise StrokeParsingError(f'Unknown file format:', data[:4])
raise StrokeParsingError('Unknown file format:', data[:4])
def __str__(self):
t = time.strftime("%y%m%d%H%M%S", time.gmtime(self.timestamp))
@ -1718,7 +1726,7 @@ class StrokeHeader(StrokePacket):
elif payload[0:3] == [0xff, 0xee, 0xee]:
self._parse_slate(data, header, payload)
else:
raise StrokeParsingError(f'Invalid StrokeHeader, expected ff fa or ff ee.', data[:8])
raise StrokeParsingError('Invalid StrokeHeader, expected ff fa or ff ee.', data[:8])
def _parse_slate(self, data, header, payload):
self.pen_id = 0
@ -1746,21 +1754,22 @@ class StrokeHeader(StrokePacket):
# if the pen id flag is set, the pen ID comes in the next 8-byte
# packet (plus 0xff header)
if needs_pen_id:
pen_packet = data[self.size + 1:]
pen_packet = data[self.size:] # header + 8 bytes payload
if not pen_packet:
raise StrokeParsingError('Missing pen ID packet')
header = data[0]
if header != 0xff:
raise StrokeParsingError(f'Unexpected pen id packet header: {header}.', data[:9])
pen_header = pen_packet[0]
if pen_header != 0xff:
raise StrokeParsingError(f'Unexpected pen id packet header: {pen_header}.', pen_packet[:9])
nbytes = bin(header).count('1')
self.pen_id = little_u64(pen_packet[:8])
pen_payload = pen_packet[1:]
nbytes = bin(pen_header).count('1')
self.pen_id = little_u64(pen_payload[:8])
self.size += 1 + nbytes
def __str__(self):
if self.timestamp is not None:
t = time.strftime(f'%y%m%d%H%M%S', time.gmtime(self.timestamp))
t = time.strftime('%y%m%d%H%M%S', time.gmtime(self.timestamp))
else:
t = time.strftime(f'boot+{self.time_offset/1000}s')
return f'StrokeHeader: time: {t} new layer: {self.is_new_layer}, pen type: {self.pen_type}, pen id: {self.pen_id:#x}'
@ -1814,7 +1823,7 @@ class StrokeDelta(object):
# 8 bit delta
delta = int.from_bytes(bytes([databytes[0]]), byteorder='little', signed=True)
if delta == 0:
raise StrokeParsingError(f'StrokeDelta: invalid delta of zero', data)
raise StrokeParsingError('StrokeDelta: invalid delta of zero', data)
assert delta != 0
size = 1
elif mask == 3:
@ -1824,7 +1833,7 @@ class StrokeDelta(object):
return value, delta, size
if (data[0] & 0b11) != 0:
raise NotImplementedError(f'LSB two bits set in mask - this is not supposed to happen')
raise NotImplementedError('LSB two bits set in mask - this is not supposed to happen')
xmask = (data[0] & 0b00001100) >> 2
ymask = (data[0] & 0b00110000) >> 4
@ -1869,7 +1878,7 @@ class StrokePoint(StrokeDelta):
header = data[0]
payload = data[1:]
if payload[:2] != [0xff, 0xff]:
raise StrokeParsingError(f'Invalid StrokePoint, expected ff ff ff', data[:9])
raise StrokeParsingError('Invalid StrokePoint, expected ff ff ff', data[:9])
# This is a wrapper around StrokeDelta which does the mask parsing.
# In theory the StrokePoint would be a separate packet but it
@ -1897,7 +1906,7 @@ class StrokeEOF(StrokePacket):
payload = data[1:]
nbytes = bin(header).count('1')
if payload[:nbytes] != [0xff] * nbytes:
raise StrokeParsingError(f'Invalid EOF, expected 0xff only', data[:9])
raise StrokeParsingError('Invalid EOF, expected 0xff only', data[:9])
self.size = nbytes + 1
@ -1907,7 +1916,7 @@ class StrokeEndOfStroke(StrokePacket):
payload = data[1:]
nbytes = bin(header).count('1')
if payload[:nbytes] != [0xff] * nbytes:
raise StrokeParsingError(f'Invalid EndOfStroke, expected 0xff only', data[:9])
raise StrokeParsingError('Invalid EndOfStroke, expected 0xff only', data[:9])
self.size = nbytes + 1
self.data = data[:self.size]
@ -1928,6 +1937,6 @@ class StrokeLostPoint(StrokePacket):
header = data[0]
payload = data[1:]
if payload[:2] != [0xdd, 0xdd]:
raise StrokeParsingError(f'Invalid StrokeLostPoint, expected ff dd dd', data[:9])
raise StrokeParsingError('Invalid StrokeLostPoint, expected ff dd dd', data[:9])
self.nlost = little_u16(payload[2:4])
self.size = bin(header).count('1') + 1

View File

@ -1,82 +0,0 @@
#!/usr/bin/env python3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
from gi.repository import GObject
import svgwrite
from svgwrite import mm
class JsonSvg(GObject.Object):
def __init__(self, json, orientation, filename, *args, **kwargs):
super().__init__(*args, **kwargs)
self.json = json
self.timestamp = json['timestamp']
self.filename = filename
self.orientation = orientation.lower()
self._convert()
def _convert(self):
js = self.json
dimensions = js['dimensions']
if dimensions == [0, 0]:
width, height = 100, 100
else:
# Original dimensions are too big for SVG Standard
# so we normalize them
width, height = dimensions[0] / 1000, dimensions[1] / 1000
if self.orientation in ['portrait', 'reverse-portrait']:
size = (height * mm, width * mm)
else:
size = (width * mm, height * mm)
svg = svgwrite.Drawing(filename=self.filename, size=size)
g = svgwrite.container.Group(id='layer0')
for stroke_num, s in enumerate(js['strokes']):
points_with_sk_width = []
for p in s['points']:
x, y = p['position']
# Normalize coordinates too
x, y = x / 1000, y / 1000
if self.orientation == 'reverse-portrait':
x, y = y, width - x
elif self.orientation == 'portrait':
x, y = height - y, x
elif self.orientation == 'reverse-landscape':
x, y = width - x, height - y
# Pressure normalized range is [0, 0xffff]
delta = (p['pressure'] - 0x8000) / 0x8000
stroke_width = 0.4 + 0.20 * delta
points_with_sk_width.append((x, y, stroke_width))
lines = svgwrite.container.Group(id=f'strokes_{stroke_num}', stroke='black')
for i, (x, y, stroke_width) in enumerate(points_with_sk_width):
if i != 0:
xp, yp, stroke_width_p = points_with_sk_width[i - 1]
lines.add(
svg.line(
start=(xp * mm, yp * mm),
end=(x * mm, y * mm),
stroke_width=stroke_width,
style='fill:none'
)
)
g.add(lines)
svg.add(g)
svg.save()

View File

@ -12,13 +12,13 @@
#
def list2hex(l, groupsize=8):
def list2hex(lst, groupsize=8):
'''Converts a list of integers to a two-letter hex string in the form
"1a 2b c3"'''
slices = []
for idx in range(0, len(l), groupsize):
s = ' '.join([f'{x:02x}' for x in l[idx:idx + groupsize]])
for idx in range(0, len(lst), groupsize):
s = ' '.join([f'{x:02x}' for x in lst[idx:idx + groupsize]])
slices.append(s)
return ' '.join(slices)

View File

@ -113,10 +113,10 @@ def b2hex(bs):
return ' '.join([''.join(s) for s in zip(hx[::2], hx[1::2])])
def list2hexlist(l):
def list2hexlist(lst):
'''Converts a list of integers to a two-letter prefixed hex string in the form
"[0x1a, 0x32, 0xab]"'''
return '[' + ', '.join([f'{x:#04x}' for x in l]) + ']'
return '[' + ', '.join([f'{x:#04x}' for x in lst]) + ']'
class DataLogger(object):
@ -204,7 +204,7 @@ class DataLogger(object):
self.logfile.write(f'name: {self.device.name}\n')
self.logfile.write(f'bluetooth: {self.btaddr}\n')
self.logfile.write(f'time: {timestamp} # host time: {time.strftime("%Y-%m-%d %H:%M:%S")}\n')
self.logfile.write(f'data:\n')
self.logfile.write('data:\n')
def _close_file(self):
if self.logfile is None:
@ -268,78 +268,6 @@ class DataLogger(object):
self._in_context = False
class WacomPacket(GObject.Object):
'''
A single protocol packet of variable length. The protocol format is a
single-byte bitmask followed by up to 8 bytes (depending on the number
of 1-bits in the bitmask). Each byte represents the matching bit in the
bitmask, i.e. the data is non-sparse.
If the bitmask has 0x1 and/or 0x2 set, those two bytes make up the
opcode of the command. So the possible layouts are:
| bitmask | opcode1 | opcode2 | payload ...
| bitmask | opcode1 | payload ...
| bitmask | opcode2 | payload ...
| bitmask | payload
On most normal packets containing motion data, the opcode is not
present.
Attributes:
bitmask .. single byte with a bitmask denoting the contents
opcode ... the 16-bit opcode or None for 'special' packets. Note that
the opcode is converted into an integer from the
little-endian protocol format
bytes .... a list of the payload bytes as sent by the device. This is
a non-sparse list matching the number of set bits in the
bitmask. it does not include the bitmask.
args ..... a sparse list of the payload bytes, expanded to match the
bitmask so that args[x] is the value for each bit x in
bitmask. it does not include the bitmask.
length ... length of the packet in bytes, including bitmask
'''
def __init__(self, data):
self.bitmask = data[0]
nbytes = bin(self.bitmask).count('1')
self.bytes = data[1:1 + nbytes]
self.length = nbytes + 1 # for the bitmask
idx = 0
# 2-byte opcode, but only if the bitmask is set for either byte
opcode = 0
if self.bitmask & 0x1:
opcode |= self.bytes[idx]
idx += 1
if self.bitmask & 0x2:
opcode |= self.bytes[idx] << 8
idx += 1
self.opcode = opcode if opcode else None
self.args = []
vals = self.bytes.copy()
mask = self.bitmask
while mask != 0:
self.args.append(vals.pop(0) if mask & 0x1 else 0x00)
mask >>= 1
def __repr__(self):
debug_data = []
debug_data.append(f'{self.bitmask:02x} ({self.bitmask:08b}) |')
if self.opcode:
debug_data.append(f'{self.opcode:04x} |')
else:
debug_data.append(f' |')
for i in range(2, 8): # start at 2 to skip the opcode
if self.bitmask & (1 << i):
debug_data.append(f'{self.args[i]:02x}')
else:
debug_data.append(' ')
return " ".join(debug_data)
class WacomProtocolLowLevelComm(GObject.Object):
'''
Internal class to handle the communication with the Wacom device.
@ -423,6 +351,13 @@ class WacomRegisterHelper(WacomProtocolLowLevelComm):
except AuthorizationError:
# this is expected
pass
except Exception as e:
logger.exception('Got other Exception while registering Spark device')
if e.errorcode == DeviceError.ErrorCode.GENERAL_ERROR:
logger.debug('Got GENERAL_ERROR while registering Spark device')
pass
else:
raise
# The "press button now command" on the spark
self.p.execute(Interactions.REGISTER_PRESS_BUTTON)
@ -506,7 +441,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm):
data = value[2:]
while data:
if bytes(data) == b'\xff\xff\xff\xff\xff\xff':
logger.debug(f'Pen left proximity')
logger.debug('Pen left proximity')
if self._uhid_device is not None:
self._uhid_device.call_input_event([1, 0, 0, 0, 0, 0, 0, 0])
@ -550,7 +485,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm):
tdelta = time.mktime(time.gmtime()) - time.mktime(t)
if abs(tdelta) > 300:
logger.error(f'device time is out by more than 5 minutes')
logger.error('device time is out by more than 5 minutes')
def get_battery_info(self):
msg = self.p.execute(Interactions.GET_BATTERY)
@ -717,7 +652,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm):
self.emit('drawing', drawing)
file_count -= 1
if TuhiConfig().peek_at_drawing:
logger.info(f'Not deleting drawing from device')
logger.info('Not deleting drawing from device')
if file_count > 0:
logger.info(f'{file_count} more files on device but I can only download the oldest one')
break
@ -951,7 +886,7 @@ class WacomDevice(GObject.Object):
protocol = ProtocolVersion.from_string(self._config['Protocol'])
self._init_protocol(protocol)
except (KeyError, ValueError):
logger.error(f'Missing or invalid Protocol entry in config file. Treating this device as unregistered')
logger.error('Missing or invalid Protocol entry in config file. Treating this device as unregistered')
self._uuid = None
def _init_protocol(self, protocol):
@ -1045,7 +980,7 @@ class WacomDevice(GObject.Object):
logger.error(f'**** Exception: {e} ****')
exception = e
except AuthorizationError as e:
logger.error(f'Authorization failed, device needs to be re-registered')
logger.error('Authorization failed, device needs to be re-registered')
exception = e
finally:
self.sync_state = 0