Unify neos/agnos updaters (#22109)
* start moving neos updater
* downloading
* ui
* move recovery
* resuming
* add verification
* fix up launch
* test
* update updater
* fix mypy
* fake updater
* review suggestions
* more tests
* abc
* update bin
* raise
Co-authored-by: Comma Device <device@comma.ai>
old-commit-hash: b3705ede5e
commatwo_master
parent
03ba5918c8
commit
9193e1afa1
22 changed files with 329 additions and 1294 deletions
@ -1,3 +0,0 @@ |
||||
local/ |
||||
prod/ |
||||
staging/ |
@ -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) |
@ -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() |
@ -1,3 +1,3 @@ |
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:24a8cab38305235147ce16fe823617b3b09f64fc2912e939d1bfa38a697a0594 |
||||
size 2513688 |
||||
oid sha256:2ca3679a8e9a066bccbf8cb27377a40bad65e348d668040adda37c27ff6bad4d |
||||
size 69 |
||||
|
@ -1,803 +0,0 @@ |
||||
#include <sys/stat.h> |
||||
#include <sys/statvfs.h> |
||||
#include <unistd.h> |
||||
|
||||
#include <cassert> |
||||
#include <cstdio> |
||||
#include <cstdlib> |
||||
#include <cstring> |
||||
#include <fstream> |
||||
#include <iostream> |
||||
#include <memory> |
||||
#include <mutex> |
||||
#include <sstream> |
||||
#include <string> |
||||
#include <thread> |
||||
|
||||
#include <curl/curl.h> |
||||
#include <openssl/sha.h> |
||||
#include <EGL/egl.h> |
||||
#include <EGL/eglext.h> |
||||
#include <GLES3/gl3.h> |
||||
#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<char[]> 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<FrameBuffer> 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<FrameBuffer>("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<std::mutex> 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<std::mutex> guard(lock); |
||||
progress_text = text; |
||||
} |
||||
|
||||
void set_error(std::string text) { |
||||
std::lock_guard<std::mutex> guard(lock); |
||||
error_text = text; |
||||
state = ERROR; |
||||
} |
||||
|
||||
void set_battery_low() { |
||||
std::lock_guard<std::mutex> guard(lock); |
||||
state = LOW_BATTERY; |
||||
} |
||||
|
||||
void set_running() { |
||||
std::lock_guard<std::mutex> 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<char[]> 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<std::mutex> 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<std::mutex> 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; |
||||
} |
@ -1,146 +0,0 @@ |
||||
#include "selfdrive/common/framebuffer.h" |
||||
|
||||
#include <cstdio> |
||||
#include <cassert> |
||||
|
||||
#include "selfdrive/common/util.h" |
||||
|
||||
#include <ui/DisplayInfo.h> |
||||
|
||||
#include <gui/ISurfaceComposer.h> |
||||
#include <gui/Surface.h> |
||||
#include <gui/SurfaceComposerClient.h> |
||||
#include <GLES2/gl2.h> |
||||
#include <EGL/eglext.h> |
||||
|
||||
#define BACKLIGHT_LEVEL 205 |
||||
|
||||
using namespace android; |
||||
|
||||
struct FramebufferState { |
||||
sp<SurfaceComposerClient> session; |
||||
sp<IBinder> dtoken; |
||||
DisplayInfo dinfo; |
||||
sp<SurfaceControl> control; |
||||
|
||||
sp<Surface> 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; |
||||
} |
@ -1,18 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <cstdlib> |
||||
|
||||
#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; |
||||
}; |
@ -1,96 +0,0 @@ |
||||
#include "selfdrive/common/touch.h" |
||||
|
||||
#include <assert.h> |
||||
#include <dirent.h> |
||||
#include <fcntl.h> |
||||
#include <linux/input.h> |
||||
#include <stdbool.h> |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <sys/poll.h> |
||||
#include <unistd.h> |
||||
|
||||
/* 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; |
||||
} |
@ -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 |
@ -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) |
@ -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() |
@ -0,0 +1,3 @@ |
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:cee626f7d0ea1b464b5c57f3dbc6996af15d61ffca611f1934b5ac89c9cac8b8 |
||||
size 3178392 |
Loading…
Reference in new issue