├── boot.sh
├── keyboard
├── current_program
├── programs
│ └── default
├── send.py
├── send_string.py
├── keymap.py
└── kb_client.py
├── desktop
├── requirements.txt
├── images
│ ├── bad.png
│ ├── heart.png
│ ├── trash.png
│ ├── Potion-icon.png
│ ├── Sword-icon.png
│ ├── Spell-Book-icon.png
│ ├── Adventure-Map-icon.png
│ ├── Crystal-Shard-icon.png
│ ├── Spell-Scroll-icon.png
│ ├── yammi-banana-icon.png
│ └── Destructive-Magic-icon.png
├── README.md
├── docs
│ └── index.html
├── systray.qrc
├── main.py
├── libmanager.py
└── window.py
├── uninstall.sh
├── .travis.yml
├── setup.sh
├── dbus
└── org.fruit2pi.btkbservice.conf
├── bluetooth.service
├── README.md
├── LICENSE
├── .gitignore
└── server
├── sdp_record.xml
└── btk_server.py
/boot.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/keyboard/current_program:
--------------------------------------------------------------------------------
1 | default
--------------------------------------------------------------------------------
/keyboard/programs/default:
--------------------------------------------------------------------------------
1 | fruit2pi.send(event)
--------------------------------------------------------------------------------
/desktop/requirements.txt:
--------------------------------------------------------------------------------
1 | pyside6
2 | pybluez
3 | cbor
4 |
5 |
--------------------------------------------------------------------------------
/desktop/images/bad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/bad.png
--------------------------------------------------------------------------------
/desktop/README.md:
--------------------------------------------------------------------------------
1 | # Build
2 | ## on Linux
3 | ```
4 | sudo apt install bluez libbluetooth-dev
5 | ```
6 |
7 |
--------------------------------------------------------------------------------
/desktop/images/heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/heart.png
--------------------------------------------------------------------------------
/desktop/images/trash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/trash.png
--------------------------------------------------------------------------------
/desktop/images/Potion-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/Potion-icon.png
--------------------------------------------------------------------------------
/desktop/images/Sword-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/Sword-icon.png
--------------------------------------------------------------------------------
/desktop/images/Spell-Book-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/Spell-Book-icon.png
--------------------------------------------------------------------------------
/desktop/images/Adventure-Map-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/Adventure-Map-icon.png
--------------------------------------------------------------------------------
/desktop/images/Crystal-Shard-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/Crystal-Shard-icon.png
--------------------------------------------------------------------------------
/desktop/images/Spell-Scroll-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/Spell-Scroll-icon.png
--------------------------------------------------------------------------------
/desktop/images/yammi-banana-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/yammi-banana-icon.png
--------------------------------------------------------------------------------
/desktop/images/Destructive-Magic-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ailisp/fruit2pi-keyboard/master/desktop/images/Destructive-Magic-icon.png
--------------------------------------------------------------------------------
/uninstall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -f "bluetooth.service.bk" ] ; then
4 | sudo cp bluetooth.service.bk /lib/systemd/system/bluetooth.service
5 | sudo systemctl daemon-reload
6 | sudo /etc/init.d/bluetooth start
7 | fi
--------------------------------------------------------------------------------
/desktop/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fruit2Pi Keyboard Help
6 | Fruit2Pi Keyboard Help
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | before_install:
3 | - echo -e "before install"
4 | install:
5 | - echo -e "install"
6 | script:
7 | - sudo ./boot.sh
8 | notifications:
9 | email:
10 | on_success: change
11 | on_failure: chang
12 |
--------------------------------------------------------------------------------
/desktop/systray.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | images/bad.png
4 | images/heart.png
5 | images/trash.png
6 | images/Adventure-Map-icon.png
7 | images/Spell-Book-icon.png
8 | images/Spell-Scroll-icon.png
9 | images/Sword-icon.png
10 | images/yammi-banana-icon.png
11 |
12 |
13 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | apt install -y tmux
3 | apt install -y bluez bluez-tools bluez-firmware
4 | apt install -y python3 python3-dev python3-dbus python3-pyudev python3-evdev python3-gi python3-cbor
5 |
6 | cp dbus/org.fruit2pi.btkbservice.conf /etc/dbus-1/system.d
7 | cp /lib/systemd/system/bluetooth.service ./bluetooth.service.bk
8 | cp bluetooth.service /lib/systemd/system/bluetooth.service
9 | systemctl daemon-reload
10 | systemctl start bluetooth.service
11 | systemctl enable bluetooth.service
12 |
--------------------------------------------------------------------------------
/dbus/org.fruit2pi.btkbservice.conf:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/bluetooth.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Bluetooth service
3 | Documentation=man:bluetoothd(8)
4 | ConditionPathIsDirectory=/sys/class/bluetooth
5 |
6 | [Service]
7 | Type=dbus
8 | BusName=org.bluez
9 | ExecStart=/usr/lib/bluetooth/bluetoothd --noplugin=input
10 | NotifyAccess=main
11 | #WatchdogSec=10
12 | #Restart=on-failure
13 | CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
14 | LimitNPROC=1
15 | ProtectHome=true
16 | ProtectSystem=full
17 |
18 | [Install]
19 | WantedBy=bluetooth.target
20 | Alias=dbus-org.bluez.service
21 |
--------------------------------------------------------------------------------
/desktop/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from PySide6.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon
4 |
5 | from window import Window
6 | from libmanager import APP_NAME
7 |
8 | if __name__ == "__main__":
9 | app = QApplication()
10 |
11 | if not QSystemTrayIcon.isSystemTrayAvailable():
12 | QMessageBox.critical(None, APP_NAME, "Cannot detect any system tray on system")
13 | sys.exit(1)
14 |
15 | QApplication.setQuitOnLastWindowClosed(False)
16 |
17 | window = Window()
18 | window.show()
19 | sys.exit(app.exec_())
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Fruit2Pi Fully Programmable Bluetooth Keyboard
2 |
3 | # Setup steps
4 |
5 | ## Setup
6 |
7 | On Raspberry Pi Zero W, flush a Raspberry Pi Lite OS. Login with username `pi` and password `raspberry`.
8 | Bootup and connect to WIFI with `raspi-config` command.
9 | Then install git and clone this repo:
10 | ```
11 | sudo apt update
12 | sudo apt install git
13 | git clone https://github.com/ailisp/BL_KEYBOARD_RPI
14 | ```
15 |
16 | Then install and start this tool (will also make this tool start on boot):
17 | ```
18 | cd BL_KEYBOARD_RPI
19 | sudo ./setup.sh
20 | ```
21 |
--------------------------------------------------------------------------------
/keyboard/send.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import sys
3 | import dbus
4 |
5 |
6 | class BtkDataClient():
7 | def __init__(self):
8 | self.bus = dbus.SystemBus()
9 | self.btkservice = self.bus.get_object(
10 | 'org.fruit2pi.btkbservice', '/org/fruit2pi/btkbservice')
11 | self.iface = dbus.Interface(self.btkservice, 'org.fruit2pi.btkbservice')
12 |
13 | def send_control_data(self, data):
14 | self.iface.send_control_data(data)
15 |
16 | def send_data(self, data):
17 | self.iface.send_data(data)
18 |
19 |
20 | if __name__ == "__main__":
21 | argv = sys.argv
22 | if(len(argv) < 2):
23 | print("Usage: send.py [-c] num1 num2 ... numn ")
24 | exit()
25 | send_control = False
26 | if argv[1] == '-c':
27 | send_control = True
28 | del argv[1]
29 | data = list(map(int, sys.argv[1:]))
30 | dc = BtkDataClient()
31 | if send_control:
32 | print("Sending control: ", data)
33 | dc.send_control_data(data)
34 | else:
35 | print("Sending: ", data)
36 | dc.send_data(data)
37 | print("Done.")
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 quangthanh010290
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | # mypy
101 | .mypy_cache/
102 | bluetooth.service.bk
103 |
104 | .vscode
--------------------------------------------------------------------------------
/keyboard/send_string.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import os # used to all external commands
3 | import sys # used to exit the script
4 | import dbus
5 | import dbus.service
6 | import dbus.mainloop.glib
7 | import time
8 | # import thread
9 | import keymap
10 |
11 |
12 | class BtkStringClient():
13 | # constants
14 | KEY_DOWN_TIME = 0.01
15 | KEY_DELAY = 0.01
16 |
17 | def __init__(self):
18 | # the structure for a bt keyboard input report (size is 10 bytes)
19 | self.state = [
20 | 0xA1, # this is an input report
21 | 0x01, # Usage report = Keyboard
22 | # Bit array for Modifier keys
23 | [0, # Right GUI - Windows Key
24 | 0, # Right ALT
25 | 0, # Right Shift
26 | 0, # Right Control
27 | 0, # Left GUI
28 | 0, # Left ALT
29 | 0, # Left Shift
30 | 0], # Left Control
31 | 0x00, # Vendor reserved
32 | 0x00, # rest is space for 6 keys
33 | 0x00,
34 | 0x00,
35 | 0x00,
36 | 0x00,
37 | 0x00]
38 | self.scancodes = {" ": "KEY_SPACE"}
39 | # connect with the Bluetooth keyboard server
40 | print("setting up DBus Client")
41 | self.bus = dbus.SystemBus()
42 | self.btkservice = self.bus.get_object(
43 | 'org.fruit2pi.btkbservice', '/org/fruit2pi/btkbservice')
44 | self.iface = dbus.Interface(self.btkservice, 'org.fruit2pi.btkbservice')
45 |
46 | def send_key_state(self):
47 | """sends a single frame of the current key state to the emulator server"""
48 | bin_str = ""
49 | element = self.state[2]
50 | for bit in element:
51 | bin_str += str(bit)
52 | self.iface.send_keys(int(bin_str, 2), self.state[4:10])
53 |
54 | def send_key_down(self, scancode):
55 | """sends a key down event to the server"""
56 | self.state[4] = scancode
57 | self.send_key_state()
58 |
59 | def send_key_up(self):
60 | """sends a key up event to the server"""
61 | self.state[4] = 0
62 | self.send_key_state()
63 |
64 | def send_string(self, string_to_send):
65 | for c in string_to_send:
66 | cu = c.upper()
67 | if(cu in self.scancodes):
68 | scantablekey = self.scancodes[cu]
69 | else:
70 | scantablekey = "KEY_"+c.upper()
71 | print(scantablekey)
72 | scancode = keymap.keytable[scantablekey]
73 | self.send_key_down(scancode)
74 | time.sleep(BtkStringClient.KEY_DOWN_TIME)
75 | self.send_key_up()
76 | time.sleep(BtkStringClient.KEY_DELAY)
77 |
78 |
79 | if __name__ == "__main__":
80 | if(len(sys.argv) < 2):
81 | print("Usage: send_string 0.5:\n fruit2pi.send(event)']))
89 | print(send_command(sock, ['set', 'default']))
90 |
91 | else:
92 | print("could not find target bluetooth device nearby")
93 |
--------------------------------------------------------------------------------
/server/sdp_record.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/keyboard/keymap.py:
--------------------------------------------------------------------------------
1 | #
2 | # Convert value returned from Linux event device ("evdev") to a HID code. This
3 | # is reverse of what's actually hardcoded in the kernel.
4 | #
5 | # Thanh Le
6 | # License: GPL
7 | #
8 | # Ported to a Python module by Thanh Le
9 | #
10 |
11 | keytable = {
12 | "KEY_RESERVED" : 0,
13 | "KEY_ESC" : 41,
14 | "KEY_1" : 30,
15 | "KEY_2" : 31,
16 | "KEY_3" : 32,
17 | "KEY_4" : 33,
18 | "KEY_5" : 34,
19 | "KEY_6" : 35,
20 | "KEY_7" : 36,
21 | "KEY_8" : 37,
22 | "KEY_9" : 38,
23 | "KEY_0" : 39,
24 | "KEY_MINUS" : 45,
25 | "KEY_EQUAL" : 46,
26 | "KEY_BACKSPACE" : 42,
27 | "KEY_TAB" : 43,
28 | "KEY_Q" : 20,
29 | "KEY_W" : 26,
30 | "KEY_E" : 8,
31 | "KEY_R" : 21,
32 | "KEY_T" : 23,
33 | "KEY_Y" : 28,
34 | "KEY_U" : 24,
35 | "KEY_I" : 12,
36 | "KEY_O" : 18,
37 | "KEY_P" : 19,
38 | "KEY_LEFTBRACE" : 47,
39 | "KEY_RIGHTBRACE" : 48,
40 | "KEY_ENTER" : 40,
41 | "KEY_LEFTCTRL" : 224,
42 | "KEY_A" : 4,
43 | "KEY_S" : 22,
44 | "KEY_D" : 7,
45 | "KEY_F" : 9,
46 | "KEY_G" : 10,
47 | "KEY_H" : 11,
48 | "KEY_J" : 13,
49 | "KEY_K" : 14,
50 | "KEY_L" : 15,
51 | "KEY_SEMICOLON" : 51,
52 | "KEY_APOSTROPHE" : 52,
53 | "KEY_GRAVE" : 53,
54 | "KEY_LEFTSHIFT" : 225,
55 | "KEY_BACKSLASH" : 50,
56 | "KEY_Z" : 29,
57 | "KEY_X" : 27,
58 | "KEY_C" : 6,
59 | "KEY_V" : 25,
60 | "KEY_B" : 5,
61 | "KEY_N" : 17,
62 | "KEY_M" : 16,
63 | "KEY_COMMA" : 54,
64 | "KEY_DOT" : 55,
65 | "KEY_SLASH" : 56,
66 | "KEY_RIGHTSHIFT" : 229,
67 | "KEY_KPASTERISK" : 85,
68 | "KEY_LEFTALT" : 226,
69 | "KEY_SPACE" : 44,
70 | "KEY_CAPSLOCK" : 57,
71 | "KEY_F1" : 58,
72 | "KEY_F2" : 59,
73 | "KEY_F3" : 60,
74 | "KEY_F4" : 61,
75 | "KEY_F5" : 62,
76 | "KEY_F6" : 63,
77 | "KEY_F7" : 64,
78 | "KEY_F8" : 65,
79 | "KEY_F9" : 66,
80 | "KEY_F10" : 67,
81 | "KEY_NUMLOCK" : 83,
82 | "KEY_SCROLLLOCK" : 71,
83 | "KEY_KP7" : 95,
84 | "KEY_KP8" : 96,
85 | "KEY_KP9" : 97,
86 | "KEY_KPMINUS" : 86,
87 | "KEY_KP4" : 92,
88 | "KEY_KP5" : 93,
89 | "KEY_KP6" : 94,
90 | "KEY_KPPLUS" : 87,
91 | "KEY_KP1" : 89,
92 | "KEY_KP2" : 90,
93 | "KEY_KP3" : 91,
94 | "KEY_KP0" : 98,
95 | "KEY_KPDOT" : 99,
96 | "KEY_ZENKAKUHANKAKU" : 148,
97 | "KEY_102ND" : 100,
98 | "KEY_F11" : 68,
99 | "KEY_F12" : 69,
100 | "KEY_RO" : 135,
101 | "KEY_KATAKANA" : 146,
102 | "KEY_HIRAGANA" : 147,
103 | "KEY_HENKAN" : 138,
104 | "KEY_KATAKANAHIRAGANA" : 136,
105 | "KEY_MUHENKAN" : 139,
106 | "KEY_KPJPCOMMA" : 140,
107 | "KEY_KPENTER" : 88,
108 | "KEY_RIGHTCTRL" : 228,
109 | "KEY_KPSLASH" : 84,
110 | "KEY_SYSRQ" : 70,
111 | "KEY_RIGHTALT" : 230,
112 | "KEY_HOME" : 74,
113 | "KEY_UP" : 82,
114 | "KEY_PAGEUP" : 75,
115 | "KEY_LEFT" : 80,
116 | "KEY_RIGHT" : 79,
117 | "KEY_END" : 77,
118 | "KEY_DOWN" : 81,
119 | "KEY_PAGEDOWN" : 78,
120 | "KEY_INSERT" : 73,
121 | "KEY_DELETE" : 76,
122 | "KEY_MUTE" : 239,
123 | "KEY_VOLUMEDOWN" : 238,
124 | "KEY_VOLUMEUP" : 237,
125 | "KEY_POWER" : 102,
126 | "KEY_KPEQUAL" : 103,
127 | "KEY_PAUSE" : 72,
128 | "KEY_KPCOMMA" : 133,
129 | "KEY_HANGEUL" : 144,
130 | "KEY_HANJA" : 145,
131 | "KEY_YEN" : 137,
132 | "KEY_LEFTMETA" : 227,
133 | "KEY_RIGHTMETA" : 231,
134 | "KEY_COMPOSE" : 101,
135 | "KEY_STOP" : 243,
136 | "KEY_AGAIN" : 121,
137 | "KEY_PROPS" : 118,
138 | "KEY_UNDO" : 122,
139 | "KEY_FRONT" : 119,
140 | "KEY_COPY" : 124,
141 | "KEY_OPEN" : 116,
142 | "KEY_PASTE" : 125,
143 | "KEY_FIND" : 244,
144 | "KEY_CUT" : 123,
145 | "KEY_HELP" : 117,
146 | "KEY_CALC" : 251,
147 | "KEY_SLEEP" : 248,
148 | "KEY_WWW" : 240,
149 | "KEY_COFFEE" : 249,
150 | "KEY_BACK" : 241,
151 | "KEY_FORWARD" : 242,
152 | "KEY_EJECTCD" : 236,
153 | "KEY_NEXTSONG" : 235,
154 | "KEY_PLAYPAUSE" : 232,
155 | "KEY_PREVIOUSSONG" : 234,
156 | "KEY_STOPCD" : 233,
157 | "KEY_REFRESH" : 250,
158 | "KEY_EDIT" : 247,
159 | "KEY_SCROLLUP" : 245,
160 | "KEY_SCROLLDOWN" : 246,
161 | "KEY_F13" : 104,
162 | "KEY_F14" : 105,
163 | "KEY_F15" : 106,
164 | "KEY_F16" : 107,
165 | "KEY_F17" : 108,
166 | "KEY_F18" : 109,
167 | "KEY_F19" : 110,
168 | "KEY_F20" : 111,
169 | "KEY_F21" : 112,
170 | "KEY_F22" : 113,
171 | "KEY_F23" : 114,
172 | "KEY_F24" : 115
173 | }
174 |
175 | # Map modifier keys to array element in the bit array
176 | modkeys = {
177 | "KEY_RIGHTMETA" : 0,
178 | "KEY_RIGHTALT" : 1,
179 | "KEY_RIGHTSHIFT" : 2,
180 | "KEY_RIGHTCTRL" : 3,
181 | "KEY_LEFTMETA" : 4,
182 | "KEY_LEFTALT": 5,
183 | "KEY_LEFTSHIFT": 6,
184 | "KEY_LEFTCTRL": 7
185 | }
186 |
187 | def convert(evdev_keycode):
188 | return keytable[evdev_keycode]
189 |
190 | def modkey(evdev_keycode):
191 | if evdev_keycode in modkeys:
192 | return modkeys[evdev_keycode]
193 | else:
194 | return -1 # Return an invalid array element
195 |
--------------------------------------------------------------------------------
/server/btk_server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | #
3 | # fruit2pi Bluetooth keyboard/Mouse emulator DBUS Service
4 | #
5 |
6 | from __future__ import absolute_import, print_function
7 | from optparse import OptionParser, make_option
8 | import os
9 | import sys
10 | import uuid
11 | import dbus
12 | import dbus.service
13 | import dbus.mainloop.glib
14 | import time
15 | import socket
16 | from gi.repository import GLib
17 | from dbus.mainloop.glib import DBusGMainLoop
18 | import logging
19 | from logging import debug, info, warning, error
20 |
21 |
22 | logging.basicConfig(level=logging.DEBUG)
23 |
24 | class BTKbDevice():
25 | # change these constants
26 | MY_DEV_NAME = "Fruit2pi_Keyboard"
27 |
28 | # define some constants
29 | P_CTRL = 17 # Service port - must match port configured in SDP record
30 | P_INTR = 19 # Service port - must match port configured in SDP record#Interrrupt port
31 | # dbus path of the bluez profile we will create
32 | # file path of the sdp record to load
33 | SDP_RECORD_PATH = sys.path[0] + "/sdp_record.xml"
34 | UUID = "00001124-0000-1000-8000-00805f9b34fb"
35 |
36 | def __init__(self):
37 | print("2. Setting up BT device")
38 | self.init_bt_device()
39 | self.init_bluez_profile()
40 | global fruit2pi
41 | fruit2pi = self
42 |
43 | # configure the bluetooth hardware device
44 | def init_bt_device(self):
45 | print("3. Configuring Device name " + BTKbDevice.MY_DEV_NAME)
46 | # set the device class to a keybord and set the name
47 | os.system("hciconfig hci0 up")
48 | os.system("hciconfig hci0 class 0x0025C0")
49 | os.system("hciconfig hci0 name " + BTKbDevice.MY_DEV_NAME)
50 | # make the device discoverable
51 | os.system("hciconfig hci0 piscan")
52 |
53 | # set up a bluez profile to advertise device capabilities from a loaded service record
54 | def init_bluez_profile(self):
55 | print("4. Configuring Bluez Profile")
56 | # setup profile options
57 | service_record = self.read_sdp_service_record()
58 | opts = {
59 | "AutoConnect": True,
60 | "ServiceRecord": service_record
61 | }
62 | # retrieve a proxy for the bluez profile interface
63 | bus = dbus.SystemBus()
64 | manager = dbus.Interface(bus.get_object(
65 | "org.bluez", "/org/bluez"), "org.bluez.ProfileManager1")
66 | manager.RegisterProfile("/org/bluez/hci0", BTKbDevice.UUID, opts)
67 | print("6. Profile registered ")
68 |
69 | # read and return an sdp record from a file
70 | def read_sdp_service_record(self):
71 | print("5. Reading service record")
72 | try:
73 | fh = open(BTKbDevice.SDP_RECORD_PATH, "r")
74 | except:
75 | sys.exit("Could not open the sdp record. Exiting...")
76 | return fh.read()
77 |
78 | # listen for incoming client connections
79 | def listen(self):
80 | print("\033[0;33m7. Waiting for connections\033[0m")
81 | self.scontrol = socket.socket(
82 | socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) # BluetoothSocket(L2CAP)
83 | self.sinterrupt = socket.socket(
84 | socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) # BluetoothSocket(L2CAP)
85 | self.scontrol.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
86 | self.sinterrupt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
87 | # bind these sockets to a port - port zero to select next available
88 | self.scontrol.bind((socket.BDADDR_ANY, self.P_CTRL))
89 | self.sinterrupt.bind((socket.BDADDR_ANY, self.P_INTR))
90 |
91 | # Start listening on the server sockets
92 | self.scontrol.listen(5)
93 | self.sinterrupt.listen(5)
94 | self.accept_conn()
95 |
96 | def accept_conn(self):
97 | self.ccontrol, cinfo = self.scontrol.accept()
98 | print (
99 | "\033[0;32mGot a connection on the control channel from %s \033[0m" % cinfo[0])
100 |
101 | self.cinterrupt, cinfo = self.sinterrupt.accept()
102 | print (
103 | "\033[0;32mGot a connection on the interrupt channel from %s \033[0m" % cinfo[0])
104 |
105 | # send a string to the bluetooth host machine
106 | def send_string(self, message):
107 | try:
108 | print('--------------')
109 | print(bytes(message))
110 | self.cinterrupt.send(bytes(message))
111 | except OSError as err:
112 | print('error in send_string')
113 | error(err)
114 | self.cinterrupt.close()
115 | self.ccontrol.close()
116 | self.accept_conn()
117 |
118 | def send_control_string(self, message):
119 | try:
120 | print('--------------')
121 | print(bytes(message))
122 | self.ccontrol.send(bytes(message))
123 | except OSError as err:
124 | print('error in send_control_string')
125 | error(err)
126 | self.cinterrupt.close()
127 | self.ccontrol.close()
128 | self.accept_conn()
129 |
130 |
131 | class BTKbService(dbus.service.Object):
132 | def __init__(self):
133 | print("1. Setting up service")
134 | # set up as a dbus service
135 | bus_name = dbus.service.BusName(
136 | "org.fruit2pi.btkbservice", bus=dbus.SystemBus())
137 | dbus.service.Object.__init__(
138 | self, bus_name, "/org/fruit2pi/btkbservice")
139 | # create and setup our device
140 | self.device = BTKbDevice()
141 | # start listening for connections
142 | self.device.listen()
143 |
144 | @dbus.service.method('org.fruit2pi.btkbservice', in_signature='yay')
145 | def send_key(self, modifier_byte, keys):
146 | global current_program
147 | print("Get on_receive_keys request through dbus")
148 | print("key msg: ", keys)
149 | state = [ 0xA1, 1, 0, 0, 0, 0, 0, 0, 0, 0 ]
150 | state[2] = int(modifier_byte)
151 | count = 4
152 | for key_code in keys:
153 | if(count < 10):
154 | state[count] = int(key_code)
155 | count += 1
156 | self.device.send_string(state)
157 |
158 | @dbus.service.method('org.fruit2pi.btkbservice', in_signature='yay')
159 | def send_mouse(self, modifier_byte, keys):
160 | state = [0xA1, 2, 0, 0, 0, 0]
161 | count = 2
162 | for key_code in keys:
163 | if(count < 6):
164 | state[count] = int(key_code)
165 | count += 1
166 | self.device.send_string(state)
167 |
168 |
169 | # main routine
170 | if __name__ == "__main__":
171 | # we an only run as root
172 | try:
173 | if not os.geteuid() == 0:
174 | sys.exit("Only root can run this script")
175 |
176 | DBusGMainLoop(set_as_default=True)
177 | myservice = BTKbService()
178 | loop = GLib.MainLoop()
179 | loop.run()
180 | except KeyboardInterrupt:
181 | sys.exit()
182 |
--------------------------------------------------------------------------------
/keyboard/kb_client.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | #
3 | # Fruit2pi Bluetooth keyboard emulation service
4 | # keyboard copy client.
5 | # Reads local key events and forwards them to the btk_server DBUS service
6 | #
7 | import os # used to all external commands
8 | import sys # used to exit the script
9 | import dbus
10 | import dbus.service
11 | import dbus.mainloop.glib
12 | import time
13 | import evdev # used to get input from the keyboard
14 | from evdev import *
15 | import keymap # used to map evdev input to hid keodes
16 | import selectors
17 | from cbor._cbor import dumps, loads
18 | import socket
19 |
20 | programs_dir = os.path.join(sys.path[0], 'programs')
21 |
22 | def list_programs():
23 | global current_program
24 | return {'programs': os.listdir(programs_dir), 'current_program': current_program['name']}
25 |
26 | def edit_program(name, code):
27 | with open(os.path.join(programs_dir, name), 'w') as f:
28 | f.write(code)
29 | return {'status': 'success'}
30 |
31 | def delete_programs(names):
32 | for name in names:
33 | f = os.path.join(programs_dir, name)
34 | if os.path.exists(f):
35 | os.remove(f)
36 | return {'status': 'success'}
37 |
38 | def load_program(name):
39 | with open(os.path.join(programs_dir, name)) as f:
40 | program = f.read()
41 | return {'program': program}
42 |
43 | def set_program(name):
44 | global current_program
45 | with open(os.path.join(programs_dir, name)) as f:
46 | program = f.read()
47 | current_program = {'name': name, 'program': program}
48 | with open(os.path.join(sys.path[0], 'current_program'), 'w') as f:
49 | f.write(name)
50 | return {'status': 'success', 'current_program': name}
51 |
52 |
53 | def process_command(data):
54 | try:
55 | cmds = loads(data)
56 | except:
57 | return {'error': 'parse'}
58 | if type(cmds) != list or not cmds:
59 | return {'error': 'format'}
60 | cmd = cmds[0]
61 | args = cmds[1:]
62 | if cmd == 'list':
63 | return list_programs()
64 | elif cmd == 'edit':
65 | if len(args) != 2:
66 | return {'error': 'format'}
67 | name = args[0]
68 | code = args[1]
69 | return edit_program(name, code)
70 | elif cmd == 'delete':
71 | if not args:
72 | return {'error': 'format'}
73 | return delete_programs(args)
74 | elif cmd == 'set':
75 | if len(args) != 1:
76 | return {'error': 'format'}
77 | name = args[0]
78 | return set_program(name)
79 | elif cmd == 'load':
80 | if len(args) != 1:
81 | return {'error': 'format'}
82 | name = args[0]
83 | return load_program(name)
84 | else:
85 | return {'error': 'format'}
86 |
87 |
88 | fruit2pi = None
89 | current_program = None
90 |
91 |
92 | # Define a client to listen to local key events
93 | class Keyboard():
94 |
95 | def __init__(self):
96 | name = None
97 | with open(os.path.join(sys.path[0], 'current_program')) as f:
98 | try:
99 | name = f.read()
100 | except:
101 | name = 'default'
102 | set_program(name)
103 |
104 | # the structure for a bt keyboard input report (size is 10 bytes)
105 | self.state = [
106 | 0xA1, # this is an input report
107 | 0x01, # Usage report = Keyboard
108 | # Bit array for Modifier keys
109 | [0, # Right GUI - Windows Key
110 | 0, # Right ALT
111 | 0, # Right Shift
112 | 0, # Right Control
113 | 0, # Left GUI
114 | 0, # Left ALT
115 | 0, # Left Shift
116 | 0], # Left Control
117 | 0x00, # Vendor reserved
118 | 0x00, # rest is space for 6 keys
119 | 0x00,
120 | 0x00,
121 | 0x00,
122 | 0x00,
123 | 0x00]
124 |
125 | print("setting up DBus Client")
126 |
127 | self.config_dbus()
128 | global fruit2pi
129 | fruit2pi = self
130 | print("waiting for keyboard")
131 | # keep trying to key a keyboard
132 | have_dev = False
133 | self.scommand = socket.socket(
134 | socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)
135 | self.scommand.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
136 | self.scommand.bind((socket.BDADDR_ANY, 21))
137 | self.scommand.listen(5)
138 | self.scommand.setblocking(False)
139 | while have_dev == False:
140 | try:
141 | # try and get a keyboard - should always be event0 as
142 | # we're only plugging one thing in
143 | self.dev = InputDevice("/dev/input/event0")
144 | have_dev = True
145 | except OSError:
146 | print("Keyboard not found, waiting 3 seconds and retrying")
147 | time.sleep(3)
148 | print("found a keyboard")
149 |
150 | def config_dbus(self):
151 | self.bus = dbus.SystemBus()
152 | self.btkservice = self.bus.get_object(
153 | 'org.fruit2pi.btkbservice', '/org/fruit2pi/btkbservice')
154 | self.iface = dbus.Interface(self.btkservice, 'org.fruit2pi.btkbservice')
155 |
156 | def change_state(self, event):
157 | evdev_code = ecodes.KEY[event.code]
158 | modkey_element = keymap.modkey(evdev_code)
159 |
160 | if modkey_element > 0:
161 | if self.state[2][modkey_element] == 0:
162 | self.state[2][modkey_element] = 1
163 | else:
164 | self.state[2][modkey_element] = 0
165 | else:
166 | # Get the keycode of the key
167 | hex_key = keymap.convert(ecodes.KEY[event.code])
168 | # Loop through elements 4 to 9 of the inport report structure
169 | for i in range(4, 10):
170 | if self.state[i] == hex_key and event.value == 0:
171 | # Code 0 so we need to depress it
172 | self.state[i] = 0x00
173 | elif self.state[i] == 0x00 and event.value == 1:
174 | # if the current space if empty and the key is being pressed
175 | self.state[i] = hex_key
176 | break
177 | return self.state
178 |
179 | # poll for keyboard events
180 | def event_loop(self):
181 | sel = selectors.DefaultSelector()
182 | def do_kbd_ev(fd, mask):
183 | for event in self.dev.read():
184 | if event.type == ecodes.EV_KEY and event.value < 2:
185 | event = self.change_state(event)
186 | eval(compile(current_program['program'], current_program['name'], 'exec'))
187 | def do_cmd(conn, mask):
188 | data = conn.recv(65535) # Should be ready
189 | if data:
190 | resp = process_command(data)
191 | conn.send(dumps(resp))
192 | else:
193 | print('closing', conn)
194 | sel.unregister(conn)
195 | conn.close()
196 | def do_cmd_conn(sock, mask):
197 | self.ccommand, cinfo = sock.accept()
198 | self.ccommand.setblocking(False)
199 | print (
200 | "\033[0;32mGot a connection on the command port from %s \033[0m" % cinfo[0])
201 | sel.register(self.ccommand, selectors.EVENT_READ, do_cmd)
202 | sel.register(self.dev.fd, selectors.EVENT_READ, do_kbd_ev)
203 | sel.register(self.scommand, selectors.EVENT_READ, do_cmd_conn)
204 | global current_program
205 | while True:
206 | try:
207 | events = sel.select()
208 | for key, mask in events:
209 | callback = key.data
210 | callback(key.fileobj, mask)
211 | except dbus.DBusException as e:
212 | print('A dbus error occurred:', file=sys.stderr)
213 | print(e.__repr__(), file=sys.stderr)
214 | print('reconfig dbus', file=sys.stderr)
215 | self.config_dbus()
216 | except BaseException as e:
217 | print('An error occurred:', file=sys.stderr)
218 | print(e.__repr__(), file=sys.stderr)
219 | print(e, file=sys.stderr)
220 |
221 | # forward keyboard events to the dbus service
222 | def send(self, event):
223 | bin_str = ""
224 | state = event
225 | print(*state)
226 | element = state[2]
227 | for bit in element:
228 | bin_str += str(bit)
229 | self.iface.send_key(int(bin_str, 2), self.state[4:10])
230 |
231 |
232 | if __name__ == "__main__":
233 | print("Setting up keyboard")
234 | kb = Keyboard()
235 |
236 | print("starting event loop")
237 | kb.event_loop()
238 |
--------------------------------------------------------------------------------
/desktop/window.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtCore import Slot, QSize, QThread, Signal, Qt
2 | from PySide6.QtGui import QAction, QIcon, QStandardItemModel, QStandardItem, QBrush, QColor, QCloseEvent
3 | from PySide6.QtWidgets import (QCheckBox, QComboBox, QDialog,
4 | QGridLayout, QGroupBox, QHBoxLayout, QLabel,
5 | QLineEdit, QMenu, QMessageBox, QPushButton,
6 | QSpinBox, QStyle, QSystemTrayIcon, QTextEdit,
7 | QVBoxLayout, QTabWidget, QTextBrowser, QWidget,
8 | QListView)
9 | from bluetooth.bluez import BluetoothSocket
10 | import rc_systray
11 | from libmanager import APP_NAME, Config, connect_keyboard, find_keyboard, send_command
12 | import time
13 |
14 | class ConnectKeyboard(QThread):
15 | found = Signal(str)
16 | connected = Signal(BluetoothSocket)
17 | def run(self):
18 | config = Config()
19 | keyboardAddr = config.get('addr') or find_keyboard()
20 | self.found.emit(keyboardAddr)
21 | sock = connect_keyboard(keyboardAddr)
22 | self.connected.emit(sock)
23 |
24 | class ListProgram(QThread):
25 | listed = Signal(list, str)
26 |
27 | def __init__(self, sock):
28 | super(ListProgram, self).__init__()
29 | self.sock = sock
30 |
31 | def run(self):
32 | res = send_command(self.sock, ['list'])
33 | print(res)
34 | self.listed.emit(res['programs'], res['current_program'])
35 |
36 | class DeleteProgram(QThread):
37 | deleted = Signal(str)
38 |
39 | def __init__(self, sock, name):
40 | super(DeleteProgram, self).__init__()
41 | self.sock = sock
42 | self.name = name
43 |
44 | def run(self):
45 | send_command(self.sock, ['delete', self.name])
46 | self.deleted.emit(self.name)
47 |
48 | class SetProgram(QThread):
49 | setDone = Signal(str)
50 |
51 | def __init__(self, sock, name):
52 | super(SetProgram, self).__init__()
53 | self.sock = sock
54 | self.name = name
55 |
56 | def run(self):
57 | send_command(self.sock, ['set', self.name])
58 | self.setDone.emit(self.name)
59 |
60 | class EditProgram(QThread):
61 | edited = Signal(str)
62 |
63 | def __init__(self, sock, name, program):
64 | super(EditProgram, self).__init__()
65 | self.sock = sock
66 | self.name = name
67 | self.program = program
68 |
69 | def run(self):
70 | send_command(self.sock, ['edit', self.name, self.program])
71 | self.edited.emit(self.name)
72 |
73 | class LoadProgram(QThread):
74 | loaded = Signal(str, str)
75 |
76 | def __init__(self, sock, name):
77 | super(LoadProgram, self).__init__()
78 | self.sock = sock
79 | self.name = name
80 |
81 | def run(self):
82 | program = send_command(self.sock, ['load', self.name])['program']
83 | self.loaded.emit(self.name, program)
84 |
85 | class Window(QDialog):
86 | def __init__(self, parent=None):
87 | super(Window, self).__init__(parent)
88 | self.createTrayIcon()
89 | self.createProgramsList()
90 | self.createCodeEditPage()
91 | self.logsPage = QTextBrowser()
92 | self.documentation = QTextBrowser()
93 |
94 | self.tabWidget = QTabWidget()
95 | self.tabWidget.setIconSize(QSize(64, 64))
96 | self.tabWidget.addTab(self.programsListPage, QIcon(":/images/Adventure-Map-icon.png"), "Programs")
97 | self.tabWidget.addTab(self.codeEditPage, QIcon(":/images/Sword-icon.png"), "Edit Program")
98 | self.tabWidget.addTab(self.logsPage, QIcon(":/images/Spell-Scroll-icon.png"), "Logs")
99 | self.tabWidget.addTab(self.documentation, QIcon(":/images/Spell-Book-icon.png"), "Documentation")
100 |
101 | self.mainLayout = QVBoxLayout()
102 | self.mainLayout.addWidget(self.tabWidget)
103 | self.setLayout(self.mainLayout)
104 |
105 | self.setWindowTitle(APP_NAME)
106 | self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.Dialog)
107 | self.resize(800, 600)
108 |
109 | self.systrayHintMsgShowed = False
110 | self.firstShow = True
111 | self.fromQuit = False
112 |
113 | def createProgramsList(self):
114 | self.programsListModel = QStandardItemModel(0, 1, self)
115 | self.programsList = QListView()
116 | self.programsList.setModel(self.programsListModel)
117 | self.programsListPage = QWidget()
118 | self.programsListLayout = QVBoxLayout()
119 | self.programsListButtons = QHBoxLayout()
120 | self.programsListButtonNew = QPushButton("New")
121 | self.programsListButtonDelete = QPushButton("Delete")
122 | self.programsListButtonEdit = QPushButton("Edit")
123 | self.programsListButtonSet = QPushButton("Set")
124 | self.programsListButtons.addWidget(self.programsListButtonNew)
125 | self.programsListButtons.addWidget(self.programsListButtonSet)
126 | self.programsListButtons.addWidget(self.programsListButtonEdit)
127 | self.programsListButtons.addWidget(self.programsListButtonDelete)
128 | self.programsListLayout.addLayout(self.programsListButtons)
129 | self.programsListLayout.addWidget(self.programsList)
130 | self.programsListPage.setLayout(self.programsListLayout)
131 | self.programsListButtonNew.clicked.connect(self.newProgram)
132 | self.programsListButtonEdit.clicked.connect(self.editProgram)
133 | self.programsListButtonDelete.clicked.connect(self.deleteProgram)
134 | self.programsListButtonSet.clicked.connect(self.setProgram)
135 |
136 | def createCodeEditPage(self):
137 | self.codeEditPage = QWidget()
138 | self.codeEditLayout = QVBoxLayout()
139 | self.codeEditNameBox = QHBoxLayout()
140 | self.codeEditNameBoxNameLabel = QLabel("Name:")
141 | self.codeEditNameBoxNameInput = QLineEdit()
142 | self.codeEditNameBoxSaveButton = QPushButton("Save")
143 | self.codeEditNameBoxCancelButton = QPushButton("Cancel")
144 | self.codeEditNameBox.addWidget(self.codeEditNameBoxNameLabel)
145 | self.codeEditNameBox.addWidget(self.codeEditNameBoxNameInput)
146 | self.codeEditNameBox.addWidget(self.codeEditNameBoxSaveButton)
147 | self.codeEditNameBox.addWidget(self.codeEditNameBoxCancelButton)
148 | self.codeEdit = QTextEdit()
149 | self.codeEditLayout.addLayout(self.codeEditNameBox)
150 | self.codeEditLayout.addWidget(self.codeEdit)
151 | self.codeEditPage.setLayout(self.codeEditLayout)
152 | self.codeEditLastCode = ''
153 | self.codeEditNameBoxSaveButton.clicked.connect(self.saveEditProgram)
154 | self.codeEditNameBoxCancelButton.clicked.connect(self.cancelEditProgram)
155 |
156 | def showEvent(self, event):
157 | super().showEvent(event)
158 | if self.firstShow:
159 | self.firstShow = False
160 | self.createWaitDialog()
161 | self.findKeyboard()
162 |
163 | def closeWaitDialog(self):
164 | time.sleep(1)
165 | self.waitDialog.close()
166 |
167 | @Slot()
168 | def newProgram(self):
169 | self.showNormal()
170 | self.tabWidget.setCurrentWidget(self.codeEditPage)
171 | self.codeEdit.setPlainText("")
172 | self.codeEditLastCode = ''
173 | self.codeEditNameBoxNameInput.setText("")
174 |
175 | @Slot()
176 | def editProgram(self):
177 | selected = self.programsList.selectedIndexes()
178 | if not selected:
179 | return
180 | selected = selected[0].data()
181 | loadProgram = LoadProgram(self.cmdSocket, selected)
182 | loadProgram.loaded.connect(self.programLoaded)
183 | loadProgram.start()
184 | self.showWaitDialog("Loading program ...")
185 |
186 | @Slot()
187 | def saveEditProgram(self):
188 | name = self.codeEditNameBoxNameInput.text()
189 | program = self.codeEdit.toPlainText()
190 | if not name:
191 | return
192 | editProgramWorker = EditProgram(self.cmdSocket, name, program)
193 | editProgramWorker.edited.connect(self.programSaved)
194 | editProgramWorker.start()
195 | self.showWaitDialog("Saving program ...")
196 |
197 | @Slot()
198 | def cancelEditProgram(self):
199 | self.codeEdit.setPlainText(self.codeEditLastCode)
200 |
201 | @Slot()
202 | def deleteProgram(self):
203 | selected = self.programsList.selectedIndexes()
204 | if not selected:
205 | return
206 | selected = selected[0].data()
207 | deleteWorker = DeleteProgram(self.cmdSocket, selected)
208 | deleteWorker.deleted.connect(self.programDeleted)
209 | deleteWorker.start()
210 | self.showWaitDialog("Deleting program ...")
211 |
212 | @Slot()
213 | def setProgram(self):
214 | selected = self.programsList.selectedIndexes()
215 | if not selected:
216 | return
217 | selected = selected[0].data()
218 | setWorker = SetProgram(self.cmdSocket, selected)
219 | setWorker.setDone.connect(self.programSet)
220 | setWorker.start()
221 | self.showWaitDialog("Setting program ...")
222 |
223 | @Slot(str, str)
224 | def programLoaded(self, name, program):
225 | self.closeWaitDialog()
226 | self.tabWidget.setCurrentWidget(self.codeEditPage)
227 | self.codeEdit.setPlainText(program)
228 | self.codeEditLastCode = program
229 | self.codeEditNameBoxNameInput.setText(name)
230 |
231 | @Slot(str)
232 | def programDeleted(self, name):
233 | self.updateProgramsList()
234 |
235 | @Slot(str)
236 | def programSet(self, name):
237 | self.updateProgramsList()
238 |
239 | @Slot(str)
240 | def programSaved(self, name):
241 | self.updateProgramsList()
242 | self.tabWidget.setCurrentWidget(self.programsListPage)
243 |
244 | def createWaitDialog(self):
245 | self.waitDialog = QDialog(self)
246 | self.waitDialogLayout = QHBoxLayout()
247 | self.waitDialogLabel = QLabel()
248 | self.waitDialogLayout.addWidget(self.waitDialogLabel)
249 | self.waitDialog.setLayout(self.waitDialogLayout)
250 | self.waitDialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
251 |
252 | def showWaitDialog(self, text='Please wait...'):
253 | self.waitDialogLabel.setText(text)
254 | self.waitDialog.exec_()
255 |
256 | def findKeyboard(self):
257 | find = ConnectKeyboard()
258 | find.connected.connect(self.keyboardConnected)
259 | find.start()
260 | self.showWaitDialog('Finding and connecting Fruit2Pi Keyboard ...')
261 |
262 | @Slot(BluetoothSocket)
263 | def keyboardConnected(self, socket):
264 | self.cmdSocket = socket
265 | self.updateProgramsList()
266 |
267 | def updateProgramsList(self):
268 | self.listProgram = ListProgram(self.cmdSocket)
269 | self.listProgram.listed.connect(self.programListUpdated)
270 | self.listProgram.start()
271 |
272 | @Slot(list, str)
273 | def programListUpdated(self, programs, current_program):
274 | print(programs)
275 | print(current_program)
276 | self.closeWaitDialog()
277 | self.programsListModel.clear()
278 | for p in programs:
279 | item = QStandardItem(p)
280 | if p == current_program:
281 | item.setForeground(QBrush(QColor(0, 0, 255, 127)))
282 | self.programsListModel.appendRow(item)
283 |
284 | def setVisible(self, visible):
285 | super().setVisible(visible)
286 |
287 | def closeEvent(self, event):
288 | if self.fromQuit:
289 | return
290 | if not event.spontaneous() or not self.isVisible():
291 | return
292 | if not self.systrayHintMsgShowed:
293 | self.systrayHintMsgShowed = True
294 | icon = QIcon(":/images/yammi-banana-icon.png")
295 | self.trayIcon.showMessage(APP_NAME,
296 | "Running on background"
297 | "To quit, choose Quit in the icon menu",
298 | icon,
299 | 5000
300 | )
301 | self.hide()
302 | event.ignore()
303 |
304 | @Slot(str)
305 | def iconActivated(self, reason):
306 | print(reason)
307 | if reason == QSystemTrayIcon.Trigger:
308 | self.showNormal()
309 | if reason == QSystemTrayIcon.DoubleClick:
310 | self.showNormal()
311 |
312 | @Slot()
313 | def showProgramsPage(self):
314 | self.showNormal()
315 | self.tabWidget.setCurrentWidget(self.programsListPage)
316 |
317 | @Slot()
318 | def showLogsPage(self):
319 | self.showNormal()
320 | self.tabWidget.setCurrentWidget(self.logsPage)
321 |
322 | @Slot()
323 | def showDocumentation(self):
324 | self.showNormal()
325 | self.tabWidget.setCurrentWidget(self.documentation)
326 |
327 | @Slot()
328 | def quit(self):
329 | self.fromQuit = True
330 | qApp.quit()
331 |
332 | def createTrayIcon(self):
333 | self.showProgramsAction = QAction("Programs", self)
334 | self.showProgramsAction.triggered.connect(self.showProgramsPage)
335 | self.showNewProgramAction = QAction("New Program", self)
336 | self.showNewProgramAction.triggered.connect(self.newProgram)
337 | self.showSetProgramAction = QAction("Logs", self)
338 | self.showSetProgramAction.triggered.connect(self.showLogsPage)
339 | self.showDocumentationAction = QAction("Documentation", self)
340 | self.showDocumentationAction.triggered.connect(self.showDocumentation)
341 | self.quitAction = QAction("Quit", self)
342 | self.quitAction.triggered.connect(self.quit)
343 |
344 | self.trayIconMenu = QMenu(self)
345 | self.trayIconMenu.addAction(self.showProgramsAction)
346 | self.trayIconMenu.addAction(self.showSetProgramAction)
347 | self.trayIconMenu.addAction(self.showNewProgramAction)
348 | self.trayIconMenu.addAction(self.showDocumentationAction)
349 | self.trayIconMenu.addSeparator()
350 | self.trayIconMenu.addAction(self.quitAction)
351 | self.trayIcon = QSystemTrayIcon(self)
352 |
353 | self.trayIcon.setContextMenu(self.trayIconMenu)
354 | self.trayIcon.activated.connect(self.iconActivated)
355 | self.trayIcon.setIcon(QIcon(":/images/yammi-banana-icon.png"))
356 | self.trayIcon.show()
357 |
--------------------------------------------------------------------------------