mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2024-12-23 13:21:44 +03:00
4b7ca736d6
* WiFi board update: windows fix * Scripts: add python\Scripts to the path * Windows: another way to call esptool * Aaaaaand once more! * logz * wifi board scripts: udev rule, dev channel * fbt: added ARGS variable for passing extra options to certain scripts; removed `devboard_flash_dev` in favor of `devboard_flash ARGS="-c dev"` * fbt: fully removed `devboard_flash_dev * scripts: wifi_board.py: cleanup * ufbt: ported ARGS for supported targets to ufbt * docs: updated for ARGS=... --------- Co-authored-by: hedger <hedger@nanode.su> Co-authored-by: hedger <hedger@users.noreply.github.com>
257 lines
8.2 KiB
Python
Executable File
257 lines
8.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
|
|
import requests
|
|
import serial.tools.list_ports as list_ports
|
|
from flipper.app import App
|
|
from serial.tools.list_ports_common import ListPortInfo
|
|
|
|
|
|
class UpdateDownloader:
|
|
UPDATE_SERVER = "https://update.flipperzero.one"
|
|
UPDATE_PROJECT = "/blackmagic-firmware"
|
|
UPDATE_INDEX = UPDATE_SERVER + UPDATE_PROJECT + "/directory.json"
|
|
UPDATE_TYPE = "full_tgz"
|
|
|
|
CHANNEL_ID_ALIAS = {
|
|
"dev": "development",
|
|
"rc": "release-candidate",
|
|
"r": "release",
|
|
"rel": "release",
|
|
}
|
|
|
|
def __init__(self):
|
|
self.logger = logging.getLogger()
|
|
|
|
def download(self, channel_id: str, target_dir: str) -> bool:
|
|
# Aliases
|
|
if channel_id in self.CHANNEL_ID_ALIAS:
|
|
channel_id = self.CHANNEL_ID_ALIAS[channel_id]
|
|
|
|
# Make directory
|
|
if not os.path.exists(target_dir):
|
|
self.logger.info(f"Creating directory {target_dir}")
|
|
os.makedirs(target_dir)
|
|
|
|
# Download json index
|
|
self.logger.info(f"Downloading {self.UPDATE_INDEX}")
|
|
response = requests.get(self.UPDATE_INDEX)
|
|
if response.status_code != 200:
|
|
self.logger.error(f"Failed to download {self.UPDATE_INDEX}")
|
|
return False
|
|
|
|
# Parse json index
|
|
try:
|
|
index = json.loads(response.content)
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to parse json index: {e}")
|
|
return False
|
|
|
|
# Find channel
|
|
channel = None
|
|
for channel_candidate in index["channels"]:
|
|
if channel_candidate["id"] == channel_id:
|
|
channel = channel_candidate
|
|
break
|
|
|
|
# Check if channel found
|
|
if channel is None:
|
|
self.logger.error(
|
|
f"Channel '{channel_id}' not found. Valid channels: {', '.join([c['id'] for c in index['channels']])}"
|
|
)
|
|
return False
|
|
|
|
self.logger.info(f"Using channel '{channel_id}'")
|
|
|
|
# Get latest version
|
|
try:
|
|
version = channel["versions"][0]
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to get version: {e}")
|
|
return False
|
|
|
|
self.logger.info(f"Using version '{version['version']}'")
|
|
|
|
# Get changelog
|
|
if changelog := version.get("changelog"):
|
|
self.logger.info(f"Changelog:")
|
|
for line in changelog.split("\n"):
|
|
if line.strip() == "":
|
|
continue
|
|
self.logger.info(f" {line}")
|
|
else:
|
|
self.logger.warning(f"Changelog not found")
|
|
|
|
# Find file
|
|
file_url = None
|
|
for file_candidate in version["files"]:
|
|
if file_candidate["type"] == self.UPDATE_TYPE:
|
|
file_url = file_candidate["url"]
|
|
break
|
|
|
|
if file_url is None:
|
|
self.logger.error(f"File not found")
|
|
return False
|
|
|
|
# Make file path
|
|
file_name = file_url.split("/")[-1]
|
|
file_path = os.path.join(target_dir, file_name)
|
|
|
|
# Download file
|
|
self.logger.info(f"Downloading {file_url} to {file_path}")
|
|
with open(file_path, "wb") as f:
|
|
response = requests.get(file_url)
|
|
f.write(response.content)
|
|
|
|
# Unzip tgz
|
|
self.logger.info(f"Unzipping {file_path}")
|
|
with tarfile.open(file_path, "r") as tar:
|
|
tar.extractall(target_dir)
|
|
|
|
return True
|
|
|
|
|
|
class Main(App):
|
|
def init(self):
|
|
self.parser.add_argument("-p", "--port", help="CDC Port", default="auto")
|
|
self.parser.add_argument(
|
|
"-c", "--channel", help="Channel name", default="release"
|
|
)
|
|
self.parser.set_defaults(func=self.update)
|
|
|
|
# logging
|
|
self.logger = logging.getLogger()
|
|
|
|
@staticmethod
|
|
def _grep_ports(regexp: str) -> list[ListPortInfo]:
|
|
# idk why, but python thinks that list_ports.grep returns tuple[str, str, str]
|
|
return list(list_ports.grep(regexp)) # type: ignore
|
|
|
|
def is_wifi_board_connected(self) -> bool:
|
|
return (
|
|
len(self._grep_ports("ESP32-S2")) > 0
|
|
or len(self._grep_ports("CMSIS-DAP")) > 0
|
|
)
|
|
|
|
@staticmethod
|
|
def is_windows() -> bool:
|
|
return os.name == "nt"
|
|
|
|
@classmethod
|
|
def find_port(cls, regexp: str) -> str:
|
|
ports: list[ListPortInfo] = cls._grep_ports(regexp)
|
|
|
|
if len(ports) == 0:
|
|
# Blackmagic probe serial port not found, will be handled later
|
|
pass
|
|
elif len(ports) > 1:
|
|
raise Exception("More than one WiFi board found")
|
|
else:
|
|
port = ports[0]
|
|
return f"\\\\.\\{port.device}" if cls.is_windows() else port.device
|
|
|
|
def find_wifi_board_bootloader_port(self):
|
|
return self.find_port("ESP32-S2")
|
|
|
|
def find_wifi_board_bootloader_port_damn_windows(self):
|
|
self.logger.info("Trying to find WiFi board using VID:PID")
|
|
return self.find_port("VID:PID=303A:0002")
|
|
|
|
def update(self):
|
|
try:
|
|
port = self.find_wifi_board_bootloader_port()
|
|
|
|
# Damn windows fix
|
|
if port is None and self.is_windows():
|
|
port = self.find_wifi_board_bootloader_port_damn_windows()
|
|
except Exception as e:
|
|
self.logger.error(f"{e}")
|
|
return 1
|
|
|
|
if port is None:
|
|
if self.is_wifi_board_connected():
|
|
self.logger.error("WiFi board found, but not in bootloader mode.")
|
|
self.logger.info("Please hold down BOOT button and press RESET button")
|
|
else:
|
|
self.logger.error("WiFi board not found")
|
|
self.logger.info(
|
|
"Please connect WiFi board to your computer, hold down BOOT button and press RESET button"
|
|
)
|
|
if not self.is_windows():
|
|
self.logger.info(
|
|
"If you are using Linux, you may need to add udev rules to access the device"
|
|
)
|
|
self.logger.info(
|
|
"Check out 41-flipper.rules & README in scripts/debug folder"
|
|
)
|
|
return 1
|
|
|
|
# get temporary dir
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
downloader = UpdateDownloader()
|
|
|
|
# download latest channel update
|
|
try:
|
|
if not downloader.download(self.args.channel, temp_dir):
|
|
self.logger.error(f"Cannot download update")
|
|
return 1
|
|
except Exception as e:
|
|
self.logger.error(f"Cannot download update: {e}")
|
|
return 1
|
|
|
|
with open(os.path.join(temp_dir, "flash.command"), "r") as f:
|
|
flash_command = f.read()
|
|
|
|
replacements = (
|
|
("\n", ""),
|
|
("\r", ""),
|
|
("(PORT)", port),
|
|
# We can't reset the board after flashing via usb
|
|
("--after hard_reset", "--after no_reset_stub"),
|
|
)
|
|
|
|
# hellish toolchain fix
|
|
if self.is_windows():
|
|
replacements += (("esptool.py", "python -m esptool"),)
|
|
else:
|
|
replacements += (("esptool.py", "python3 -m esptool"),)
|
|
|
|
for old, new in replacements:
|
|
flash_command = flash_command.replace(old, new)
|
|
|
|
args = list(filter(None, flash_command.split()))
|
|
|
|
self.logger.info(f'Running command: "{" ".join(args)}" in "{temp_dir}"')
|
|
|
|
process = subprocess.Popen(
|
|
args,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
cwd=temp_dir,
|
|
bufsize=1,
|
|
universal_newlines=True,
|
|
)
|
|
|
|
while process.poll() is None:
|
|
if process.stdout is not None:
|
|
for line in process.stdout:
|
|
self.logger.debug(f"{line.strip()}")
|
|
|
|
if process.returncode != 0:
|
|
self.logger.error(f"Failed to flash WiFi board")
|
|
else:
|
|
self.logger.info("WiFi board flashed successfully")
|
|
self.logger.info("Press RESET button on WiFi board to start it")
|
|
|
|
return process.returncode
|
|
|
|
|
|
if __name__ == "__main__":
|
|
Main()()
|