diff --git a/.android b/.android new file mode 120000 index 0000000..d2882e7 --- /dev/null +++ b/.android @@ -0,0 +1 @@ +../NineRiFt-base/.android \ No newline at end of file diff --git a/.gitignore b/.gitignore index 194e205..905e80c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,8 @@ __pycache__/ .idea/ bin/ build/ -develop-eggs/ .buildozer/ +develop-eggs/ dist/ downloads/ eggs/ @@ -36,7 +36,7 @@ MANIFEST # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec +.spec # Installer logs pip-log.txt @@ -109,3 +109,11 @@ venv.bak/ # mypy .mypy_cache/ + +# JetBrains stuff +.idea/ + +.android/bin +.android/ndk +.android/sdk +/packaging/ninerift-1a.svg diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7374fdd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule ".android/able"] + path = .android/able + url = https://github.com/b3b/able +[submodule "vendor/py9b"] + path = vendor/py9b + url = https://github.com/Slinger360/py9b diff --git a/README b/README new file mode 100644 index 0000000..c9cbba3 --- /dev/null +++ b/README @@ -0,0 +1,49 @@ +This is an Open-Source Ninebot and Xiaomi compatible scooter interface app. +THIS TOOL IS ONLY FOR USE ON DEVICES YOU OWN!!! It is still being added to but is in a functional state. + + +This application is written in Python3. To install required libraries, run the following two commands: + +git clone --recursive https://github.com/slinger360/NineRiFt-kivy.git +pip install -r requirements.txt + +To build for Android read up on Buildozer. + +After that, you can either run NineRiFt on your Windows, Mac, or Linux machine by opening main.py using your Python3 interpreter or you can use a prebuilt APK for Android (you could also compile a build if you want). + +On Android, BLE and TCP-Serial is supported. + +On, Windows, Mac, and Linux, BLE, Serial, and TCP is supported. + + +The Download screen is for downloading firmware: + +1. Select device you need firmware for in the dropdown on the left + +2. Select the firmware version you need + +3. Click "Download it!" and wait for download to complete + + +The Flash screen is for flashing firmware: + +1. (Optional) Type the first few digits or the full length of the MAC address of the target scooter for flashing + +2. Select the interface you want to use to connect (if wired, plug it in first) + +3. Select the part you wish to flash + +4. Select the firmware file you want flashed to the target scooter. DO NOT SELECT AN MD5 FILE!!! THIS IS NOT THE FIRMWARE!!! + +5. Click "Flash it!" and wait for flashing to complete + + + +At the moment only Segway-Ninebot SNSC, ES1, ES2, and ES4 and Xiaomi M365 and M365 Pro are supported. + + +SNSC dashboards cannot be flashed without either TCP-Serial or Serial interface. + + + +If you appreciate my work, be sure to donate at https://PayPal.com/dilsha21 or any of the other options listed on my GitHub. diff --git a/changesn.kv b/changesn.kv new file mode 100644 index 0000000..49525f7 --- /dev/null +++ b/changesn.kv @@ -0,0 +1,19 @@ +GridLayout: + cols: 1 + rows: 2 + GridLayout: + rows: 2 + cols: 1 + size_hint_y: .15 + Label: + font_size: '12sp' + height: '12sp' + text: "NewSN:" + TextInput: + multiline: False + font_size: '12sp' + height: '12sp' + on_text_validate: app.com.setnewsn(self.text) + font_size: '12sp' + BoxLayout: + size_hint_y: .85 diff --git a/dump.kv b/dump.kv new file mode 100644 index 0000000..f344ec1 --- /dev/null +++ b/dump.kv @@ -0,0 +1,12 @@ +GridLayout: + cols: 1 + rows: 2 + Spinner: + text: "Device" + font_size: '12sp' + height: '12sp' + size_hint_y: .15 + values: ['esc','ble','bms','extbms'] + on_text: app.com.setdev(self.text) + BoxLayout: + size_hint_y: .85 diff --git a/fwget.py b/fwget.py index cb0aad6..715c7dc 100644 --- a/fwget.py +++ b/fwget.py @@ -1,9 +1,17 @@ import os -from os import path import requests -from kivy.utils import platform import hashlib +try: + from kivymd.toast import toast +except: + print('no toast for you') +# toast or print +def tprint(msg): + try: + toast(msg) + except: + print(msg) class FWGet(): def __init__(self, cache): @@ -12,7 +20,17 @@ def __init__(self, cache): self.dirname = "null" if not os.path.exists(self.cachePath): os.makedirs(self.cachePath) - print("Created NineRiFt cache directory") + tprint("Created NineRiFt cache directory") + self.progress = 0 + self.maxprogress = 100 + self.model = '' + self.getboth = False + self.BLE = [] + self.DRV = [] + self.BMS = [] + + def setModel(self, model): + self.model = model def setRepo(self, repo): self.repoURL = repo @@ -36,7 +54,7 @@ def md5Checksum(self, filePath, url): def getFile(self, FWtype, version): if (self.repoURL == "http://null" or self.dirname == "null"): - print("You need to load a valid repo first.") + tprint("You need to load a valid repo first.") return(False) noInternet = False if not os.path.exists(self.cachePath + "/" + self.dirname + "/"): @@ -46,11 +64,18 @@ def getFile(self, FWtype, version): r = requests.head(self.repoURL) if (r.status_code != 200): noInternet = True - print("Failed to connect to the repo, using local files if available (Server response not 200)") + tprint("Failed to connect to the repo, using local files if available (Server response not 200)") except requests.ConnectionError: - print("Failed to connect to the repo, using local files if available (requests.ConnectionError)") + tprint("Failed to connect to the repo, using local files if available (requests.ConnectionError)") noInternet = True - filename = FWtype.upper() + version + ".bin.enc" + if self.model.startswith('m365'): + if FWtype is 'DRV' and self.getboth is False: + filename = FWtype.upper() + version + ".bin.enc" + self.getboth = True + else: + filename = FWtype.upper() + version + ".bin" + else: + filename = FWtype.upper() + version + ".bin.enc" completePath = self.cachePath + "/" + self.dirname + "/" + filename isFilePresent = os.path.isfile(completePath) if noInternet == False: @@ -73,9 +98,10 @@ def getFile(self, FWtype, version): else: url = self.repoURL + FWtype.lower() + "/" + filename try: + print('download started') r = requests.head(url) if (r.status_code == 404): - print("Failed to fetch " + filename + " (Error 404 file not found)") + tprint("Failed to fetch " + filename + " (Error 404 file not found)") return(False, completePath) print('Beginning file download; writing to ' + completePath) url = self.repoURL + FWtype.lower() + "/" + filename @@ -85,25 +111,35 @@ def getFile(self, FWtype, version): f.write(r.content) if (r.status_code == 200): print(filename + " downloaded successfully.") + print('download finished') + with open(completePath + ".md5", "r") as md5cached: + checksum = md5cached.read() + match = self.md5Checksum(completePath, None) == checksum + if not match: + if os.path.exists(completePath): + os.remove(completePath) + else: + print("The file does not exist") + tprint('File was corrupted. try again?') return(True, completePath) else: - print("Server couldn't respond to download request. Local files aren't available. Aborting.") + tprint("Server couldn't respond to download request. Local files aren't available. Aborting.") return(False, completePath) except requests.ConnectionError: - print("Connection error. Local files aren't available. Aborting.") + tprint("Connection error. Local files aren't available. Aborting.") return(False, completePath) def loadRepo(self, jsonURL): d = '' noInternet = False - hashedName = hashlib.md5(jsonURL).hexdigest() + hashedName = hashlib.md5(jsonURL.encode("utf-8")).hexdigest() try: r = requests.head(jsonURL) if (r.status_code != 200): noInternet = True - print("Failed to fetch JSON! Will use cached if available. (Server response not 200)") + tprint("Failed to fetch JSON! Will use cached if available. (Server response not 200)") except requests.ConnectionError: - print("Failed to fetch JSON! Will use cached if available. (requests.ConnectionError)") + tprint("Failed to fetch JSON! Will use cached if available. (requests.ConnectionError)") noInternet = True if noInternet == False: try: @@ -114,7 +150,7 @@ def loadRepo(self, jsonURL): d = eval(f.read()) print("Fetched repo JSON.") except requests.ConnectionError: - print("Failed to grab JSON! (requests.ConnectionError)") + tprint("Failed to grab JSON! (requests.ConnectionError)") return(False) elif os.path.isfile(self.cachePath + hashedName + ".json"): @@ -122,7 +158,7 @@ def loadRepo(self, jsonURL): d = eval(f.read()) print("Fetched cached repo JSON.") else: - print("Couldn't download file and couldn't load from cache. Aborting.") + tprint("Couldn't download file and couldn't load from cache. Aborting.") return(False) self.dirname = str(d["repo"]["infos"]["dirname"]) self.repoURL = str(d["repo"]["infos"]["files_URL"]) @@ -135,4 +171,7 @@ def loadRepo(self, jsonURL): return(True, self.dirname, self.repoURL, name, self.DRV, self.BMS, self.BLE) def Gimme(self, firm, ver): + tprint('download started') self.getFile(firm,ver) + self.getFile(firm, ver) + tprint('download finished') diff --git a/fwupd.py b/fwupd.py index 01f78be..f66b744 100644 --- a/fwupd.py +++ b/fwupd.py @@ -1,3 +1,4 @@ +import os from py9b.link.base import LinkOpenException, LinkTimeoutException from py9b.transport.base import BaseTransport as BT @@ -5,153 +6,103 @@ from py9b.transport.ninebot import NinebotTransport from py9b.command.regio import ReadRegs, WriteRegs from py9b.command.update import * + from kivy.utils import platform -import os +from kivy.clock import mainthread +from kivy.event import EventDispatcher +from kivy.properties import BooleanProperty, StringProperty, ObjectProperty +from utils import tprint, specialthread + -class FWUpd(object): - def __init__(self): - self.devices = {'ble': BT.BLE, 'esc': BT.ESC, 'bms': BT.BMS, 'extbms': BT.EXTBMS} +class FWUpd(EventDispatcher): + device = StringProperty('') + lock = BooleanProperty(True) + + def __init__(self, conn): + self.devices = {'ble': BT.BLE, 'drv': BT.ESC, 'bms': BT.BMS, 'extbms': BT.EXTBMS} self.protocols = {'xiaomi': XiaomiTransport, 'ninebot': NinebotTransport} - self.PING_RETRIES = 20 - self.device = 'esc' - self.fwfilep = '' - self.interface = 'ble' - self.protocol = 'ninebot' - self.address = '' - - def setaddr(self, a): - self.address = a - print(self.address+' selected as address') - - def setdev(self, d): - self.device = d.lower() - print(self.device+' selected as device') - - def setfwfilep(self, f): - self.fwfilep = f - print(self.fwfilep+' selected as fwfile') - - def setiface(self, i): - self.interface = i.lower() - print(self.interface+' selected as interface') - - def setproto(self, p): - self.protocol = p.lower() - print(self.protocol+' selected as protocol') - - def checksum(s, data): + self.PING_RETRIES = 3 + self.nolock = False + self.conn = conn + + def checksum(self, s, data): for c in data: - s += ord(c) + s += c return (s & 0xFFFFFFFF) - def UpdateFirmware(self, link, tran, dev, fwfile): - print('flashing '+self.fwfilep+' to ' + self.device) + def UpdateFirmware(self, tran, dev, fwfile): + tprint('update started') + + tprint('flashing '+self.fwfilep+' to ' + self.device) fwfile.seek(0, os.SEEK_END) fw_size = fwfile.tell() fwfile.seek(0) fw_page_size = 0x80 dev = self.devices.get(self.device) - print('Pinging...') + + for retry in range(self.PING_RETRIES): - print('.') + tprint('Pinging...') try: if dev == BT.BLE: - tran.execute(ReadRegs(dev, 0, '13s')) + tran.execute(ReadRegs(dev, 0, "13s")) else: - tran.execute(ReadRegs(dev, 0x10, '14s')) + tran.execute(ReadRegs(dev, 0x10, "14s")) except LinkTimeoutException: continue break else: - print('Timed out !') + tprint("Timed out!") return False - print('OK') + tprint("OK") - if self.interface != 'tcpnl': - print('Locking...') + if self.lock: + tprint('Locking...') tran.execute(WriteRegs(BT.ESC, 0x70, '=141': + sf = ['*.bin.enc'] + elif vers=='<141': + sf = ['*.bin'] + else: + sf = [''] + filters = sf+check + if mod is 'm365pro': + if dev is 'DRV': + sf = ['*.bin.enc'] + else: + sf = ['*.bin'] + filters = sf+check + if mod is 'esx' or 'max': + sf = ['*.bin.enc'] + filters = sf+check + print('selfile_filter set to %s' % filters) + return filters + + @mainthread + def fwget_update_versions(self, screen): + sel = screen.ids.part.text + if sel == 'BLE': + dev = self.fwget.BLE + elif sel == 'BMS': + dev = self.fwget.BMS + elif sel == 'DRV': + dev = self.fwget.DRV + else: + dev = [] + if dev != []: + versions = [str(e) for e in dev] + tprint('FWGet Vers. available: '+str(versions)) + screen.ids.version.values = versions + + def select_model(self, mod): + values = ['BLE', 'DRV', 'BMS'] + if mod.startswith('m365'): + self.hasextbms = False + if mod is 'm365': + self.versel = True + elif mod is 'm365pro': + self.versel = False + if mod is 'esx': + self.versel = False + self.hasextbms = True + if mod is 'max': + self.versel = False + self.hasextbms = False + if self.hasextbms is True: + try: + values.append('ExtBMS') + except: + print('ExtBMS entry already present') + if self.hasextbms is False: + try: + values.remove('ExtBMS') + except: + print('no ExtBMS entry to remove') + return values + + def on_stop(self): + self.conn.disconnect() + + @sidethread + def fwupd_func(self, chooser): + if len(chooser.selection) != 1: + tprint('Choose file to flash') + return + self.fwupd.Flash(chooser.selection[0]) + + @mainthread + def setprogbar(self, prog, maxprog): + FlashScreen.setprog(prog, maxprog) if __name__ == "__main__": NineRiFt().run() + tprint("Don't forget to post a review") diff --git a/mocklink.py b/mocklink.py new file mode 100644 index 0000000..14d3193 --- /dev/null +++ b/mocklink.py @@ -0,0 +1,71 @@ +from struct import pack +import binascii + +from py9b.link.base import BaseLink, LinkTimeoutException +from py9b.transport.base import checksum, BaseTransport as BT + +try: + import queue +except ImportError: + import Queue as queue + + +class Fifo: + def __init__(self): + self.q = queue.Queue() + + def write(self, data): # put bytes + for b in data: + self.q.put(b) + + def read(self, size=1, timeout=None): # but read string + res = bytearray() + for i in range(size): + res.append(self.q.get(True, timeout)) + return res + + +class MockLink: + def __init__(self): + self.q = Fifo() + self.timeout = 1.0 + + def __enter__(self): + return self + + def scan(self): + return [ + ('test', 'Test Device'), + ] + + def close(self): + print('Closed') + + def open(self, addr): + print('Connected to', addr) + + def write(self, data): + hdata = binascii.hexlify(data) + print('>>', hdata.decode()) + if hdata == b'55aa032001680271ff': + self.q.write(self._buildx(0x23, 0x01, 0x68, bytearray([0x01, 0x01]))) + + def read(self, size=1): + try: + data = self.q.read(size, timeout=self.timeout) + except queue.Empty: + raise LinkTimeoutException + return data + + def _buildx(self, dev, cmd, arg, data): + pkt = ( + pack( + " 0x081 and self.link is ('ble'): + transport.keys = link.fetch_keys_pro() + transport.recover_keys() + tprint('Keys recovered') + + self._tran = transport + self._link = link + + return transport + + except Exception as exc: + self.update_state('disconnected') + self.dispatch('on_error', repr(exc)) + raise exc + + @mainthread + def update_state(self, state): + print('Current state:', state) + self.state = state + + @specialthread + def disconnect(self): + if self.state == 'connected': + self.update_state('disconnecting') + self._link.close() + self.update_state('disconnected') + + def on_error(self, *args): + # Required for event handling dispatch + pass diff --git a/nbcmd.py b/nbcmd.py new file mode 100644 index 0000000..a7b1069 --- /dev/null +++ b/nbcmd.py @@ -0,0 +1,54 @@ +import time +from py9b.link.base import LinkTimeoutException +from py9b.transport.base import BaseTransport as BT +from py9b.command.regio import ReadRegs, WriteRegs + +from kivy.utils import platform +try: + from kivymd.toast import toast +except: + print('no toast for you') + +# toast or print +def tprint(msg): + try: + toast(msg) + except: + print(msg) + + +class Command: + def __init__(self, conn): + self.new_sn = '' + self.device = '' + self.conn = conn + + def setdev(self, d): + self.device = d.lower() + tprint(self.device+' selected as device') + + def setnewsn(self, p): + self.new_sn = p + tprint(self.new_sn+' input for newsn') + + def powerdown(self): + tran = self.conn._tran + tran.execute(WriteRegs(BT.ESC, 0x79, ": + size_hint_x: None + size: self.texture_size + +: + GridLayout: + cols: 1 + rows: 3 + GridLayout: + rows: 2 + size_hint_y: .20 + GridLayout: + cols: 2 + rows: 2 + Spinner: + id: fwumodel + text:'Model' + values: ['esx', 'max'] + sync_height: True + font_size: '12sp' + height: '12sp' + on_text: app.select_model(self.text); app.selfile_filter(self.text, versel.text, fwupart.text); filechooser.do_layout() + Spinner: + id: fwupart + text: 'Part' + values: app.select_model(fwumodel.text) + font_size: '12sp' + height: '12sp' + sync_height: True + on_text: app.fwupd.device = self.text.lower(); app.selfile_filter(fwumodel.text, versel.text, self.text); filechooser.do_layout() + GridLayout: + cols: 2 + FormLabel: + size_hint_x: .2 + font_size: '12sp' + text: "Lock:" + Switch: + size_hint_x: .8 + active: True + on_active: app.fwupd.lock = self.active + BoxLayout: + Spinner: + disabled: fwumodel.text != 'm365' + id: versel + text: 'Vers.' + font_size: '12sp' + height: '12sp' + sync_height: True + values: ['<141', '>=141'] + on_text: app.selfile_filter(fwumodel.text, self.text, fwupart.text); filechooser.do_layout() + BoxLayout: + orientation: 'vertical' + size_hint_y: .6 + FileChooserListView: + id: filechooser + path: app.cache_folder + filters: app.selfile_filter(fwumodel.text, versel.text, fwupart.text) + GridLayout: + rows: 2 + size_hint_y: .20 + ProgressBar: + id: fwprogbar + size_hint_y: .3 + value: 0 + max: 100 + Button: + disabled: app.conn.state in ('disconnected',) + size_hint_y: .7 + font_size: '12sp' + height: '12sp' + text: "Start" + on_press: app.fwupd_func(filechooser) + +: + GridLayout: + cols: 1 + rows: 3 + AnchorLayout: + anchor_y: 'top' + GridLayout: + cols: 3 + size_hint_y: .15 + Spinner: + id: model + text: 'Model' + values: ['esx', 'max'] + on_text: app.fwget_select_model(root, self.text) + font_size: '12sp' + height: '12sp' + sync_height: True + Spinner: + id: part + text: 'Part' + values: ['BLE', 'DRV', 'BMS'] + on_text: app.fwget_select_model(root, model.text); app.fwget_update_versions(root) + font_size: '12sp' + height: '12sp' + sync_height: True + Spinner: + id: version + text: 'Version' + values: [] + sync_height: True + font_size: '12sp' + height: '12sp' + text_autoupdate: True + AnchorLayout: + size_hint_y: 0.3 + anchor_y: 'bottom' + Button: + text: 'Download it!' + on_press: app.fwget_func(part.text, version.text) + +: + orientation: 'vertical' + size_hint_y: .80 + +: + GridLayout: + cols: 1 + rows: 3 + GridLayout: + rows: 1 + size_hint_y: .1 + BoxLayout: + orientation: 'vertical' + Spinner: + id: scriptsel + text: 'Script' + values: ['lock', 'unlock', 'reboot', 'powerdown'] + on_text: root.setcmd(self.text) + font_size: '12sp' + height: '12sp' + + ScriptUI: + id: scriptspace + + AnchorLayout: + anchor_y: 'bottom' + size_hint_y: .10 + Button: + text: "Execute" + on_press: app.executecmd(scriptsel.text) + +: + orientation: "vertical" + GridLayout: + cols: 1 + rows: 4 + GridLayout: + id: connman + size_hint_y: .15 + cols: 1 + rows: 4 + BoxLayout: + size_hint_y: .325 + orientation: 'horizontal' + FormLabel: + font_size: '12sp' + text: "Addr:" + TextInput: + multiline: False + on_text_validate: app.conn.address = self.text.lower() + font_size: '12sp' + BoxLayout: + size_hint_y: .325 + orientation: 'horizontal' + Spinner: + text: 'Interface' + values: ['BLE', 'TCP'] + on_text: app.conn.link = self.text.lower() + font_size: '12sp' + height: '12sp' + Spinner: + text: 'Protocol' + values: ['xiaomi', 'ninebot'] + on_text: app.conn.transport = self.text.lower() + font_size: '12sp' + height: '12sp' + BoxLayout: + size_hint_y: .02 + AnchorLayout: + size_hint_y: .33 + anchor_y: 'bottom' + Button: + text: "Connect" if app.conn.state == 'disconnected' else "Disconnect" + disabled: app.conn.state not in ('connected', 'disconnected') + font_size: '12sp' + on_press: app.connection_toggle() + GridLayout: + cols: 1 + size_hint_y: .01 + BoxLayout: + orientation: 'horizontal' + size_hint_y: .05 + Button: + text: "Flash" + font_size: '12sp' + height: '12sp' + on_press: manager.current = 'flash' + Button: + text: "Download" + font_size: '12sp' + height: '12sp' + on_press: manager.current = 'download' + Button: + text: "Command" + font_size: '12sp' + height: '12sp' + on_press: manager.current = 'command' + ScreenManager: + size_hint_y: .80 + id: manager + FlashScreen: + name: "flash" + DownloadScreen: + name: "download" + CommandScreen: + name: "command" diff --git a/packaging/android-arm32/.buildozer b/packaging/android-arm32/.buildozer new file mode 120000 index 0000000..c2dcf91 --- /dev/null +++ b/packaging/android-arm32/.buildozer @@ -0,0 +1 @@ +../../../NineRiFt-base/packaging/lite/.buildozer32/ \ No newline at end of file diff --git a/buildozer.spec b/packaging/android-arm32/buildozer.spec similarity index 93% rename from buildozer.spec rename to packaging/android-arm32/buildozer.spec index 2faa5af..90befb2 100644 --- a/buildozer.spec +++ b/packaging/android-arm32/buildozer.spec @@ -4,13 +4,13 @@ title = NineRiFt # (str) Package name -package.name = NineRiFt +package.name = NineRiFtLite # (str) Package domain (needed for android/ios packaging) package.domain = com.netrafire # (str) Source code where the main.py live -source.dir = . +source.dir = ../../ # (list) Source files to include (let empty to include all the files) source.include_exts = py,png,jpg,kv,atlas @@ -28,7 +28,7 @@ source.include_exts = py,png,jpg,kv,atlas #source.exclude_patterns = license,images/*/*.jpg # (str) Application versioning (method 1) -version = 0.5 +version = 1.4 # (str) Application versioning (method 2) # version.regex = __version__ = ['"](.*)['"] @@ -36,7 +36,7 @@ version = 0.5 # (list) Application requirements # comma separated e.g. requirements = sqlite3,kivy -requirements = python3,kivy,android,able,requests,openssl +requirements = python3, kivy, android, requests, openssl, kivymd, able, pyjnius, pyserial, usb4a, usbserial4a # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes # requirements.source.kivy = ../../kivy @@ -48,7 +48,7 @@ requirements = python3,kivy,android,able,requests,openssl #presplash.filename = %(source.dir)s/data/presplash.png # (str) Icon of the application -#icon.filename = %(source.dir)s/data/icon.png +icon.filename = %(source.dir)s/packaging/icon.png # (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) orientation = all @@ -64,7 +64,7 @@ orientation = all # author = © Copyright Info # change the major version of python used by the app -osx.python_version = 2 +osx.python_version = 3 # Kivy version to use osx.kivy_version = 1.9.1 @@ -74,7 +74,7 @@ osx.kivy_version = 1.9.1 # # (bool) Indicate if the application should be fullscreen or not -fullscreen = 1 +fullscreen = 0 # (string) Presplash background color (for new android toolchain) # Supported formats are: #RRGGBB #AARRGGBB or one of the following names: @@ -87,10 +87,10 @@ fullscreen = 1 android.permissions = INTERNET, READ_EXTERNAL_STORAGE, BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_COARSE_LOCATION # (int) Target Android API, should be as high as possible. -android.api = 28 +android.api = 29 # (int) Minimum API your APK will support. -#android.minapi = 18 +android.minapi = 21 # (int) Android SDK version to use #android.sdk = 28 @@ -105,10 +105,10 @@ android.api = 28 #android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) -android.ndk_path = /opt/android-studio/ndk-bundle +android.ndk_path = ../../.android/ndk # (str) Android SDK directory (if empty, it will be automatically downloaded.) -android.sdk_path = /opt/Android/Sdk +android.sdk_path = ../../.android/sdk # (str) ANT directory (if empty, it will be automatically downloaded.) #android.ant_path = @@ -128,7 +128,7 @@ android.accept_sdk_license = True #android.entrypoint = org.renpy.android.PythonActivity # (list) Pattern to whitelist for the whole project -#android.whitelist = +android.whitelist = lib-dynload/termios.so # (str) Path to a custom whitelist file #android.whitelist_src = @@ -167,9 +167,6 @@ android.accept_sdk_license = True # (str) Filename of OUYA Console icon. It must be a 732x412 png image. #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png -# (str) XML file to include as an intent filters in tag -#android.manifest.intent_filters = - # (str) launchMode to set for the main activity #android.manifest.launch_mode = standard @@ -191,7 +188,7 @@ android.wakelock = True #android.library_references = # (str) Android logcat filters to use -#android.logcat_filters = *:S python:D +android.logcat_filters = *:S python:D # (bool) Copy library instead of making a libpymodules.so #android.copy_libs = 1 @@ -207,7 +204,7 @@ android.arch = armeabi-v7a #p4a.source_dir = # (str) The directory in which python-for-android should look for your own build recipes (if any) -p4a.local_recipes = ~/able/recipes +p4a.local_recipes = ../../.android/able/recipes # (str) Filename to the hook for p4a #p4a.hook = @@ -218,22 +215,20 @@ p4a.local_recipes = ~/able/recipes # (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) #p4a.port = -android.whitelist = lib-dynload/termios.so - # # iOS specific # # (str) Path to a custom kivy-ios folder -#ios.kivy_ios_dir = ../kivy-ios +#ios.kivy_ios_dir = ../../kivy-ios # Alternately, specify the URL and branch of a git checkout: ios.kivy_ios_url = https://github.com/kivy/kivy-ios ios.kivy_ios_branch = master # Another platform dependency: ios-deploy # Uncomment to use a custom checkout -#ios.ios_deploy_dir = ../ios_deploy +#ios.ios_deploy_dir = ../../ios_deploy # Or specify URL and branch ios.ios_deploy_url = https://github.com/phonegap/ios-deploy ios.ios_deploy_branch = 1.7.0 @@ -255,10 +250,10 @@ log_level = 2 warn_on_root = 1 # (str) Path to build artifact storage, absolute or relative to spec file -# build_dir = ./.buildozer +build_dir = ./.buildozer # (str) Path to build output (i.e. .apk, .ipa) storage -# bin_dir = ./bin +bin_dir = ../bin32 # ----------------------------------------------------------------------------- # List as sections diff --git a/packaging/android-arm64/.buildozer b/packaging/android-arm64/.buildozer new file mode 120000 index 0000000..fda3715 --- /dev/null +++ b/packaging/android-arm64/.buildozer @@ -0,0 +1 @@ +../../../NineRiFt-base/packaging/lite/.buildozer64/ \ No newline at end of file diff --git a/packaging/android-arm64/buildozer.spec b/packaging/android-arm64/buildozer.spec new file mode 100644 index 0000000..915752d --- /dev/null +++ b/packaging/android-arm64/buildozer.spec @@ -0,0 +1,294 @@ +[app] + +# (str) Title of your application +title = NineRiFt + +# (str) Package name +package.name = NineRiFtLite + +# (str) Package domain (needed for android/ios packaging) +package.domain = com.netrafire + +# (str) Source code where the main.py live +source.dir = ../../ + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas + +# (list) List of inclusions using pattern matching +#source.include_patterns = assets/*,images/*.png + +# (list) Source files to exclude (let empty to not exclude anything) +#source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +#source.exclude_dirs = tests, bin + +# (list) List of exclusions using pattern matching +#source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +version = 1.4 + +# (str) Application versioning (method 2) +# version.regex = __version__ = ['"](.*)['"] +# version.filename = %(source.dir)s/main.py + +# (list) Application requirements +# comma separated e.g. requirements = sqlite3,kivy +requirements = python3, kivy, android, requests, openssl, kivymd, able, pyjnius, pyserial, usb4a, usbserial4a +# (str) Custom source folders for requirements +# Sets custom source for any requirements with recipes +# requirements.source.kivy = ../../kivy + +# (list) Garden requirements +#garden_requirements = + +# (str) Presplash of the application +#presplash.filename = %(source.dir)s/data/presplash.png + +# (str) Icon of the application +icon.filename = %(source.dir)s/packaging/icon.png + +# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) +orientation = all + +# (list) List of service to declare +#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY + +# +# OSX Specific +# + +# +# author = © Copyright Info + +# change the major version of python used by the app +osx.python_version = 3 + +# Kivy version to use +osx.kivy_version = 1.9.1 + +# +# Android specific +# + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = 0 + +# (string) Presplash background color (for new android toolchain) +# Supported formats are: #RRGGBB #AARRGGBB or one of the following names: +# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, +# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, +# olive, purple, silver, teal. +#android.presplash_color = #FFFFFF + +# (list) Permissions +android.permissions = INTERNET, READ_EXTERNAL_STORAGE, BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_COARSE_LOCATION + +# (int) Target Android API, should be as high as possible. +android.api = 29 + +# (int) Minimum API your APK will support. +android.minapi = 21 + +# (int) Android SDK version to use +#android.sdk = 28 + +# (str) Android NDK version to use +#android.ndk = 19c + +# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. +#android.ndk_api = 21 + +# (bool) Use --private data storage (True) or --dir public storage (False) +#android.private_storage = True + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +android.ndk_path = ../../.android/ndk + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +android.sdk_path = ../../.android/sdk + +# (str) ANT directory (if empty, it will be automatically downloaded.) +#android.ant_path = + +# (bool) If True, then skip trying to update the Android sdk +# This can be useful to avoid excess Internet downloads or save time +# when an update is due and you just want to test/build your package +# android.skip_update = False + +# (bool) If True, then automatically accept SDK license +# agreements. This is intended for automation only. If set to False, +# the default, you will be shown the license when first running +# buildozer. +android.accept_sdk_license = True + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.renpy.android.PythonActivity + +# (list) Pattern to whitelist for the whole project +android.whitelist = lib-dynload/termios.so + +# (str) Path to a custom whitelist file +#android.whitelist_src = + +# (str) Path to a custom blacklist file +#android.blacklist_src = + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (list) Android AAR archives to add (currently works only with sdl2_gradle +# bootstrap) +#android.add_aars = + +# (list) Gradle dependencies to add (currently works only with sdl2_gradle +# bootstrap) +#android.gradle_dependencies = + +# (list) Java classes to add as activities to the manifest. +#android.add_activites = com.example.ExampleActivity + +# (str) python-for-android branch to use, defaults to master +#p4a.branch = master + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) launchMode to set for the main activity +#android.manifest.launch_mode = standard + +# (list) Android additional libraries to copy into libs/armeabi +#android.add_libs_armeabi = libs/android/*.so +#android.add_libs_armeabi_v7a = libs/android-v7/*.so +#android.add_libs_x86 = libs/android-x86/*.so +#android.add_libs_mips = libs/android-mips/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +android.wakelock = True + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# (str) Android logcat filters to use +android.logcat_filters = *:S python:D + +# (bool) Copy library instead of making a libpymodules.so +#android.copy_libs = 1 + +# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86 +android.arch = arm64-v8a + +# +# Python for android (p4a) specific +# + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) +#p4a.source_dir = + +# (str) The directory in which python-for-android should look for your own build recipes (if any) +p4a.local_recipes = ../../.android/able/recipes + +# (str) Filename to the hook for p4a +#p4a.hook = + +# (str) Bootstrap to use for android builds +# p4a.bootstrap = sdl2 + +# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) +#p4a.port = + + +# +# iOS specific +# + +# (str) Path to a custom kivy-ios folder +#ios.kivy_ios_dir = ../../kivy-ios +# Alternately, specify the URL and branch of a git checkout: +ios.kivy_ios_url = https://github.com/kivy/kivy-ios +ios.kivy_ios_branch = master + +# Another platform dependency: ios-deploy +# Uncomment to use a custom checkout +#ios.ios_deploy_dir = ../../ios_deploy +# Or specify URL and branch +ios.ios_deploy_url = https://github.com/phonegap/ios-deploy +ios.ios_deploy_branch = 1.7.0 + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: ()" + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 2 + +# (int) Display warning if buildozer is run as root (0 = False, 1 = True) +warn_on_root = 1 + +# (str) Path to build artifact storage, absolute or relative to spec file +build_dir = ./.buildozer + +# (str) Path to build output (i.e. .apk, .ipa) storage +bin_dir = ../bin64 + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +#[app] +#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +#[app:source.exclude_patterns] +#license +#data/audio/*.wav +#data/images/original/* +# + + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +#[app@demo] +#title = My Application (demo) +# +#[app:source.exclude_patterns@demo] +#images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +#buildozer --profile demo android debug diff --git a/packaging/icon.png b/packaging/icon.png new file mode 100644 index 0000000..7cebf3b Binary files /dev/null and b/packaging/icon.png differ diff --git a/packaging/requirements.txt b/packaging/requirements.txt new file mode 100644 index 0000000..0641673 --- /dev/null +++ b/packaging/requirements.txt @@ -0,0 +1,9 @@ +kivymd +bleak +kivy +kivymd +docutils +requests +twisted +image +pyserial diff --git a/py9b b/py9b new file mode 120000 index 0000000..272ebd0 --- /dev/null +++ b/py9b @@ -0,0 +1 @@ +./vendor/py9b/py9b \ No newline at end of file diff --git a/py9b/__init__.py b/py9b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/py9b/command/__init__.py b/py9b/command/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/py9b/command/base.py b/py9b/command/base.py deleted file mode 100644 index 3f4b71d..0000000 --- a/py9b/command/base.py +++ /dev/null @@ -1,19 +0,0 @@ -from ..transport.packet import BasePacket as PKT -from ..transport.base import BaseTransport as BT - - -class InvalidResponse(Exception): - pass - - -class BaseCommand(object): - def __init__(self, src=BT.HOST, dst=0, cmd=0, arg=0, data="", has_response=True): - self.has_response = has_response - self.request = PKT(src, dst, cmd, arg, data) - - - def handle_response(self, response): - return True - - -__all__ = ["BaseCommand", "InvalidResponse"] diff --git a/py9b/command/custom.py b/py9b/command/custom.py deleted file mode 100644 index 322acb3..0000000 --- a/py9b/command/custom.py +++ /dev/null @@ -1,17 +0,0 @@ -from struct import pack, unpack, calcsize -from .base import BaseCommand, InvalidResponse - - -class ReadMem(BaseCommand): - def __init__(self, dev, addr, format): - super(ReadMem, self).__init__(dst=dev, cmd=0x80, arg=calcsize(format), data=pack("', hexlify(data).upper()) - size = len(data) - ofs = 0 - while size: - chunk_sz = min(size, _write_chunk_size) - self._dev.char_write_handle(self._wr_handle, bytearray(data[ofs:ofs+chunk_sz])) - ofs += chunk_sz - size -= chunk_sz - - -__all__ = ['BLELink'] diff --git a/py9b/link/blefleet.py b/py9b/link/blefleet.py deleted file mode 100644 index 4d14fd4..0000000 --- a/py9b/link/blefleet.py +++ /dev/null @@ -1,109 +0,0 @@ -"""BLE link using BlueGiga adapter via PyGatt/BGAPI""" - - -import pygatt -from .base import BaseLink, LinkTimeoutException, LinkOpenException -from binascii import hexlify - -SCAN_TIMEOUT = 3 - - -try: - import queue -except ImportError: - import queue as queue - -class Fifo(): - def __init__(self): - self.q = queue.Queue() - - def write(self, data): # put bytes - for b in data: - self.q.put(b) - - def read(self, size=1, timeout=None): # but read string - res = '' - for i in range(size): - res += chr(self.q.get(True, timeout)) - return res - - -#_cccd_uuid = '00002902-0000-1000-8000-00805f9b34fb' -_rx_char_uuid = eval(input('RX UUID?')) -_tx_char_uuid = eval(input('TX UUID?')) -_write_chunk_size = 20 # as in android dumps - -class BLELink(BaseLink): - def __init__(self, *args, **kwargs): - super(BLELink, self).__init__(*args, **kwargs) - self._adapter = None - self._dev = None - self._wr_handle = None - self._rx_fifo = Fifo() - - - def __enter__(self): - self._adapter = pygatt.BGAPIBackend() - self._adapter.start() - return self - - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - - def _make_rx_cb(self): # this is a closure :) - def rx_cb(handle, value): - self._rx_fifo.write(value) - return rx_cb - - - def scan(self): - res = [] - devices = self._adapter.scan(timeout=SCAN_TIMEOUT) - for dev in devices: - if dev['name'].startswith(('MISc', 'NBSc')): - res.append((dev['name'], dev['address'])) - return res - - - def open(self, port): - try: - self._dev = self._adapter.connect(port, address_type=pygatt.BLEAddressType.random) - self._dev.subscribe(_tx_char_uuid, callback=self._make_rx_cb()) - self._wr_handle = self._dev.get_handle(_rx_char_uuid) - except pygatt.exceptions.NotConnectedError: - raise LinkOpenException - - - def close(self): - if self._dev: - self._dev.disconnect() - self._dev = None - if self._adapter: - self._adapter.stop() - - - def read(self, size): - try: - data = self._rx_fifo.read(size, timeout=self.timeout) - except queue.Empty: - raise LinkTimeoutException - if self.dump: - print('<', hexlify(data).upper()) - return data - - - def write(self, data): - if self.dump: - print('>', hexlify(data).upper()) - size = len(data) - ofs = 0 - while size: - chunk_sz = min(size, _write_chunk_size) - self._dev.char_write_handle(self._wr_handle, bytearray(data[ofs:ofs+chunk_sz])) - ofs += chunk_sz - size -= chunk_sz - - -__all__ = ['BLELink'] diff --git a/py9b/link/droidble.py b/py9b/link/droidble.py deleted file mode 100644 index 130a3d3..0000000 --- a/py9b/link/droidble.py +++ /dev/null @@ -1,244 +0,0 @@ -"""BLE link using ABLE""" - -from __future__ import absolute_import - -try: - from able import GATT_SUCCESS, Advertisement, BluetoothDispatcher -except ImportError: - exit('error importing able') -try: - from .base import BaseLink, LinkTimeoutException, LinkOpenException -except ImportError: - exit('error importing .base') -from binascii import hexlify -from kivy.logger import Logger -from kivy.properties import StringProperty - -try: - import queue -except ImportError: - import Queue as queue - -SCAN_TIMEOUT = 3 - -_write_chunk_size = 20 - -identity = bytearray([ -0x4e, 0x42, 0x21, 0x00, 0x00, 0x00, 0x00, 0xDE, # Ninebot Bluetooth ID 4E422100000000DE -0x4e, 0x42, 0x21, 0x00, 0x00, 0x00, 0x00, 0xDF # Xiaomi Bluetooth ID 4E422100000000DF -]) - -service_ids = { -'retail': '6e400001-b5a3-f393-e0a9-e50e24dcca9e' #service UUID -} - -receive_ids = { -'retail': '6e400002-b5a3-f393-e0a9-e50e24dcca9e' #receive characteristic UUID -} - -transmit_ids = { -'retail': '6e400003-b5a3-f393-e0a9-e50e24dcca9e' #transmit characteristic UUID -} - -scoot_found = False - - -class Fifo(): - def __init__(self): - self.q = queue.Queue() - - def write(self, data): # put bytes - for b in data: - self.q.put(b) - - def read(self, size=1, timeout=None): # but read string - res = '' - for i in range(size): - res += chr(self.q.get(True, timeout)) - return res - - -class ScootBT(BluetoothDispatcher): - def __init__(self): - super(ScootBT, self).__init__() - self.rx_fifo = Fifo() - self.ble_device = None - self.state = StringProperty() - self.dump = True - self.tx_characteristic = None - self.rx_characteristic = None - self.timeout = SCAN_TIMEOUT - self.set_queue_timeout(self.timeout) - - - def __enter__(self): - return self - - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - - def discover(self): - self.start_scan() - self.state = 'scan' - print(self.state) - - - def on_device(self, device, rssi, advertisement): - global scoot_found - if self.state != 'scan': - return - Logger.debug("on_device event {}".format(list(advertisement))) - self.addr = device.getAddress() - if self.addr and address.startswith(self.addr): - print(self.addr) - self.ble_device = device - self.scoot_found = True - self.stop_scan() - else: - for ad in advertisement: - print(ad) - if ad.ad_type == Advertisement.ad_types.manufacturer_specific_data: - if ad.data.startswith(self.identity): - scoot_found = True - else: - break - elif ad.ad_type == Advertisement.ad_types.complete_local_name: - name = str(ad.data) - if scoot_found: - self.state = 'found' - print(self.state) - self.ble_device = device - Logger.debug("Scooter detected: {}".format(name)) - self.stop_scan() - - - def on_scan_completed(self): - if self.ble_device: - self.connect_gatt(self.ble_device) - self.state = 'connected' - print(self.state) - else: - self.start_scan() - - - def on_connection_state_change(self, status, state): - if status == GATT_SUCCESS and state: - self.discover_services() - self.state = 'discover' - print(self.state) - else: - self.close_gatt() - self.rx_characteristic = None - self.tx_characteristic = None - self.services = None - - - def on_services(self, status, services): - self.services = services - for uuid in list(receive_ids.values()): - self.rx_characteristic = self.services.search(uuid) - print('RX: '+uuid) - for uuid in list(transmit_ids.values()): - self.tx_characteristic = self.services.search(uuid) - print('TX: '+uuid) - self.enable_notifications(self.tx_characteristic) - - - def on_characteristic_changed(self, characteristic): - if characteristic == self.tx_characteristic: - data = characteristic.getValue() - self.rx_fifo.write(data) - - - def open(self, port): - self.addr = port - if self.ble_device == None: - self.discover() - if self.state!='connected': - self.connect_gatt(self.ble_device) - else: - return - - - def close(self): - if self.ble_device != None: - self.close_gatt() - self.services = None - print('close') - - - def read(self, size): - print('read') - if self.ble_device: - try: - data = self.rx_fifo.read(size, timeout=self.timeout) - except queue.Empty: - raise LinkTimeoutException - if self.dump: - print('<', hexlify(data).upper()) - return data - else: - print('BLE not connected') - self.discover() - - - def write(self, data): - print('write') - if self.ble_device: - if self.dump: - print('>', hexlify(data).upper()) - size = len(data) - ofs = 0 - while size: - chunk_sz = min(size, _write_chunk_size) - self.write_characteristic(self.rx_characteristic, bytearray(data[ofs:ofs+chunk_sz])) - ofs += chunk_sz - size -= chunk_sz - else: - print('BLE not connected') - self.discover() - - - def scan(self): - self.discover() - - -class BLELink(BaseLink): - def __init__(self, *args, **kwargs): - super(BLELink, self).__init__(*args, **kwargs) - self._adapter = None - - - def __enter__(self): - self._adapter = ScootBT() - self._adapter.discover() - return self - - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - - def scan(self): - devices = self._adapter.scan() - - - def open(self, port): - self._adapter.open(port) - - - def close(self): - self._adapter.close() - - - def read(self, size): - self._adapter.read(size) - - - def write(self, data): - self._adapter.write(data) - - -__all__ = ['BLELink'] diff --git a/py9b/link/serial.py b/py9b/link/serial.py deleted file mode 100644 index 84447a7..0000000 --- a/py9b/link/serial.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Direct serial link""" - - -import serial -import serial.tools.list_ports as lp -from binascii import hexlify -from .base import BaseLink, LinkTimeoutException, LinkOpenException - - -class SerialLink(BaseLink): - def __init__(self, *args, **kwargs): - super(SerialLink, self).__init__(*args, **kwargs) - self.com = None - - - def __enter__(self): - return self - - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - - def scan(self): - ports = lp.comports() - res = [("%s %04X:%04X" % (port.device, port.vid, port.pid), port.device) for port in ports] - return res - - - def open(self, port): - try: - self.com = serial.Serial(port, 115200, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=self.timeout) - except serial.SerialException: - raise LinkOpenException - - - def close(self): - if self.com: - self.com.close() - self.com = None - - - def read(self, size): - try: - data = self.com.read(size) - except serial.SerialTimeoutException: - raise LinkTimeoutException - if len(data)", hexlify(data).upper()) - self.com.write(data) - - -__all__ = ["SerialLink"] diff --git a/py9b/link/tcp.py b/py9b/link/tcp.py deleted file mode 100644 index 8e0957d..0000000 --- a/py9b/link/tcp.py +++ /dev/null @@ -1,80 +0,0 @@ -"""TCP-BLE bridge link""" - -import socket -from binascii import hexlify -from .base import BaseLink, LinkTimeoutException, LinkOpenException - -HOST, PORT = "127.0.0.1", 6000 - -_write_chunk_size = 20 # 20 as in android dumps - -def recvall(sock, size): - data = "" - while len(data)", hexlify(data).upper()) - size = len(data) - ofs = 0 - while size: - chunk_sz = min(size, _write_chunk_size) - self.sock.sendall(data[ofs:ofs+chunk_sz]) - ofs += chunk_sz - size -= chunk_sz - - -__all__ = ["TCPLink"] diff --git a/py9b/transport/__init__.py b/py9b/transport/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/py9b/transport/base.py b/py9b/transport/base.py deleted file mode 100644 index 291e43d..0000000 --- a/py9b/transport/base.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Transport abstract class""" - -def checksum(data): - s = 0 - for c in data: - s += ord(c) - return (s & 0xFFFF) ^ 0xFFFF - - - -class BaseTransport(object): - MOTOR = 0x01 - ESC = 0x20 - BLE = 0x21 - BMS = 0x22 - EXTBMS = 0x23 - HOST = 0x3E - - DeviceNames = { MOTOR : "MOTOR", ESC : "ESC", BLE : "BLE", BMS : "BMS", EXTBMS : "EXTBMS", HOST : "HOST" } - - def __init__(self, link): - self.link = link - - def recv(self): - raise NotImplementedError() - - def send(self, src, dst, cmd, arg, data=""): - raise NotImplementedError() - - def execute(self, command): - self.send(command.request) - if not command.has_response: - return True - #TODO: retry ? - rsp = self.recv() - return command.handle_response(rsp) - - @staticmethod - def GetDeviceName(dev): - return BaseTransport.DeviceNames.get(dev, "%02X" % (dev)) - - -__all__ = ["checksum", "BaseTransport"] diff --git a/py9b/transport/ninebot.py b/py9b/transport/ninebot.py deleted file mode 100644 index bcefc0e..0000000 --- a/py9b/transport/ninebot.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Ninebot packet transport""" -from struct import pack, unpack -from .base import checksum, BaseTransport as BT -from .packet import BasePacket - - -class NinebotTransport(BT): - def __init__(self, link, device=BT.HOST): - super(NinebotTransport, self).__init__(link) - self.device = device - - - def _wait_pre(self): - while True: - while True: - c = self.link.read(1) - if c=="\x5A": - break - while True: - c = self.link.read(1) - if c=="\xA5": - return True - if c!="\x5A": - break # start waiting 5A again, else - this is 5A, so wait for A5 - - - def recv(self): - self._wait_pre() - pkt = self.link.read(1) - l = ord(pkt)+6 - for i in range(l): - pkt += self.link.read(1) - ck_calc = checksum(pkt[0:-2]) - ck_pkt = unpack("%s: %02X @%02X %s" % (BT.GetDeviceName(self.src), BT.GetDeviceName(self.dst), self.cmd, self.arg, hexlify(self.data).upper()) - - -__all__ = ["BasePacket"] diff --git a/py9b/transport/xiaomi.py b/py9b/transport/xiaomi.py deleted file mode 100644 index 28eb518..0000000 --- a/py9b/transport/xiaomi.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Xiaomi packet transport""" -from struct import pack, unpack -from .base import checksum, BaseTransport as BT -from .packet import BasePacket - - -class XiaomiTransport(BT): - MASTER2ESC = 0x20 - ESC2MASTER = 0x23 - - MASTER2BLE = 0x21 - BLE2MASTER = 0x24 - - MASTER2BMS = 0x22 - BMS2MASTER = 0x25 - - MOTOR = 0x01 - DEVFF = 0xFF - - _SaDa2Addr = { BT.HOST : { BT.MOTOR : MOTOR, BT.ESC : MASTER2ESC, BT.BLE : MASTER2BLE, BT.BMS : MASTER2BMS }, - BT.ESC : { BT.HOST : ESC2MASTER, BT.BLE : MASTER2BLE, BT.BMS : MASTER2BMS, BT.MOTOR : MOTOR }, - BT.BMS : { BT.HOST : BMS2MASTER, BT.ESC : BMS2MASTER, BT.MOTOR : MOTOR }, - BT.MOTOR : {BT.HOST : MOTOR, BT.ESC : MOTOR, BT.BMS : MOTOR } } - - # TBC - _BleAddr2SaDa = { MASTER2ESC : (BT.HOST, BT.ESC), - ESC2MASTER : (BT.ESC, BT.HOST), - MASTER2BMS : (BT.HOST, BT.BMS), - BMS2MASTER : (BT.BMS, BT.HOST), - MASTER2BLE : (BT.HOST, BT.BLE), - BLE2MASTER : (BT.BLE, BT.HOST), - MOTOR : (BT.MOTOR, BT.HOST) } - - _BmsAddr2SaDa = { MASTER2ESC : (BT.BMS, BT.ESC), - ESC2MASTER : (BT.ESC, BT.BMS), - MASTER2BMS : (BT.ESC, BT.BMS), - BMS2MASTER : (BT.BMS, BT.ESC), - MASTER2BLE : (BT.BMS, BT.BLE), - BLE2MASTER : (BT.BLE, BT.BMS), - MOTOR : (BT.MOTOR, BT.BMS) } - - - def __init__(self, link, device=BT.HOST): - super(XiaomiTransport, self).__init__(link) - self.device = device - - - def _make_addr(self, src, dst): - return XiaomiTransport._SaDa2Addr[src][dst] - - - def _split_addr(self, addr): - if self.device==BT.BMS: - return XiaomiTransport._BmsAddr2SaDa[addr] - else: - return XiaomiTransport._BleAddr2SaDa[addr] - - - def _wait_pre(self): - while True: - while True: - c = self.link.read(1) - if c=="\x55": - break - while True: - c = self.link.read(1) - if c=="\xAA": - return True - if c!="\x55": - break # start waiting 55 again, else - this is 55, so wait for AA - - - def recv(self): - self._wait_pre() - pkt = self.link.read(1) - l = ord(pkt)+3 - for i in range(l): - pkt += self.link.read(1) - ck_calc = checksum(pkt[0:-2]) - ck_pkt = unpack("