├── 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 | --------------------------------------------------------------------------------