diff --git a/installer/Makefile b/installer/Makefile new file mode 100644 index 00000000..4fbe3cf2 --- /dev/null +++ b/installer/Makefile @@ -0,0 +1,120 @@ +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 -fPIC -O2 $(WARN_FLAGS) +CXXFLAGS = -std=c++11 -fPIC -O2 $(WARN_FLAGS) + +NANOVG_FLAGS = -I$(PHONELIBS)/nanovg + +OPENGL_LIBS = -lGLESv3 + +FRAMEBUFFER_LIBS = -lutils -lgui -lEGL + +COMMON_OBJS = ../selfdrive/common/glutil.o \ + ../selfdrive/common/framebuffer.o \ + $(PHONELIBS)/nanovg/nanovg.o \ + ../selfdrive/common/spinner.o \ + opensans_semibold.o \ + img_spinner_track.o \ + img_spinner_comma.o + +OPENPILOT_OBJS = installer_openpilot.o \ + continue_openpilot.o \ + $(COMMON_OBJS) + +DASHCAM_OBJS = installer_dashcam.o \ + continue_dashcam.o \ + $(COMMON_OBJS) + +DEPS := $(OPENPILOT_OBJS:.o=.d) $(DASHCAM_OBJS:.o=.d) + +.PHONY: all +all: installers/installer_dashcam installers/installer_openpilot + +installers/installer_openpilot: $(OPENPILOT_OBJS) + @echo "[ LINK ] $@" + $(CXX) -fPIC -o '$@' $^ \ + -s \ + $(FRAMEBUFFER_LIBS) \ + -L/system/vendor/lib64 \ + $(OPENGL_LIBS) \ + -lm -llog + +installers/installer_dashcam: $(DASHCAM_OBJS) + @echo "[ LINK ] $@" + $(CXX) -fPIC -o '$@' $^ \ + -s \ + $(FRAMEBUFFER_LIBS) \ + -L/system/vendor/lib64 \ + $(OPENGL_LIBS) \ + -lm -llog + +../selfdrive/common/framebuffer.o: ../selfdrive/common/framebuffer.cc + @echo "[ CXX ] $@" + $(CXX) $(CXXFLAGS) -MMD \ + -I$(PHONELIBS)/android_frameworks_native/include \ + -I$(PHONELIBS)/android_system_core/include \ + -I$(PHONELIBS)/android_hardware_libhardware/include \ + -c -o '$@' '$<' + +../selfdrive/common/spinner.o: ../selfdrive/common/spinner.c + @echo "[ CXX ] $@" + $(CXX) $(CXXFLAGS) -MMD \ + -I$(PHONELIBS)/android_frameworks_native/include \ + -I$(PHONELIBS)/android_system_core/include \ + -I$(PHONELIBS)/android_hardware_libhardware/include \ + $(NANOVG_FLAGS) \ + -c -o '$@' '$<' + +opensans_semibold.o: ../selfdrive/assets/fonts/opensans_semibold.ttf + @echo "[ bin2o ] $@" + cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)' + +img_spinner_track.o: ../selfdrive/assets/img_spinner_track.png + @echo "[ bin2o ] $@" + cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)' + +img_spinner_comma.o: ../selfdrive/assets/img_spinner_comma.png + @echo "[ bin2o ] $@" + cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)' + +installer_openpilot.o: installer.c + @echo "[ CC ] $@" + $(CC) $(CFLAGS) -MMD \ + -I.. -I../selfdrive \ + -DBRAND=openpilot \ + -DBRANCH=release2 \ + -c -o '$@' '$<' + +installer_dashcam.o: installer.c + @echo "[ CC ] $@" + $(CC) $(CFLAGS) -MMD \ + -I.. -I../selfdrive \ + -DBRAND=dashcam \ + -DBRANCH=dashcam \ + -c -o '$@' '$<' + +%.o: %.c + @echo "[ CC ] $@" + $(CC) $(CFLAGS) -MMD \ + -I.. -I../selfdrive \ + -c -o '$@' '$<' + +%.o: %.sh + @echo "[ bin2o ] $@" + cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)' + +.PHONY: clean +clean: + rm -f installer_openpilot installer_dashcam $(OPENPILOT_OBJS) $(DASHCAM_OBJS) $(DEPS) + +-include $(DEPS) diff --git a/installer/continue_dashcam.sh b/installer/continue_dashcam.sh new file mode 100755 index 00000000..25233fff --- /dev/null +++ b/installer/continue_dashcam.sh @@ -0,0 +1,4 @@ +#!/usr/bin/bash + +cd /data/openpilot +exec ./launch_chffrplus.sh diff --git a/installer/continue_openpilot.sh b/installer/continue_openpilot.sh new file mode 100755 index 00000000..3da67313 --- /dev/null +++ b/installer/continue_openpilot.sh @@ -0,0 +1,4 @@ +#!/usr/bin/bash + +cd /data/openpilot +exec ./launch_openpilot.sh diff --git a/installer/installer.c b/installer/installer.c new file mode 100644 index 00000000..1892f763 --- /dev/null +++ b/installer/installer.c @@ -0,0 +1,140 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "common/util.h" +#include "common/mat.h" +#include "common/glutil.h" +#include "common/framebuffer.h" +#include "common/spinner.h" + +#ifndef BRAND +#define BRAND openpilot +#endif + +#define STR(X) #X +#define STR2(X) STR(X) +#define PASTE(A, B) A ## B +#define PASTE2(A, B) PASTE(A, B) +#define BRAND_S STR2(BRAND) +#define BRANCH_S STR2(BRANCH) + +#define PRE_CHECKOUT_FOLDER "/system/comma/openpilot" +#define GIT_CLONE_COMMAND "git clone https://github.com/commaai/openpilot.git " + + +extern const uint8_t str_continue[] asm("_binary_continue_" BRAND_S "_sh_start"); +extern const uint8_t str_continue_end[] asm("_binary_continue_" BRAND_S "_sh_end"); + +static int use_pre_checkout() { + int err; + + // Cleanup + err = system("rm -rf /tmp/openpilot"); + if(err) return 1; + err = system("rm -rf /data/openpilot"); + if(err) return 1; + + // Copy pre checkout into tmp so we can work on it + err = system("cp -rp " PRE_CHECKOUT_FOLDER " /tmp"); + if(err) return 1; + + err = chdir("/tmp/openpilot"); + if(err) return 1; + + // Checkout correct branch + err = system("git remote set-branches --add origin " BRANCH_S); + if(err) return 1; + err = system("git fetch origin " BRANCH_S); + if(err) return 1; + err = system("git checkout " BRANCH_S); + if(err) return 1; + + // Move to final location + err = system("mv /tmp/openpilot /data"); + if(err) return 1; + + return 0; +} + +static int fresh_clone() { + int err; + + // Cleanup + err = chdir("/tmp"); + if(err) return 1; + err = system("rm -rf /tmp/openpilot"); + if(err) return 1; + + err = system(GIT_CLONE_COMMAND " -b " BRANCH_S " --depth=1 openpilot"); + if(err) return 1; + + // Cleanup old folder in /data + err = system("rm -rf /data/openpilot"); + if(err) return 1; + + // this won't move if /data/openpilot exists + err = system("mv /tmp/openpilot /data"); + if(err) return 1; + + return 0; +} + +static int do_install() { + int err; + + struct stat sb; + if (stat(PRE_CHECKOUT_FOLDER, &sb) == 0 && S_ISDIR(sb.st_mode)) { + printf("Pre-checkout found\n"); + err = use_pre_checkout(); + } else { + printf("Doing fresh clone\n"); + err = fresh_clone(); + } + if(err) return 1; + + + // Write continue.sh + FILE *of = fopen("/data/data/com.termux/files/continue.sh", "wb"); + assert(of); + + size_t num = str_continue_end - str_continue; + size_t num_written = fwrite(str_continue, 1, num, of); + if (num != num_written) return 1; + + fclose(of); + + err = system("chmod +x /data/data/com.termux/files/continue.sh"); + if(err) return 1; + + return 0; +} + + +void * run_spinner(void * args) { + char *loading_msg = "Installing " BRAND_S; + char *argv[2] = {NULL, loading_msg}; + spin(2, argv); + return NULL; +} + + +int main() { + pthread_t spinner_thread; + assert(pthread_create(&spinner_thread, NULL, run_spinner, NULL) == 0); + + int status = do_install(); + + return status; +} diff --git a/installer/installers/deploy.sh b/installer/installers/deploy.sh new file mode 100755 index 00000000..2b904f0e --- /dev/null +++ b/installer/installers/deploy.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -e + +ENVIRONMENT="${1}" +if [ "${ENVIRONMENT}" != "staging" -a "${ENVIRONMENT}" != "prod" ]; then + echo "usage: $0 " >&2 + echo " = staging or prod" >&2 + exit 1 +fi + +SUFFIX="" +if [ "${ENVIRONMENT}" != "prod" ]; then + SUFFIX="_test" +fi + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +if [[ -z $(az account show 2>/dev/null) ]]; then + echo "$(date --rfc-3339=s) LOGIN: azure" + az login +fi + +FILES=( +installer_openpilot +installer_dashcam +) +for FILE in ${FILES[@]}; do + KEY="${FILE}${SUFFIX}" + echo "$(date --rfc-3339=s) PUSHING: ${FILE} -> ${KEY}" + az storage blob upload \ + --account-name commadist \ + --container-name neosupdate \ + --name "${KEY}" \ + --file "${FILE}" +done diff --git a/installer/installers/installer_dashcam b/installer/installers/installer_dashcam new file mode 100755 index 00000000..a907b874 Binary files /dev/null and b/installer/installers/installer_dashcam differ diff --git a/installer/installers/installer_openpilot b/installer/installers/installer_openpilot new file mode 100755 index 00000000..c3740131 Binary files /dev/null and b/installer/installers/installer_openpilot differ diff --git a/installer/updater/.gitignore b/installer/updater/.gitignore new file mode 100644 index 00000000..339d49be --- /dev/null +++ b/installer/updater/.gitignore @@ -0,0 +1,3 @@ +local/ +prod/ +staging/ diff --git a/installer/updater/Makefile b/installer/updater/Makefile new file mode 100644 index 00000000..d252fc2a --- /dev/null +++ b/installer/updater/Makefile @@ -0,0 +1,98 @@ +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++11 -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/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/update.json b/installer/updater/update.json new file mode 100644 index 00000000..6e2692bf --- /dev/null +++ b/installer/updater/update.json @@ -0,0 +1,7 @@ +{ + "ota_url": "https://commadist.azureedge.net/neosupdate/ota-signed-efdf7de63b1aef63d68301e6175930991bf9a5927d16ec6fcc69287e2ee7ca4a.zip", + "ota_hash": "efdf7de63b1aef63d68301e6175930991bf9a5927d16ec6fcc69287e2ee7ca4a", + "recovery_url": "https://commadist.azureedge.net/neosupdate/recovery-97c27e6ed04ed6bb0608b845a2d4100912093f9380c3f2ba6b56bccd608e5f6e.img", + "recovery_len": 15861036, + "recovery_hash": "97c27e6ed04ed6bb0608b845a2d4100912093f9380c3f2ba6b56bccd608e5f6e" +} diff --git a/installer/updater/updater b/installer/updater/updater new file mode 100755 index 00000000..15858eab Binary files /dev/null and b/installer/updater/updater differ diff --git a/installer/updater/updater.cc b/installer/updater/updater.cc new file mode 100644 index 00000000..a76be8b8 --- /dev/null +++ b/installer/updater/updater.cc @@ -0,0 +1,768 @@ +#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 "nanovg_gl.h" +#include "nanovg_gl_utils.h" + +#include "json11.hpp" + +#include "common/framebuffer.h" +#include "common/touch.h" +#include "common/utilpp.h" + +#define USER_AGENT "NEOSUpdater-0.2" + +#define MANIFEST_URL_EON_STAGING "https://github.com/commaai/eon-neos/raw/master/update.staging.json" +#define MANIFEST_URL_EON_LOCAL "http://192.168.5.1:8000/neosupdate/update.local.json" +#define MANIFEST_URL_EON "https://github.com/commaai/eon-neos/raw/master/update.json" +const char *manifest_url = MANIFEST_URL_EON; + +#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, 1); + 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); +} + +struct Updater { + bool do_exit = false; + + TouchState touch; + + int fb_w, fb_h; + + FramebufferState *fb = NULL; + NVGcontext *vg = NULL; + int font_regular; + int font_semibold; + int font_bold; + + std::thread update_thread_handle; + + std::mutex lock; + + // i hate state machines give me coroutines already + 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; + + CURL *curl = NULL; + + Updater() { + touch_init(&touch); + + fb = framebuffer_init("updater", 0x00001000, false, + &fb_w, &fb_h); + assert(fb); + + 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; + + 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, 1); + 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 stage_download(std::string url, std::string hash, std::string name) { + std::string out_fn = UPDATE_DIR "/" + util::base_name(url); + + set_progress("Downloading " + name + "..."); + bool r = download_file(url, out_fn); + if (!r) { + set_error("failed to download " + name); + return ""; + } + + set_progress("Verifying " + name + "..."); + std::string fn_hash = sha256_file(out_fn); + 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; + } + + void run_stages() { + curl = curl_easy_init(); + assert(curl); + + 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); + usleep(1000000); + } + set_running(); + } + + if (!check_space()) { + set_error("2GB of free space required to update"); + return; + } + + mkdir(UPDATE_DIR, 0777); + + const int EON = (access("/EON", F_OK) != -1); + + set_progress("Finding latest version..."); + std::string manifest_s; + if (EON) { + manifest_s = download_string(curl, manifest_url); + } else { + // don't update NEO + exit(0); + } + + 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; + } + + 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(); + std::string recovery_hash = manifest["recovery_hash"].string_value(); + int 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; + } + + // std::string installer_fn = stage_download(installer_url, installer_hash, "installer"); + // if (installer_fn.empty()) { + // //error'd + // return; + // } + + std::string recovery_fn; + 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 = stage_download(recovery_url, recovery_hash, "recovery"); + if (recovery_fn.empty()) { + // error'd + return; + } + } + } + + std::string ota_fn = stage_download(ota_url, ota_hash, "update"); + if (ota_fn.empty()) { + //error'd + return; + } + + 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); + usleep(1000000); + } + 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(1) 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 EON is connected to power.", 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); + + switch (state) { + case ERROR: + case 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; + } + } + } + } + default: + break; + } + } + + + void go() { + 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); + + framebuffer_swap(fb); + + assert(glGetError() == GL_NO_ERROR); + + // no simple way to do 30fps vsync with surfaceflinger... + usleep(30000); + } + + if (update_thread_handle.joinable()) { + update_thread_handle.join(); + } + + system("service call power 16 i32 0 i32 0 i32 1"); + } + + 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; + } + +}; + +} +int main(int argc, char *argv[]) { + if (argc > 1) { + if (strcmp(argv[1], "local") == 0) { + manifest_url = MANIFEST_URL_EON_LOCAL; + } else if (strcmp(argv[1], "staging") == 0) { + manifest_url = MANIFEST_URL_EON_STAGING; + } else { + manifest_url = argv[1]; + } + } + printf("updating from %s\n", manifest_url); + Updater updater; + updater.go(); + + return 0; +}