diff --git a/Jenkinsfile b/Jenkinsfile index f6d07bc85..c0dd8710b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -176,7 +176,7 @@ pipeline { ["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"], ["test encoder", "python selfdrive/loggerd/tests/test_encoder.py"], ["test logcatd", "python selfdrive/logcatd/tests/test_logcatd_android.py"], - //["test updater", "python installer/updater/test_updater.py"], + ["test updater", "python selfdrive/hardware/eon/test_neos_updater.py"], ]) } } diff --git a/README.md b/README.md index 2507713c9..25ee06959 100755 --- a/README.md +++ b/README.md @@ -354,7 +354,6 @@ Directory Structure . ├── cereal # The messaging spec and libs used for all logs ├── common # Library like functionality we've developed here - ├── installer/updater # Manages updates of NEOS ├── opendbc # Files showing how to interpret data from cars ├── panda # Code used to communicate on CAN ├── phonelibs # External libraries diff --git a/installer/updater/.gitignore b/installer/updater/.gitignore deleted file mode 100644 index 339d49be6..000000000 --- a/installer/updater/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -local/ -prod/ -staging/ diff --git a/installer/updater/Makefile b/installer/updater/Makefile deleted file mode 100644 index 54bd7c9df..000000000 --- a/installer/updater/Makefile +++ /dev/null @@ -1,99 +0,0 @@ -CC = clang -CXX = clang++ - -PHONELIBS = ../../phonelibs - -WARN_FLAGS = -Werror=implicit-function-declaration \ - -Werror=incompatible-pointer-types \ - -Werror=int-conversion \ - -Werror=return-type \ - -Werror=format-extra-args - -CFLAGS = -std=gnu11 -g -fPIC -O2 $(WARN_FLAGS) -CXXFLAGS = -std=c++1z -g -fPIC -O2 $(WARN_FLAGS) - -CURL_FLAGS = -I$(PHONELIBS)/curl/include -CURL_LIBS = $(PHONELIBS)/curl/lib/libcurl.a \ - $(PHONELIBS)/zlib/lib/libz.a - -BORINGSSL_FLAGS = -I$(PHONELIBS)/boringssl/include -BORINGSSL_LIBS = $(PHONELIBS)/boringssl/lib/libssl_static.a \ - $(PHONELIBS)/boringssl/lib/libcrypto_static.a \ - -NANOVG_FLAGS = -I$(PHONELIBS)/nanovg - -JSON11_FLAGS = -I$(PHONELIBS)/json11 - -OPENGL_LIBS = -lGLESv3 - -FRAMEBUFFER_LIBS = -lutils -lgui -lEGL - -.PHONY: all -all: updater - -OBJS = opensans_regular.ttf.o \ - opensans_semibold.ttf.o \ - opensans_bold.ttf.o \ - ../../selfdrive/common/util.o \ - ../../selfdrive/common/touch.o \ - ../../selfdrive/common/framebuffer.o \ - $(PHONELIBS)/json11/json11.o \ - $(PHONELIBS)/nanovg/nanovg.o - -DEPS := $(OBJS:.o=.d) - -updater: updater.o $(OBJS) - @echo "[ LINK ] $@" - $(CXX) $(CPPFLAGS) -fPIC -o 'updater' $^ \ - $(FRAMEBUFFER_LIBS) \ - $(CURL_LIBS) \ - $(BORINGSSL_LIBS) \ - -L/system/vendor/lib64 \ - $(OPENGL_LIBS) \ - -lcutils -lm -llog - strip updater - -opensans_regular.ttf.o: ../../selfdrive/assets/fonts/opensans_regular.ttf - @echo "[ bin2o ] $@" - cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)' - -opensans_bold.ttf.o: ../../selfdrive/assets/fonts/opensans_bold.ttf - @echo "[ bin2o ] $@" - cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)' - -opensans_semibold.ttf.o: ../../selfdrive/assets/fonts/opensans_semibold.ttf - @echo "[ bin2o ] $@" - cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)' - -%.o: %.c - mkdir -p $(@D) - @echo "[ CC ] $@" - $(CC) $(CPPFLAGS) $(CFLAGS) \ - -I../.. \ - -I$(PHONELIBS)/android_frameworks_native/include \ - -I$(PHONELIBS)/android_system_core/include \ - -I$(PHONELIBS)/android_hardware_libhardware/include \ - $(NANOVG_FLAGS) \ - -c -o '$@' '$<' - -%.o: %.cc - mkdir -p $(@D) - @echo "[ CXX ] $@" - $(CXX) $(CPPFLAGS) $(CXXFLAGS) \ - -I../../selfdrive \ - -I../../ \ - -I$(PHONELIBS)/android_frameworks_native/include \ - -I$(PHONELIBS)/android_system_core/include \ - -I$(PHONELIBS)/android_hardware_libhardware/include \ - $(NANOVG_FLAGS) \ - $(JSON11_FLAGS) \ - $(CURL_FLAGS) \ - $(BORINGSSL_FLAGS) \ - -c -o '$@' '$<' - - -.PHONY: clean -clean: - rm -f $(OBJS) $(DEPS) - --include $(DEPS) diff --git a/installer/updater/test_updater.py b/installer/updater/test_updater.py deleted file mode 100755 index 6e811921d..000000000 --- a/installer/updater/test_updater.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -import os -import shutil -import subprocess -import tempfile -import time -import unittest - -from common.basedir import BASEDIR - -UPDATER_PATH = os.path.join(BASEDIR, "installer/updater") -UPDATER = os.path.join(UPDATER_PATH, "updater") -UPDATE_MANIFEST = os.path.join(UPDATER_PATH, "update.json") - - -class TestUpdater(unittest.TestCase): - - @classmethod - def setUpClass(cls): - # test that the updater builds - cls.assertTrue(f"cd {UPDATER_PATH} && make clean && make", "updater failed to build") - - # restore the checked-in version, since that's what actually runs on devices - os.system(f"git reset --hard {UPDATER_PATH}") - - def setUp(self): - self._clear_dir() - - def tearDown(self): - self._clear_dir() - - def _clear_dir(self): - if os.path.isdir("/data/neoupdate"): - shutil.rmtree("/data/neoupdate") - - def _assert_ok(self, cmd, msg=None): - self.assertTrue(os.system(cmd) == 0, msg) - - def _assert_fails(self, cmd): - self.assertFalse(os.system(cmd) == 0) - - def test_background_download(self): - self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") - - def test_background_download_bad_manifest(self): - # update with bad manifest should fail - with tempfile.NamedTemporaryFile(mode="w", suffix=".json") as f: - f.write("{}") - self._assert_fails(f"{UPDATER} bgcache 'file://{f.name}'") - - def test_cache_resume(self): - self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") - # a full download takes >1m, but resuming from fully cached should only be a few seconds - start_time = time.monotonic() - self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") - self.assertLess(time.monotonic() - start_time, 10) - - # make sure we can recover from corrupt downloads - def test_recover_from_corrupt(self): - # download the whole update - self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") - - # write some random bytes - for f in os.listdir("/data/neoupdate"): - with open(os.path.join("/data/neoupdate", f), "ab") as f: - f.write(b"\xab"*20) - - # this attempt should fail, then it unlinks - self._assert_fails(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") - - # now it should pass - self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") - - # simple test that the updater doesn't crash in UI mode - def test_ui_init(self): - with subprocess.Popen(UPDATER) as proc: - time.sleep(5) - self.assertTrue(proc.poll() is None) - proc.terminate() - -if __name__ == "__main__": - unittest.main() diff --git a/installer/updater/updater b/installer/updater/updater index d833cb782..8bf40708a 100755 Binary files a/installer/updater/updater and b/installer/updater/updater differ diff --git a/installer/updater/updater.cc b/installer/updater/updater.cc deleted file mode 100644 index d1a694ef9..000000000 --- a/installer/updater/updater.cc +++ /dev/null @@ -1,803 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include "nanovg.h" -#define NANOVG_GLES3_IMPLEMENTATION -#include "json11.hpp" -#include "nanovg_gl.h" -#include "nanovg_gl_utils.h" - -#include "selfdrive/common/framebuffer.h" -#include "selfdrive/common/touch.h" -#include "selfdrive/common/util.h" - -#define USER_AGENT "NEOSUpdater-0.2" - -#define MANIFEST_URL_NEOS_STAGING "https://github.com/commaai/eon-neos/raw/master/update.staging.json" -#define MANIFEST_URL_NEOS_LOCAL "http://192.168.5.1:8000/neosupdate/update.local.json" -#define MANIFEST_URL_NEOS "https://github.com/commaai/eon-neos/raw/master/update.json" -const char *manifest_url = MANIFEST_URL_NEOS; - -#define RECOVERY_DEV "/dev/block/bootdevice/by-name/recovery" -#define RECOVERY_COMMAND "/cache/recovery/command" - -#define UPDATE_DIR "/data/neoupdate" - -extern const uint8_t bin_opensans_regular[] asm("_binary_opensans_regular_ttf_start"); -extern const uint8_t bin_opensans_regular_end[] asm("_binary_opensans_regular_ttf_end"); -extern const uint8_t bin_opensans_semibold[] asm("_binary_opensans_semibold_ttf_start"); -extern const uint8_t bin_opensans_semibold_end[] asm("_binary_opensans_semibold_ttf_end"); -extern const uint8_t bin_opensans_bold[] asm("_binary_opensans_bold_ttf_start"); -extern const uint8_t bin_opensans_bold_end[] asm("_binary_opensans_bold_ttf_end"); - -namespace { - -std::string sha256_file(std::string fn, size_t limit=0) { - SHA256_CTX ctx; - SHA256_Init(&ctx); - - FILE *file = fopen(fn.c_str(), "rb"); - if (!file) return ""; - - const size_t buf_size = 8192; - std::unique_ptr buffer( new char[ buf_size ] ); - - bool read_limit = (limit != 0); - while (true) { - size_t read_size = buf_size; - if (read_limit) read_size = std::min(read_size, limit); - size_t bytes_read = fread(buffer.get(), 1, read_size, file); - if (!bytes_read) break; - - SHA256_Update(&ctx, buffer.get(), bytes_read); - - if (read_limit) { - limit -= bytes_read; - if (limit == 0) break; - } - } - - uint8_t hash[SHA256_DIGEST_LENGTH]; - SHA256_Final(hash, &ctx); - - fclose(file); - - return util::tohex(hash, sizeof(hash)); -} - -size_t download_string_write(void *ptr, size_t size, size_t nmeb, void *up) { - size_t sz = size * nmeb; - ((std::string*)up)->append((char*)ptr, sz); - return sz; -} - -std::string download_string(CURL *curl, std::string url) { - std::string os; - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); - curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 0); - curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT); - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1); - curl_easy_setopt(curl, CURLOPT_RESUME_FROM, 0); - - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1); - - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, download_string_write); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &os); - CURLcode res = curl_easy_perform(curl); - if (res != CURLE_OK) { - return ""; - } - - return os; -} - -size_t download_file_write(void *ptr, size_t size, size_t nmeb, void *up) { - return fwrite(ptr, size, nmeb, (FILE*)up); -} - -int battery_capacity() { - std::string bat_cap_s = util::read_file("/sys/class/power_supply/battery/capacity"); - return atoi(bat_cap_s.c_str()); -} - -int battery_current() { - std::string current_now_s = util::read_file("/sys/class/power_supply/battery/current_now"); - return atoi(current_now_s.c_str()); -} - -bool check_battery() { - int bat_cap = battery_capacity(); - int current_now = battery_current(); - return bat_cap > 35 || (current_now < 0 && bat_cap > 10); -} - -bool check_space() { - struct statvfs stat; - if (statvfs("/data/", &stat) != 0) { - return false; - } - size_t space = stat.f_bsize * stat.f_bavail; - return space > 2000000000ULL; // 2GB -} - -static void start_settings_activity(const char* name) { - char launch_cmd[1024]; - snprintf(launch_cmd, sizeof(launch_cmd), - "am start -W --ez :settings:show_fragment_as_subsetting true -n 'com.android.settings/.%s'", name); - system(launch_cmd); -} - -bool is_settings_active() { - FILE *fp; - char sys_output[4096]; - - fp = popen("/bin/dumpsys window windows", "r"); - if (fp == NULL) { - return false; - } - - bool active = false; - while (fgets(sys_output, sizeof(sys_output), fp) != NULL) { - if (strstr(sys_output, "mCurrentFocus=null") != NULL) { - break; - } - - if (strstr(sys_output, "mCurrentFocus=Window") != NULL) { - active = true; - break; - } - } - - pclose(fp); - - return active; -} - -struct Updater { - bool do_exit = false; - - TouchState touch; - - int fb_w, fb_h; - - std::unique_ptr fb; - NVGcontext *vg = NULL; - int font_regular; - int font_semibold; - int font_bold; - - std::thread update_thread_handle; - - std::mutex lock; - - enum UpdateState { - CONFIRMATION, - LOW_BATTERY, - RUNNING, - ERROR, - }; - UpdateState state; - - std::string progress_text; - float progress_frac; - - std::string error_text; - - std::string low_battery_text; - std::string low_battery_title; - std::string low_battery_context; - std::string battery_cap_text; - int min_battery_cap = 35; - - // button - int b_x, b_w, b_y, b_h; - int balt_x; - - // download stage writes these for the installation stage - int recovery_len; - std::string recovery_hash; - std::string recovery_fn; - std::string ota_fn; - - CURL *curl = NULL; - - void ui_init() { - touch_init(&touch); - - fb = std::make_unique("updater", 0x00001000, false, &fb_w, &fb_h); - - fb->set_power(HWC_POWER_MODE_NORMAL); - - vg = nvgCreateGLES3(NVG_ANTIALIAS | NVG_STENCIL_STROKES | NVG_DEBUG); - assert(vg); - - font_regular = nvgCreateFontMem(vg, "opensans_regular", (unsigned char*)bin_opensans_regular, (bin_opensans_regular_end - bin_opensans_regular), 0); - assert(font_regular >= 0); - - font_semibold = nvgCreateFontMem(vg, "opensans_semibold", (unsigned char*)bin_opensans_semibold, (bin_opensans_semibold_end - bin_opensans_semibold), 0); - assert(font_semibold >= 0); - - font_bold = nvgCreateFontMem(vg, "opensans_bold", (unsigned char*)bin_opensans_bold, (bin_opensans_bold_end - bin_opensans_bold), 0); - assert(font_bold >= 0); - - b_w = 640; - balt_x = 200; - b_x = fb_w-b_w-200; - b_y = 720; - b_h = 220; - - if (download_stage(true)) { - state = RUNNING; - update_thread_handle = std::thread(&Updater::run_stages, this); - } else { - state = CONFIRMATION; - } - } - - int download_file_xferinfo(curl_off_t dltotal, curl_off_t dlno, - curl_off_t ultotal, curl_off_t ulnow) { - { - std::lock_guard guard(lock); - if (dltotal != 0) { - progress_frac = (float) dlno / dltotal; - } - } - // printf("info: %ld %ld %f\n", dltotal, dlno, progress_frac); - return 0; - } - - bool download_file(std::string url, std::string out_fn) { - FILE *of = fopen(out_fn.c_str(), "ab"); - assert(of); - - CURLcode res; - long last_resume_from = 0; - - fseek(of, 0, SEEK_END); - - int tries = 4; - - bool ret = false; - - while (true) { - long resume_from = ftell(of); - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); - curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 0); - curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT); - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1); - curl_easy_setopt(curl, CURLOPT_RESUME_FROM, resume_from); - - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, download_file_write); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, of); - - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); - - curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this); - curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, &Updater::download_file_xferinfo); - - CURLcode res = curl_easy_perform(curl); - - long response_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); - - // double content_length = 0.0; - // curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &content_length); - - printf("download %s res %d, code %ld, resume from %ld\n", url.c_str(), res, response_code, resume_from); - if (res == CURLE_OK) { - ret = true; - break; - } else if (res == CURLE_HTTP_RETURNED_ERROR && response_code == 416) { - // failed because the file is already complete? - ret = true; - break; - } else if (resume_from == last_resume_from) { - // failed and dind't make make forward progress. only retry a couple times - tries--; - if (tries <= 0) { - break; - } - } - last_resume_from = resume_from; - } - // printf("res %d\n", res); - - // printf("- %ld %f\n", response_code, content_length); - - fclose(of); - - return ret; - } - - void set_progress(std::string text) { - std::lock_guard guard(lock); - progress_text = text; - } - - void set_error(std::string text) { - std::lock_guard guard(lock); - error_text = text; - state = ERROR; - } - - void set_battery_low() { - std::lock_guard guard(lock); - state = LOW_BATTERY; - } - - void set_running() { - std::lock_guard guard(lock); - state = RUNNING; - } - - std::string download(std::string url, std::string hash, std::string name, bool dry_run) { - std::string out_fn = UPDATE_DIR "/" + util::base_name(url); - - std::string fn_hash = sha256_file(out_fn); - if (dry_run) { - return (hash.compare(fn_hash) != 0) ? "" : out_fn; - } - - // start or resume downloading if hash doesn't match - if (hash.compare(fn_hash) != 0) { - set_progress("Downloading " + name + "..."); - bool r = download_file(url, out_fn); - if (!r) { - set_error("failed to download " + name); - unlink(out_fn.c_str()); - return ""; - } - fn_hash = sha256_file(out_fn); - } - - set_progress("Verifying " + name + "..."); - printf("got %s hash: %s\n", name.c_str(), hash.c_str()); - if (fn_hash != hash) { - set_error(name + " was corrupt"); - unlink(out_fn.c_str()); - return ""; - } - return out_fn; - } - - bool download_stage(bool dry_run = false) { - curl = curl_easy_init(); - assert(curl); - - // ** quick checks before download ** - - if (!check_space()) { - if (!dry_run) set_error("2GB of free space required to update"); - return false; - } - - mkdir(UPDATE_DIR, 0777); - - set_progress("Finding latest version..."); - std::string manifest_s = download_string(curl, manifest_url); - printf("manifest: %s\n", manifest_s.c_str()); - - std::string err; - auto manifest = json11::Json::parse(manifest_s, err); - if (manifest.is_null() || !err.empty()) { - set_error("failed to load update manifest"); - return false; - } - - std::string ota_url = manifest["ota_url"].string_value(); - std::string ota_hash = manifest["ota_hash"].string_value(); - - std::string recovery_url = manifest["recovery_url"].string_value(); - recovery_hash = manifest["recovery_hash"].string_value(); - recovery_len = manifest["recovery_len"].int_value(); - - // std::string installer_url = manifest["installer_url"].string_value(); - // std::string installer_hash = manifest["installer_hash"].string_value(); - - if (ota_url.empty() || ota_hash.empty()) { - set_error("invalid update manifest"); - return false; - } - - // std::string installer_fn = download(installer_url, installer_hash, "installer"); - // if (installer_fn.empty()) { - // //error'd - // return; - // } - - // ** handle recovery download ** - if (recovery_url.empty() || recovery_hash.empty() || recovery_len == 0) { - set_progress("Skipping recovery flash..."); - } else { - // only download the recovery if it differs from what's flashed - set_progress("Checking recovery..."); - std::string existing_recovery_hash = sha256_file(RECOVERY_DEV, recovery_len); - printf("existing recovery hash: %s\n", existing_recovery_hash.c_str()); - - if (existing_recovery_hash != recovery_hash) { - recovery_fn = download(recovery_url, recovery_hash, "recovery", dry_run); - if (recovery_fn.empty()) { - // error'd - return false; - } - } - } - - // ** handle ota download ** - ota_fn = download(ota_url, ota_hash, "update", dry_run); - if (ota_fn.empty()) { - //error'd - return false; - } - - // download sucessful - return true; - } - - // thread that handles downloading and installing the update - void run_stages() { - printf("run_stages start\n"); - - - // ** download update ** - - if (!check_battery()) { - set_battery_low(); - int battery_cap = battery_capacity(); - while(battery_cap < min_battery_cap) { - battery_cap = battery_capacity(); - battery_cap_text = std::to_string(battery_cap); - util::sleep_for(1000); - } - set_running(); - } - - bool sucess = download_stage(); - if (!sucess) { - return; - } - - // ** install update ** - - if (!check_battery()) { - set_battery_low(); - int battery_cap = battery_capacity(); - while(battery_cap < min_battery_cap) { - battery_cap = battery_capacity(); - battery_cap_text = std::to_string(battery_cap); - util::sleep_for(1000); - } - set_running(); - } - - if (!recovery_fn.empty()) { - // flash recovery - set_progress("Flashing recovery..."); - - FILE *flash_file = fopen(recovery_fn.c_str(), "rb"); - if (!flash_file) { - set_error("failed to flash recovery"); - return; - } - - FILE *recovery_dev = fopen(RECOVERY_DEV, "w+b"); - if (!recovery_dev) { - fclose(flash_file); - set_error("failed to flash recovery"); - return; - } - - const size_t buf_size = 4096; - std::unique_ptr buffer( new char[ buf_size ] ); - - while (true) { - size_t bytes_read = fread(buffer.get(), 1, buf_size, flash_file); - if (!bytes_read) break; - - size_t bytes_written = fwrite(buffer.get(), 1, bytes_read, recovery_dev); - if (bytes_read != bytes_written) { - fclose(recovery_dev); - fclose(flash_file); - set_error("failed to flash recovery: write failed"); - return; - } - } - - fclose(recovery_dev); - fclose(flash_file); - - set_progress("Verifying flash..."); - std::string new_recovery_hash = sha256_file(RECOVERY_DEV, recovery_len); - printf("new recovery hash: %s\n", new_recovery_hash.c_str()); - - if (new_recovery_hash != recovery_hash) { - set_error("recovery flash corrupted"); - return; - } - - } - - // write arguments to recovery - FILE *cmd_file = fopen(RECOVERY_COMMAND, "wb"); - if (!cmd_file) { - set_error("failed to reboot into recovery"); - return; - } - fprintf(cmd_file, "--update_package=%s\n", ota_fn.c_str()); - fclose(cmd_file); - - set_progress("Rebooting"); - - // remove the continue.sh so we come back into the setup. - // maybe we should go directly into the installer, but what if we don't come back with internet? :/ - //unlink("/data/data/com.termux/files/continue.sh"); - - // TODO: this should be generic between android versions - // IPowerManager.reboot(confirm=false, reason="recovery", wait=true) - system("service call power 16 i32 0 s16 recovery i32 1"); - while (true) pause(); - - // execl("/system/bin/reboot", "recovery"); - // set_error("failed to reboot into recovery"); - } - - void draw_ack_screen(const char *title, const char *message, const char *button, const char *altbutton) { - nvgFillColor(vg, nvgRGBA(255,255,255,255)); - nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_BASELINE); - - nvgFontFace(vg, "opensans_bold"); - nvgFontSize(vg, 120.0f); - nvgTextBox(vg, 110, 220, fb_w-240, title, NULL); - - nvgFontFace(vg, "opensans_regular"); - nvgFontSize(vg, 86.0f); - nvgTextBox(vg, 130, 380, fb_w-260, message, NULL); - - // draw button - if (button) { - nvgBeginPath(vg); - nvgFillColor(vg, nvgRGBA(8, 8, 8, 255)); - nvgRoundedRect(vg, b_x, b_y, b_w, b_h, 20); - nvgFill(vg); - - nvgFillColor(vg, nvgRGBA(255, 255, 255, 255)); - nvgFontFace(vg, "opensans_semibold"); - nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgText(vg, b_x+b_w/2, b_y+b_h/2, button, NULL); - - nvgBeginPath(vg); - nvgStrokeColor(vg, nvgRGBA(255, 255, 255, 50)); - nvgStrokeWidth(vg, 5); - nvgRoundedRect(vg, b_x, b_y, b_w, b_h, 20); - nvgStroke(vg); - } - - // draw button - if (altbutton) { - nvgBeginPath(vg); - nvgFillColor(vg, nvgRGBA(8, 8, 8, 255)); - nvgRoundedRect(vg, balt_x, b_y, b_w, b_h, 20); - nvgFill(vg); - - nvgFillColor(vg, nvgRGBA(255, 255, 255, 255)); - nvgFontFace(vg, "opensans_semibold"); - nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - nvgText(vg, balt_x+b_w/2, b_y+b_h/2, altbutton, NULL); - - nvgBeginPath(vg); - nvgStrokeColor(vg, nvgRGBA(255, 255, 255, 50)); - nvgStrokeWidth(vg, 5); - nvgRoundedRect(vg, balt_x, b_y, b_w, b_h, 20); - nvgStroke(vg); - } - } - - void draw_battery_screen() { - low_battery_title = "Low Battery"; - low_battery_text = "Please connect EON to your charger. Update will continue once EON battery reaches 35%."; - low_battery_context = "Current battery charge: " + battery_cap_text + "%"; - - nvgFillColor(vg, nvgRGBA(255,255,255,255)); - nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_BASELINE); - - nvgFontFace(vg, "opensans_bold"); - nvgFontSize(vg, 120.0f); - nvgTextBox(vg, 110, 220, fb_w-240, low_battery_title.c_str(), NULL); - - nvgFontFace(vg, "opensans_regular"); - nvgFontSize(vg, 86.0f); - nvgTextBox(vg, 130, 380, fb_w-260, low_battery_text.c_str(), NULL); - - nvgFontFace(vg, "opensans_bold"); - nvgFontSize(vg, 86.0f); - nvgTextBox(vg, 130, 700, fb_w-260, low_battery_context.c_str(), NULL); - } - - void draw_progress_screen() { - // draw progress message - nvgFontSize(vg, 64.0f); - nvgFillColor(vg, nvgRGBA(255,255,255,255)); - nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_BASELINE); - nvgFontFace(vg, "opensans_bold"); - nvgFontSize(vg, 86.0f); - nvgTextBox(vg, 0, 380, fb_w, progress_text.c_str(), NULL); - - // draw progress bar - { - int progress_width = 1000; - int progress_x = fb_w/2-progress_width/2; - int progress_y = 520; - int progress_height = 50; - - int powerprompt_y = 312; - nvgFontFace(vg, "opensans_regular"); - nvgFontSize(vg, 64.0f); - nvgText(vg, fb_w/2, 740, "Ensure your device remains connected to a power source.", NULL); - - NVGpaint paint = nvgBoxGradient( - vg, progress_x + 1, progress_y + 1, - progress_width - 2, progress_height, 3, 4, nvgRGB(27, 27, 27), nvgRGB(27, 27, 27)); - nvgBeginPath(vg); - nvgRoundedRect(vg, progress_x, progress_y, progress_width, progress_height, 12); - nvgFillPaint(vg, paint); - nvgFill(vg); - - float value = std::min(std::max(0.0f, progress_frac), 1.0f); - int bar_pos = ((progress_width - 2) * value); - - paint = nvgBoxGradient( - vg, progress_x, progress_y, - bar_pos+1.5f, progress_height-1, 3, 4, - nvgRGB(245, 245, 245), nvgRGB(105, 105, 105)); - - nvgBeginPath(vg); - nvgRoundedRect( - vg, progress_x+1, progress_y+1, - bar_pos, progress_height-2, 12); - nvgFillPaint(vg, paint); - nvgFill(vg); - } - } - - void ui_draw() { - std::lock_guard guard(lock); - - nvgBeginFrame(vg, fb_w, fb_h, 1.0f); - - switch (state) { - case CONFIRMATION: - draw_ack_screen("An update to NEOS is required.", - "Your device will now be reset and upgraded. You may want to connect to wifi as download is around 1 GB. Existing data on device should not be lost.", - "Continue", - "Connect to WiFi"); - break; - case LOW_BATTERY: - draw_battery_screen(); - break; - case RUNNING: - draw_progress_screen(); - break; - case ERROR: - draw_ack_screen("There was an error", (error_text).c_str(), NULL, "Reboot"); - break; - } - - nvgEndFrame(vg); - } - - void ui_update() { - std::lock_guard guard(lock); - - if (state == ERROR || state == CONFIRMATION) { - int touch_x = -1, touch_y = -1; - int res = touch_poll(&touch, &touch_x, &touch_y, 0); - if (res == 1 && !is_settings_active()) { - if (touch_x >= b_x && touch_x < b_x+b_w && touch_y >= b_y && touch_y < b_y+b_h) { - if (state == CONFIRMATION) { - state = RUNNING; - update_thread_handle = std::thread(&Updater::run_stages, this); - } - } - if (touch_x >= balt_x && touch_x < balt_x+b_w && touch_y >= b_y && touch_y < b_y+b_h) { - if (state == CONFIRMATION) { - start_settings_activity("Settings$WifiSettingsActivity"); - } else if (state == ERROR) { - do_exit = 1; - } - } - } - } - } - - void go() { - ui_init(); - - while (!do_exit) { - ui_update(); - - glClearColor(0.08, 0.08, 0.08, 1.0); - glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); - - // background - nvgBeginPath(vg); - NVGpaint bg = nvgLinearGradient(vg, fb_w, 0, fb_w, fb_h, - nvgRGBA(0, 0, 0, 0), nvgRGBA(0, 0, 0, 255)); - nvgFillPaint(vg, bg); - nvgRect(vg, 0, 0, fb_w, fb_h); - nvgFill(vg); - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - ui_draw(); - - glDisable(GL_BLEND); - - fb->swap(); - - assert(glGetError() == GL_NO_ERROR); - - // no simple way to do 30fps vsync with surfaceflinger... - util::sleep_for(30); - } - - if (update_thread_handle.joinable()) { - update_thread_handle.join(); - } - - // reboot - system("service call power 16 i32 0 i32 0 i32 1"); - } - -}; - -} - -int main(int argc, char *argv[]) { - bool background_cache = false; - if (argc > 1) { - if (strcmp(argv[1], "local") == 0) { - manifest_url = MANIFEST_URL_NEOS_LOCAL; - } else if (strcmp(argv[1], "staging") == 0) { - manifest_url = MANIFEST_URL_NEOS_STAGING; - } else if (strcmp(argv[1], "bgcache") == 0) { - manifest_url = argv[2]; - background_cache = true; - } else { - manifest_url = argv[1]; - } - } - - printf("updating from %s\n", manifest_url); - Updater updater; - - int err = 0; - if (background_cache) { - err = !updater.download_stage(); - } else { - updater.go(); - } - return err; -} diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 6ac935e9f..7ef671c26 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -90,7 +90,11 @@ function two_init { # Check for NEOS update if [ $(< /VERSION) != "$REQUIRED_NEOS_VERSION" ]; then - "$DIR/installer/updater/updater" "file://$DIR/installer/updater/update.json" + echo "Installing NEOS update" + NEOS_PY="$DIR/selfdrive/hardware/eon/neos.py" + MANIFEST="$DIR/selfdrive/hardware/eon/neos.json" + $NEOS_PY --swap-if-ready $MANIFEST + $DIR/selfdrive/hardware/eon/updater $NEOS_PY $MANIFEST fi } diff --git a/release/files_common b/release/files_common index 626548ee4..988b39ba0 100644 --- a/release/files_common +++ b/release/files_common @@ -188,9 +188,6 @@ selfdrive/debug/*.py selfdrive/common/SConscript selfdrive/common/version.h -selfdrive/common/framebuffer.h -selfdrive/common/framebuffer.cc -selfdrive/common/touch.[c,h] selfdrive/common/swaglog.h selfdrive/common/swaglog.cc selfdrive/common/util.cc @@ -271,9 +268,12 @@ selfdrive/hardware/base.h selfdrive/hardware/base.py selfdrive/hardware/hw.h selfdrive/hardware/eon/__init__.py +selfdrive/hardware/eon/androidd.py selfdrive/hardware/eon/hardware.h selfdrive/hardware/eon/hardware.py -selfdrive/hardware/eon/androidd.py +selfdrive/hardware/eon/neos.py +selfdrive/hardware/eon/neos.json +selfdrive/hardware/eon/updater selfdrive/hardware/tici/__init__.py selfdrive/hardware/tici/hardware.py selfdrive/hardware/tici/amplifier.py @@ -483,11 +483,6 @@ phonelibs/android_frameworks_native/** phonelibs/android_hardware_libhardware/** phonelibs/android_system_core/** -installer/updater/updater -installer/updater/updater.cc -installer/updater/update.json -installer/updater/Makefile - scripts/update_now.sh scripts/stop_updater.sh diff --git a/selfdrive/common/SConscript b/selfdrive/common/SConscript index f0bfe5404..9b496fef7 100644 --- a/selfdrive/common/SConscript +++ b/selfdrive/common/SConscript @@ -22,10 +22,6 @@ files = [ ] if arch == "aarch64": - files += [ - 'framebuffer.cc', - 'touch.c', - ] _gpu_libs = ['gui', 'adreno_utils'] elif arch == "larch64": _gpu_libs = ["GLESv2"] diff --git a/selfdrive/common/framebuffer.cc b/selfdrive/common/framebuffer.cc deleted file mode 100644 index 79051f7e8..000000000 --- a/selfdrive/common/framebuffer.cc +++ /dev/null @@ -1,146 +0,0 @@ -#include "selfdrive/common/framebuffer.h" - -#include -#include - -#include "selfdrive/common/util.h" - -#include - -#include -#include -#include -#include -#include - -#define BACKLIGHT_LEVEL 205 - -using namespace android; - -struct FramebufferState { - sp session; - sp dtoken; - DisplayInfo dinfo; - sp control; - - sp s; - EGLDisplay display; - - EGLint egl_major, egl_minor; - EGLConfig config; - EGLSurface surface; - EGLContext context; -}; - -void FrameBuffer::swap() { - eglSwapBuffers(s->display, s->surface); - assert(glGetError() == GL_NO_ERROR); -} - -bool set_brightness(int brightness) { - char bright[64]; - snprintf(bright, sizeof(bright), "%d", brightness); - return 0 == util::write_file("/sys/class/leds/lcd-backlight/brightness", bright, strlen(bright)); -} - -void FrameBuffer::set_power(int mode) { - SurfaceComposerClient::setDisplayPowerMode(s->dtoken, mode); -} - -FrameBuffer::FrameBuffer(const char *name, uint32_t layer, int alpha, int *out_w, int *out_h) { - s = new FramebufferState; - - s->session = new SurfaceComposerClient(); - assert(s->session != NULL); - - s->dtoken = SurfaceComposerClient::getBuiltInDisplay( - ISurfaceComposer::eDisplayIdMain); - assert(s->dtoken != NULL); - - status_t status = SurfaceComposerClient::getDisplayInfo(s->dtoken, &s->dinfo); - assert(status == 0); - - //int orientation = 3; // rotate framebuffer 270 degrees - int orientation = 1; // rotate framebuffer 90 degrees - if(orientation == 1 || orientation == 3) { - int temp = s->dinfo.h; - s->dinfo.h = s->dinfo.w; - s->dinfo.w = temp; - } - - printf("dinfo %dx%d\n", s->dinfo.w, s->dinfo.h); - - Rect destRect(s->dinfo.w, s->dinfo.h); - s->session->setDisplayProjection(s->dtoken, orientation, destRect, destRect); - - s->control = s->session->createSurface(String8(name), - s->dinfo.w, s->dinfo.h, PIXEL_FORMAT_RGBX_8888); - assert(s->control != NULL); - - SurfaceComposerClient::openGlobalTransaction(); - status = s->control->setLayer(layer); - SurfaceComposerClient::closeGlobalTransaction(); - assert(status == 0); - - s->s = s->control->getSurface(); - assert(s->s != NULL); - - // init opengl and egl - const EGLint attribs[] = { - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, alpha ? 8 : 0, - EGL_DEPTH_SIZE, 0, - EGL_STENCIL_SIZE, 8, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, - // enable MSAA - EGL_SAMPLE_BUFFERS, 1, - EGL_SAMPLES, 4, - EGL_NONE, - }; - - s->display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - assert(s->display != EGL_NO_DISPLAY); - - int success = eglInitialize(s->display, &s->egl_major, &s->egl_minor); - assert(success); - - printf("egl version %d.%d\n", s->egl_major, s->egl_minor); - - EGLint num_configs; - success = eglChooseConfig(s->display, attribs, &s->config, 1, &num_configs); - assert(success); - - s->surface = eglCreateWindowSurface(s->display, s->config, s->s.get(), NULL); - assert(s->surface != EGL_NO_SURFACE); - - const EGLint context_attribs[] = { - EGL_CONTEXT_CLIENT_VERSION, 3, - EGL_NONE, - }; - s->context = eglCreateContext(s->display, s->config, NULL, context_attribs); - assert(s->context != EGL_NO_CONTEXT); - - EGLint w, h; - eglQuerySurface(s->display, s->surface, EGL_WIDTH, &w); - eglQuerySurface(s->display, s->surface, EGL_HEIGHT, &h); - printf("egl w %d h %d\n", w, h); - - success = eglMakeCurrent(s->display, s->surface, s->surface, s->context); - assert(success); - - printf("gl version %s\n", glGetString(GL_VERSION)); - - set_brightness(BACKLIGHT_LEVEL); - - if (out_w) *out_w = w; - if (out_h) *out_h = h; -} - -FrameBuffer::~FrameBuffer() { - eglDestroyContext(s->display, s->context); - eglDestroySurface(s->display, s->surface); - eglTerminate(s->display); - delete s; -} diff --git a/selfdrive/common/framebuffer.h b/selfdrive/common/framebuffer.h deleted file mode 100644 index d7054374c..000000000 --- a/selfdrive/common/framebuffer.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - -#include "hardware/hwcomposer_defs.h" - -bool set_brightness(int brightness); - -struct FramebufferState; -class FrameBuffer { - public: - FrameBuffer(const char *name, uint32_t layer, int alpha, int *out_w, int *out_h); - ~FrameBuffer(); - void set_power(int mode); - void swap(); -private: - FramebufferState *s; -}; diff --git a/selfdrive/common/touch.c b/selfdrive/common/touch.c deleted file mode 100644 index eef83d8f2..000000000 --- a/selfdrive/common/touch.c +++ /dev/null @@ -1,96 +0,0 @@ -#include "selfdrive/common/touch.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* this macro is used to tell if "bit" is set in "array" - * it selects a byte from the array, and does a boolean AND - * operation with a byte that only has the relevant bit set. - * eg. to check for the 12th bit, we do (array[1] & 1<<4) - */ -#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8))) - -static int find_dev() { - int err; - - int ret = -1; - - DIR *dir = opendir("/dev/input"); - assert(dir); - struct dirent* de = NULL; - while ((de = readdir(dir))) { - if (strncmp(de->d_name, "event", 5)) continue; - - int fd = openat(dirfd(dir), de->d_name, O_RDONLY); - assert(fd >= 0); - - unsigned char ev_bits[KEY_MAX / 8 + 1]; - err = ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(ev_bits)), ev_bits); - assert(err >= 0); - - if (test_bit(ABS_MT_POSITION_X, ev_bits) && test_bit(ABS_MT_POSITION_Y, ev_bits)) { - ret = fd; - break; - } - close(fd); - } - closedir(dir); - - return ret; -} - -void touch_init(TouchState *s) { - s->fd = find_dev(); - assert(s->fd >= 0); -} - -int touch_poll(TouchState *s, int* out_x, int* out_y, int timeout) { - assert(out_x && out_y); - bool up = false; - while (true) { - struct pollfd polls[] = {{ - .fd = s->fd, - .events = POLLIN, - }}; - int err = poll(polls, 1, timeout); - if (err < 0) { - return -1; - } - if (!(polls[0].revents & POLLIN)) { - break; - } - - struct input_event event; - err = read(polls[0].fd, &event, sizeof(event)); - if (err < sizeof(event)) { - return -1; - } - - switch (event.type) { - case EV_ABS: - if (event.code == ABS_MT_POSITION_X) { - s->last_x = event.value; - } else if (event.code == ABS_MT_POSITION_Y) { - s->last_y = event.value; - } else if (event.code == ABS_MT_TRACKING_ID && event.value != -1) { - up = true; - } - break; - default: - break; - } - } - if (up) { - // adjust for flippening - *out_x = s->last_y; - *out_y = 1080 - s->last_x; - } - return up; -} diff --git a/selfdrive/common/touch.h b/selfdrive/common/touch.h deleted file mode 100644 index fa0faa271..000000000 --- a/selfdrive/common/touch.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct TouchState { - int fd; - int last_x, last_y; -} TouchState; - -void touch_init(TouchState *s); -int touch_poll(TouchState *s, int *out_x, int *out_y, int timeout); - -#ifdef __cplusplus -} -#endif diff --git a/installer/updater/update.json b/selfdrive/hardware/eon/neos.json similarity index 100% rename from installer/updater/update.json rename to selfdrive/hardware/eon/neos.json diff --git a/selfdrive/hardware/eon/neos.py b/selfdrive/hardware/eon/neos.py new file mode 100755 index 000000000..13b2e0ee0 --- /dev/null +++ b/selfdrive/hardware/eon/neos.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import json +import logging +import os +import requests + +NEOSUPDATE_DIR = "/data/neoupdate" + +RECOVERY_DEV = "/dev/block/bootdevice/by-name/recovery" +RECOVERY_COMMAND = "/cache/recovery/command" + + +def get_fn(url: str): + return os.path.join(NEOSUPDATE_DIR, os.path.basename(url)) + + +def download_file(url: str, fn: str, sha256: str, display_name: str, cloudlog=logging) -> None: + # check if already downloaded + if check_hash(fn, sha256): + cloudlog.info(f"{display_name} already cached") + return + + try: + with open(fn, "ab+") as f: + headers = {"Range": f"bytes={f.tell()}-"} + r = requests.get(url, stream=True, allow_redirects=True, headers=headers) + r.raise_for_status() + + total = int(r.headers['Content-Length']) + if 'Content-Range' in r.headers: + total = int(r.headers['Content-Range'].split('/')[-1]) + + for chunk in r.iter_content(chunk_size=1024 * 1024): + f.write(chunk) + print(f"Downloading {display_name}: {f.tell() / total * 100}") + except Exception: + cloudlog.error("download error") + if os.path.isfile(fn): + os.unlink(fn) + raise + + if not check_hash(fn, sha256): + if os.path.isfile(fn): + os.unlink(fn) + raise Exception("downloaded update failed hash check") + + +def check_hash(fn: str, sha256: str, length: int = -1) -> bool: + if not os.path.exists(fn): + return False + + h = hashlib.sha256() + with open(fn, "rb") as f: + while f.tell() != length: + r = min(max(0, length - f.tell()), 1024 * 1024) if length > 0 else 1024 * 1024 + dat = f.read(r) + if not dat: + break + h.update(dat) + return h.hexdigest().lower() == sha256.lower() + + +def flash_update(update_fn: str, out_path: str) -> None: + with open(update_fn, "rb") as update, open(out_path, "w+b") as out: + while True: + dat = update.read(8192) + if len(dat) == 0: + break + out.write(dat) + + +def download_neos_update(manifest_path: str, cloudlog=logging) -> None: + with open(manifest_path) as f: + m = json.load(f) + + os.makedirs(NEOSUPDATE_DIR, exist_ok=True) + + # handle recovery updates + if not check_hash(RECOVERY_DEV, m['recovery_hash'], m['recovery_len']): + cloudlog.info("recovery needs update") + recovery_fn = os.path.join(NEOSUPDATE_DIR, os.path.basename(m['recovery_url'])) + download_file(m['recovery_url'], recovery_fn, m['recovery_hash'], "recovery", cloudlog) + + flash_update(recovery_fn, RECOVERY_DEV) + assert check_hash(RECOVERY_DEV, m['recovery_hash'], m['recovery_len']), "recovery flash corrupted" + cloudlog.info("recovery successfully flashed") + + # download OTA update + download_file(m['ota_url'], get_fn(m['ota_url']), m['ota_hash'], "system", cloudlog) + + +def verify_update_ready(manifest_path: str) -> bool: + with open(manifest_path) as f: + m = json.load(f) + + ota_downloaded = check_hash(get_fn(m['ota_url']), m['ota_hash']) + recovery_flashed = check_hash(RECOVERY_DEV, m['recovery_hash'], m['recovery_len']) + return ota_downloaded and recovery_flashed + + +def perform_ota_update(manifest_path: str) -> None: + with open(manifest_path) as f: + m = json.load(f) + + # reboot into recovery + ota_fn = get_fn(m['ota_url']) + with open(RECOVERY_COMMAND, "w") as rf: + rf.write(f"--update_package={ota_fn}\n") + os.system("service call power 16 i32 0 s16 recovery i32 1") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="NEOS update utility", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--swap", action="store_true", help="Peform update after downloading") + parser.add_argument("--swap-if-ready", action="store_true", help="Perform update if already downloaded") + parser.add_argument("manifest", help="Manifest json") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + if args.swap_if_ready: + if verify_update_ready(args.manifest): + perform_ota_update(args.manifest) + else: + download_neos_update(args.manifest, logging) + if args.swap: + perform_ota_update(args.manifest) diff --git a/selfdrive/hardware/eon/test_neos_updater.py b/selfdrive/hardware/eon/test_neos_updater.py new file mode 100755 index 000000000..e258f943d --- /dev/null +++ b/selfdrive/hardware/eon/test_neos_updater.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +import hashlib +import http.server +import json +import os +import unittest +import random +import requests +import shutil +import socketserver +import tempfile +import multiprocessing +from pathlib import Path + +from selfdrive.hardware.eon.neos import RECOVERY_DEV, NEOSUPDATE_DIR, get_fn, download_file, \ + verify_update_ready, download_neos_update + +EON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) +MANIFEST = os.path.join(EON_DIR, "neos.json") + +PORT = 8000 + +def server_thread(port): + socketserver.TCPServer.allow_reuse_address = True + httpd = socketserver.TCPServer(("", port), http.server.SimpleHTTPRequestHandler) + httpd.serve_forever() + + +class TestNeosUpdater(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # generate a fake manifest + cls.manifest = {} + for i in ('ota', 'recovery'): + with tempfile.NamedTemporaryFile(delete=False, dir=os.getcwd()) as f: + dat = os.urandom(random.randint(1000, 100000)) + f.write(dat) + cls.manifest[f"{i}_url"] = f"http://localhost:{PORT}/" + os.path.relpath(f.name) + cls.manifest[F"{i}_hash"] = hashlib.sha256(dat).hexdigest() + if i == "recovery": + cls.manifest["recovery_len"] = len(dat) + + with tempfile.NamedTemporaryFile(delete=False, mode='w') as f: + f.write(json.dumps(cls.manifest)) + cls.fake_manifest = f.name + + @classmethod + def tearDownClass(cls): + os.unlink(cls.fake_manifest) + os.unlink(os.path.basename(cls.manifest['ota_url'])) + os.unlink(os.path.basename(cls.manifest['recovery_url'])) + + def setUp(self): + # server for update files + self.server = multiprocessing.Process(target=server_thread, args=(PORT, )) + self.server.start() + + # clean up + if os.path.exists(NEOSUPDATE_DIR): + shutil.rmtree(NEOSUPDATE_DIR) + + def tearDown(self): + self.server.kill() + self.server.join(1) + + def _corrupt_recovery(self): + with open(RECOVERY_DEV, "wb") as f: + f.write(b'\x00'*100) + + def test_manifest(self): + with open(MANIFEST) as f: + m = json.load(f) + + assert m['ota_hash'] in m['ota_url'] + assert m['recovery_hash'] in m['recovery_url'] + assert m['recovery_len'] > 0 + + for url in (m['ota_url'], m['recovery_url']): + r = requests.head(m['recovery_url']) + r.raise_for_status() + self.assertEqual(r.headers['Content-Type'], "application/octet-stream") + if url == m['recovery_url']: + self.assertEqual(int(r.headers['Content-Length']), m['recovery_len']) + + def test_download_hash_check(self): + os.makedirs(NEOSUPDATE_DIR, exist_ok=True) + Path(get_fn(self.manifest['ota_url'])).touch() + with self.assertRaisesRegex(Exception, "failed hash check"): + download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), + self.manifest['ota_hash']+'a', "system") + + # should've unlinked after the failed hash check, should succeed now + download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), + self.manifest['ota_hash'], "system") + + # TODO: needs an http server that supports Content-Range + #def test_download_resume(self): + # os.makedirs(NEOSUPDATE_DIR, exist_ok=True) + # with open(os.path.basename(self.manifest['ota_url']), "rb") as src, \ + # open(get_fn(self.manifest['ota_url']), "wb") as dest: + # l = dest.write(src.read(8192)) + # assert l == 8192 + # download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), + # self.manifest['ota_hash'], "system") + + def test_download_no_internet(self): + self.server.kill() + os.makedirs(NEOSUPDATE_DIR, exist_ok=True) + # fail, no internet + with self.assertRaises(requests.exceptions.ConnectionError): + download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), + self.manifest['ota_hash'], "system") + + # already cached, ensure we don't hit the server + shutil.copyfile(os.path.basename(self.manifest['ota_url']), get_fn(self.manifest['ota_url'])) + download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), + self.manifest['ota_hash'], "system") + + + def test_download_update(self): + download_neos_update(self.fake_manifest) + self.assertTrue(verify_update_ready(self.fake_manifest)) + + def test_verify_update(self): + # good state + download_neos_update(self.fake_manifest) + self.assertTrue(verify_update_ready(self.fake_manifest)) + + # corrupt recovery + self._corrupt_recovery() + self.assertFalse(verify_update_ready(self.fake_manifest)) + + # back to good state + download_neos_update(self.fake_manifest) + self.assertTrue(verify_update_ready(self.fake_manifest)) + + # corrupt ota + self._corrupt_recovery() + with open(os.path.join(NEOSUPDATE_DIR, os.path.basename(self.manifest['ota_url'])), "ab") as f: + f.write(b'\x00') + self.assertFalse(verify_update_ready(self.fake_manifest)) + +if __name__ == "__main__": + unittest.main() diff --git a/selfdrive/hardware/eon/updater b/selfdrive/hardware/eon/updater new file mode 100755 index 000000000..ff44c7251 Binary files /dev/null and b/selfdrive/hardware/eon/updater differ diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 6d7f32014..8b75dda19 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -58,17 +58,18 @@ qt_src = ["main.cc", "ui.cc", "paint.cc", "qt/sidebar.cc", "qt/onroad.cc", qt_env.Program("_ui", qt_src + [asset_obj], LIBS=qt_libs) -# setup, factory resetter, and agnos updater +# setup and factory resetter if arch != 'aarch64' and GetOption('setup'): - qt_env.Program("qt/setup/reset", ["qt/setup/reset.cc"], LIBS=qt_libs) - qt_env.Program("qt/setup/updater", ["qt/setup/updater.cc", asset_obj], LIBS=qt_libs) qt_env.Program("qt/setup/setup", ["qt/setup/setup.cc", asset_obj], LIBS=qt_libs + ['curl', 'common', 'json11']) -# build installers if GetOption('setup'): + # buidl updater UI + qt_env.Program("qt/setup/updater", ["qt/setup/updater.cc", asset_obj], LIBS=qt_libs) + + # build installers senv = qt_env.Clone() senv['LINKFLAGS'].append('-Wl,-strip-debug') diff --git a/selfdrive/ui/qt/setup/updater.cc b/selfdrive/ui/qt/setup/updater.cc index c8055a9c0..d39f6d149 100644 --- a/selfdrive/ui/qt/setup/updater.cc +++ b/selfdrive/ui/qt/setup/updater.cc @@ -5,9 +5,11 @@ #include "selfdrive/hardware/hw.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/offroad/networking.h" #include "selfdrive/ui/qt/setup/updater.h" +#ifndef QCOM +#include "selfdrive/ui/qt/offroad/networking.h" +#endif Updater::Updater(const QString &updater_path, const QString &manifest_path, QWidget *parent) : updater(updater_path), manifest(manifest_path), QStackedWidget(parent) { @@ -41,7 +43,11 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid QPushButton *connect = new QPushButton("Connect to WiFi"); connect->setObjectName("navBtn"); QObject::connect(connect, &QPushButton::clicked, [=]() { +#ifndef QCOM setCurrentWidget(wifi); +#else + HardwareEon::launch_wifi(); +#endif }); hlayout->addWidget(connect); @@ -58,9 +64,11 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid QVBoxLayout *layout = new QVBoxLayout(wifi); layout->setContentsMargins(100, 100, 100, 100); +#ifndef QCOM Networking *networking = new Networking(this, false); networking->setStyleSheet("Networking { background-color: #292929; border-radius: 13px; }"); layout->addWidget(networking, 1); +#endif QPushButton *back = new QPushButton("Back"); back->setObjectName("navBtn"); @@ -78,7 +86,7 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid layout->setContentsMargins(150, 330, 150, 150); layout->setSpacing(0); - text = new QLabel("Installing..."); + text = new QLabel("Loading..."); text->setStyleSheet("font-size: 90px; font-weight: 600;"); layout->addWidget(text, 0, Qt::AlignTop); @@ -111,6 +119,7 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid setStyleSheet(R"( * { color: white; + outline: none; font-family: Inter; } Updater { @@ -166,10 +175,25 @@ void Updater::updateFinished(int exitCode, QProcess::ExitStatus exitStatus) { } } +bool Updater::eventFilter(QObject *obj, QEvent *event) { +#ifdef QCOM + // filter out touches while in android activity + const static QSet filter_events({QEvent::MouseButtonPress, QEvent::MouseMove, QEvent::TouchBegin, QEvent::TouchUpdate, QEvent::TouchEnd}); + if (HardwareEon::launched_activity && filter_events.contains(event->type())) { + HardwareEon::check_activity(); + if (HardwareEon::launched_activity) { + return true; + } + } +#endif + return false; +} + int main(int argc, char *argv[]) { initApp(); QApplication a(argc, argv); Updater updater(argv[1], argv[2]); setMainWindow(&updater); + a.installEventFilter(&updater); return a.exec(); } diff --git a/selfdrive/ui/qt/setup/updater.h b/selfdrive/ui/qt/setup/updater.h index af7373aae..32aef43f5 100644 --- a/selfdrive/ui/qt/setup/updater.h +++ b/selfdrive/ui/qt/setup/updater.h @@ -19,12 +19,13 @@ private slots: void updateFinished(int exitCode, QProcess::ExitStatus exitStatus); private: + bool eventFilter(QObject *obj, QEvent *event) override; + + QProcess proc; QString updater, manifest; QLabel *text; QProgressBar *bar; QPushButton *reboot; - QProcess proc; - QWidget *prompt, *wifi, *progress; }; diff --git a/selfdrive/updated.py b/selfdrive/updated.py index 7d5845586..f3fb5d8d8 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -247,6 +247,8 @@ def handle_agnos_update(wait_helper): def handle_neos_update(wait_helper: WaitTimeHelper) -> None: + from selfdrive.hardware.eon.neos import download_neos_update + cur_neos = HARDWARE.get_os_version() updated_neos = run(["bash", "-c", r"unset REQUIRED_NEOS_VERSION && source launch_env.sh && \ echo -n $REQUIRED_NEOS_VERSION"], OVERLAY_MERGED).strip() @@ -258,8 +260,7 @@ def handle_neos_update(wait_helper: WaitTimeHelper) -> None: cloudlog.info(f"Beginning background download for NEOS {updated_neos}") set_offroad_alert("Offroad_NeosUpdate", True) - updater_path = os.path.join(OVERLAY_MERGED, "installer/updater/updater") - update_manifest = f"file://{OVERLAY_MERGED}/installer/updater/update.json" + update_manifest = os.path.join(OVERLAY_MERGED, "selfdrive/hardware/eon/neos.json") neos_downloaded = False start_time = time.monotonic() @@ -268,9 +269,9 @@ def handle_neos_update(wait_helper: WaitTimeHelper) -> None: (time.monotonic() - start_time < 60*60*24): wait_helper.ready_event.clear() try: - run([updater_path, "bgcache", update_manifest], OVERLAY_MERGED, low_priority=True) + download_neos_update(update_manifest, cloudlog) neos_downloaded = True - except subprocess.CalledProcessError: + except Exception: cloudlog.info("NEOS background download failed, retrying") wait_helper.sleep(120)