replay: ncurses GUI (#23608)
* initial version
* print all message's in ncurses window
* show download progress bar
* move all to class ConsoleUI
* timeline
* improve timeline&stats
* fix logMessage
* add warning indicator
* continue
* cleanup
* cast type to int
* simplify seekToFlag
* more
* <=
* handle enter
* add box to logging window
* fix multiple threads problem
* fix concurrency issues
* draw indicator
* many improvements
* more
* fix multipe threads logging
* stop replay before exit
* use lambda instead of std::bind
* cleanup
* small cleanup
* use carFingerPrint
* don't emit signal in replay::stream
* merge car_events into timeline
* cleanup DonloadStats
* cleanup
* rename carname to carFingerprint
* improve alert
* add comments
* add help functions
templete function
* handle term resize
* display replaying status
* rename to INSTANT
* helper function pauseReplay
* more
* cleanup
use rDebug
* no template
* less colors
* remove function mv_add_str
* use BORDER_SIZE
* tune colors
* add spaces
* apply reviews
use /
old-commit-hash: 3ca8e3653b
commatwo_master
parent
31857ee45b
commit
c8b528e850
13 changed files with 634 additions and 220 deletions
@ -0,0 +1,353 @@ |
|||||||
|
#include "selfdrive/ui/replay/consoleui.h" |
||||||
|
|
||||||
|
#include <QApplication> |
||||||
|
#include <initializer_list> |
||||||
|
|
||||||
|
#include "selfdrive/common/version.h" |
||||||
|
|
||||||
|
namespace { |
||||||
|
|
||||||
|
const int BORDER_SIZE = 3; |
||||||
|
|
||||||
|
const std::initializer_list<std::pair<std::string, std::string>> keyboard_shortcuts[] = { |
||||||
|
{ |
||||||
|
{"s", "+10s"}, |
||||||
|
{"shift+s", "-10s"}, |
||||||
|
{"m", "+60s"}, |
||||||
|
{"shift+m", "-60s"}, |
||||||
|
{"p", "Pause/Resume"}, |
||||||
|
{"e", "Next Engagement"}, |
||||||
|
{"d", "Next Disengagement"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
{"enter", "Enter seek request"}, |
||||||
|
{"x", "+/-Replay speed"}, |
||||||
|
{"q", "Exit"}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
enum Color { |
||||||
|
Default, |
||||||
|
Debug, |
||||||
|
Yellow, |
||||||
|
Green, |
||||||
|
Red, |
||||||
|
BrightWhite, |
||||||
|
Engaged, |
||||||
|
Disengaged, |
||||||
|
}; |
||||||
|
|
||||||
|
void add_str(WINDOW *w, const char *str, Color color = Color::Default, bool bold = false) { |
||||||
|
if (color != Color::Default) wattron(w, COLOR_PAIR(color)); |
||||||
|
if (bold) wattron(w, A_BOLD); |
||||||
|
waddstr(w, str); |
||||||
|
if (bold) wattroff(w, A_BOLD); |
||||||
|
if (color != Color::Default) wattroff(w, COLOR_PAIR(color)); |
||||||
|
} |
||||||
|
|
||||||
|
std::string format_seconds(int s) { |
||||||
|
int total_minutes = s / 60; |
||||||
|
int seconds = s % 60; |
||||||
|
int hours = total_minutes / 60; |
||||||
|
int minutes = total_minutes % 60; |
||||||
|
return util::string_format("%02d:%02d:%02d", hours, minutes, seconds); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ConsoleUI::ConsoleUI(Replay *replay, QObject *parent) : replay(replay), sm({"carState", "liveParameters"}), QObject(parent) { |
||||||
|
// Initialize curses
|
||||||
|
initscr(); |
||||||
|
clear(); |
||||||
|
curs_set(false); |
||||||
|
cbreak(); // Line buffering disabled. pass on everything
|
||||||
|
noecho(); |
||||||
|
keypad(stdscr, true); |
||||||
|
nodelay(stdscr, true); // non-blocking getchar()
|
||||||
|
|
||||||
|
// Initialize all the colors. https://www.ditig.com/256-colors-cheat-sheet
|
||||||
|
start_color(); |
||||||
|
init_pair(Color::Debug, 246, COLOR_BLACK); // #949494
|
||||||
|
init_pair(Color::Yellow, 184, COLOR_BLACK); |
||||||
|
init_pair(Color::Red, COLOR_RED, COLOR_BLACK); |
||||||
|
init_pair(Color::BrightWhite, 15, COLOR_BLACK); |
||||||
|
init_pair(Color::Disengaged, COLOR_BLUE, COLOR_BLUE); |
||||||
|
init_pair(Color::Engaged, 28, 28); |
||||||
|
init_pair(Color::Green, 34, COLOR_BLACK); |
||||||
|
|
||||||
|
initWindows(); |
||||||
|
|
||||||
|
qRegisterMetaType<uint64_t>("uint64_t"); |
||||||
|
qRegisterMetaType<ReplyMsgType>("ReplyMsgType"); |
||||||
|
installMessageHandler([this](ReplyMsgType type, const std::string msg) { |
||||||
|
emit logMessageSignal(type, QString::fromStdString(msg)); |
||||||
|
}); |
||||||
|
installDownloadProgressHandler([this](uint64_t cur, uint64_t total, bool success) { |
||||||
|
emit updateProgressBarSignal(cur, total, success); |
||||||
|
}); |
||||||
|
|
||||||
|
QObject::connect(replay, &Replay::streamStarted, this, &ConsoleUI::updateSummary); |
||||||
|
QObject::connect(¬ifier, SIGNAL(activated(int)), SLOT(readyRead())); |
||||||
|
QObject::connect(this, &ConsoleUI::updateProgressBarSignal, this, &ConsoleUI::updateProgressBar); |
||||||
|
QObject::connect(this, &ConsoleUI::logMessageSignal, this, &ConsoleUI::logMessage); |
||||||
|
|
||||||
|
sm_timer.callOnTimeout(this, &ConsoleUI::updateStatus); |
||||||
|
sm_timer.start(100); |
||||||
|
getch_timer.start(1000, this); |
||||||
|
readyRead(); |
||||||
|
} |
||||||
|
|
||||||
|
ConsoleUI::~ConsoleUI() { |
||||||
|
endwin(); |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::initWindows() { |
||||||
|
getmaxyx(stdscr, max_height, max_width); |
||||||
|
w.fill(nullptr); |
||||||
|
w[Win::Title] = newwin(1, max_width, 0, 0); |
||||||
|
w[Win::Stats] = newwin(2, max_width - 2 * BORDER_SIZE, 2, BORDER_SIZE); |
||||||
|
w[Win::Timeline] = newwin(4, max_width - 2 * BORDER_SIZE, 5, BORDER_SIZE); |
||||||
|
w[Win::TimelineDesc] = newwin(1, 100, 10, BORDER_SIZE); |
||||||
|
w[Win::CarState] = newwin(3, 100, 12, BORDER_SIZE); |
||||||
|
w[Win::DownloadBar] = newwin(1, 100, 16, BORDER_SIZE); |
||||||
|
if (int log_height = max_height - 27; log_height > 4) { |
||||||
|
w[Win::LogBorder] = newwin(log_height, max_width - 2 * (BORDER_SIZE - 1), 17, BORDER_SIZE - 1); |
||||||
|
box(w[Win::LogBorder], 0, 0); |
||||||
|
w[Win::Log] = newwin(log_height - 2, max_width - 2 * BORDER_SIZE, 18, BORDER_SIZE); |
||||||
|
scrollok(w[Win::Log], true); |
||||||
|
} |
||||||
|
w[Win::Help] = newwin(5, max_width - (2 * BORDER_SIZE), max_height - 6, BORDER_SIZE); |
||||||
|
|
||||||
|
// set the title bar
|
||||||
|
wbkgd(w[Win::Title], A_REVERSE); |
||||||
|
mvwprintw(w[Win::Title], 0, 3, "openpilot replay %s", COMMA_VERSION); |
||||||
|
|
||||||
|
// show windows on the real screen
|
||||||
|
refresh(); |
||||||
|
displayTimelineDesc(); |
||||||
|
displayHelp(); |
||||||
|
updateSummary(); |
||||||
|
updateTimeline(); |
||||||
|
for (auto win : w) { |
||||||
|
if (win) wrefresh(win); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::timerEvent(QTimerEvent *ev) { |
||||||
|
if (ev->timerId() != getch_timer.timerId()) return; |
||||||
|
|
||||||
|
if (is_term_resized(max_height, max_width)) { |
||||||
|
for (auto win : w) { |
||||||
|
if (win) delwin(win); |
||||||
|
} |
||||||
|
endwin(); |
||||||
|
clear(); |
||||||
|
refresh(); |
||||||
|
initWindows(); |
||||||
|
rWarning("resize term %dx%d", max_height, max_width); |
||||||
|
} |
||||||
|
updateTimeline(); |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::updateStatus() { |
||||||
|
auto write_item = [this](int y, int x, const char *key, const std::string &value, const char *unit, |
||||||
|
bool bold = false, Color color = Color::BrightWhite) { |
||||||
|
auto win = w[Win::CarState]; |
||||||
|
wmove(win, y, x); |
||||||
|
add_str(win, key); |
||||||
|
add_str(win, value.c_str(), color, bold); |
||||||
|
add_str(win, unit); |
||||||
|
}; |
||||||
|
static const std::pair<const char *, Color> status_text[] = { |
||||||
|
{"loading...", Color::Red}, |
||||||
|
{"playing", Color::Green}, |
||||||
|
{"paused...", Color::Yellow}, |
||||||
|
}; |
||||||
|
|
||||||
|
sm.update(0); |
||||||
|
|
||||||
|
if (status != Status::Paused) { |
||||||
|
status = (sm.updated("carState") || sm.updated("liveParameters")) ? Status::Playing : Status::Waiting; |
||||||
|
} |
||||||
|
auto [status_str, status_color] = status_text[status]; |
||||||
|
write_item(0, 0, "STATUS: ", status_str, " ", false, status_color); |
||||||
|
std::string suffix = util::string_format(" / %s [%d/%d] ", format_seconds(replay->totalSeconds()).c_str(), |
||||||
|
replay->currentSeconds() / 60, replay->route()->segments().size()); |
||||||
|
write_item(0, 25, "TIME: ", format_seconds(replay->currentSeconds()), suffix.c_str(), true); |
||||||
|
|
||||||
|
auto p = sm["liveParameters"].getLiveParameters(); |
||||||
|
write_item(1, 0, "STIFFNESS: ", util::string_format("%.2f %%", p.getStiffnessFactor() * 100), " "); |
||||||
|
write_item(1, 25, "SPEED: ", util::string_format("%.2f", sm["carState"].getCarState().getVEgo()), " m/s"); |
||||||
|
write_item(2, 0, "STEER RATIO: ", util::string_format("%.2f", p.getSteerRatio()), ""); |
||||||
|
auto angle_offsets = util::string_format("%.2f|%.2f", p.getAngleOffsetAverageDeg(), p.getAngleOffsetDeg()); |
||||||
|
write_item(2, 25, "ANGLE OFFSET(AVG|INSTANT): ", angle_offsets, " deg"); |
||||||
|
|
||||||
|
wrefresh(w[Win::CarState]); |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::displayHelp() { |
||||||
|
for (int i = 0; i < std::size(keyboard_shortcuts); ++i) { |
||||||
|
wmove(w[Win::Help], i * 2, 0); |
||||||
|
for (auto &[key, desc] : keyboard_shortcuts[i]) { |
||||||
|
wattron(w[Win::Help], A_REVERSE); |
||||||
|
waddstr(w[Win::Help], (' ' + key + ' ').c_str()); |
||||||
|
wattroff(w[Win::Help], A_REVERSE); |
||||||
|
waddstr(w[Win::Help], (' ' + desc + ' ').c_str()); |
||||||
|
} |
||||||
|
} |
||||||
|
wrefresh(w[Win::Help]); |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::displayTimelineDesc() { |
||||||
|
std::tuple<Color, const char *, bool> indicators[]{ |
||||||
|
{Color::Engaged, " Engaged ", false}, |
||||||
|
{Color::Disengaged, " Disengaged ", false}, |
||||||
|
{Color::Green, " Info ", true}, |
||||||
|
{Color::Yellow, " Warning ", true}, |
||||||
|
{Color::Red, " Critical ", true}, |
||||||
|
}; |
||||||
|
for (auto [color, name, bold] : indicators) { |
||||||
|
add_str(w[Win::TimelineDesc], "__", color, bold); |
||||||
|
add_str(w[Win::TimelineDesc], name); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::logMessage(ReplyMsgType type, const QString &msg) { |
||||||
|
if (auto win = w[Win::Log]) { |
||||||
|
Color color = Color::Default; |
||||||
|
if (type == ReplyMsgType::Debug) { |
||||||
|
color = Color::Debug; |
||||||
|
} else if (type == ReplyMsgType::Warning) { |
||||||
|
color = Color::Yellow; |
||||||
|
} else if (type == ReplyMsgType::Critical) { |
||||||
|
color = Color::Red; |
||||||
|
} |
||||||
|
add_str(win, qPrintable(msg + "\n"), color); |
||||||
|
wrefresh(win); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::updateProgressBar(uint64_t cur, uint64_t total, bool success) { |
||||||
|
werase(w[Win::DownloadBar]); |
||||||
|
if (success && cur < total) { |
||||||
|
const int width = 35; |
||||||
|
const float progress = cur / (double)total; |
||||||
|
const int pos = width * progress; |
||||||
|
wprintw(w[Win::DownloadBar], "Downloading [%s>%s] %d%% %s", std::string(pos, '=').c_str(), |
||||||
|
std::string(width - pos, ' ').c_str(), int(progress * 100.0), formattedDataSize(total).c_str()); |
||||||
|
} |
||||||
|
wrefresh(w[Win::DownloadBar]); |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::updateSummary() { |
||||||
|
const auto &route = replay->route(); |
||||||
|
mvwprintw(w[Win::Stats], 0, 0, "Route: %s, %d segments", qPrintable(route->name()), route->segments().size()); |
||||||
|
mvwprintw(w[Win::Stats], 1, 0, "Car Fingerprint: %s", replay->carFingerprint().c_str()); |
||||||
|
wrefresh(w[Win::Stats]); |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::updateTimeline() { |
||||||
|
auto win = w[Win::Timeline]; |
||||||
|
int width = getmaxx(win); |
||||||
|
werase(win); |
||||||
|
|
||||||
|
wattron(win, COLOR_PAIR(Color::Disengaged)); |
||||||
|
mvwhline(win, 1, 0, ' ', width); |
||||||
|
mvwhline(win, 2, 0, ' ', width); |
||||||
|
wattroff(win, COLOR_PAIR(Color::Disengaged)); |
||||||
|
|
||||||
|
const int total_sec = replay->totalSeconds(); |
||||||
|
for (auto [begin, end, type] : replay->getTimeline()) { |
||||||
|
int start_pos = ((double)begin / total_sec) * width; |
||||||
|
int end_pos = ((double)end / total_sec) * width; |
||||||
|
if (type == TimelineType::Engaged) { |
||||||
|
mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); |
||||||
|
mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); |
||||||
|
} else { |
||||||
|
auto color_id = Color::Green; |
||||||
|
if (type != TimelineType::AlertInfo) { |
||||||
|
color_id = type == TimelineType::AlertWarning ? Color::Yellow : Color::Red; |
||||||
|
} |
||||||
|
mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, color_id, NULL); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
int cur_pos = ((double)replay->currentSeconds() / total_sec) * width; |
||||||
|
wattron(win, COLOR_PAIR(Color::BrightWhite)); |
||||||
|
mvwaddch(win, 0, cur_pos, ACS_VLINE); |
||||||
|
mvwaddch(win, 3, cur_pos, ACS_VLINE); |
||||||
|
wattroff(win, COLOR_PAIR(Color::BrightWhite)); |
||||||
|
wrefresh(win); |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::readyRead() { |
||||||
|
int c; |
||||||
|
while ((c = getch()) != ERR) { |
||||||
|
handleKey(c); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::pauseReplay(bool pause) { |
||||||
|
replay->pause(pause); |
||||||
|
status = pause ? Status::Paused : Status::Waiting; |
||||||
|
} |
||||||
|
|
||||||
|
void ConsoleUI::handleKey(char c) { |
||||||
|
if (c == '\n') { |
||||||
|
// pause the replay and blocking getchar()
|
||||||
|
pauseReplay(true); |
||||||
|
updateStatus(); |
||||||
|
getch_timer.stop(); |
||||||
|
curs_set(true); |
||||||
|
nodelay(stdscr, false); |
||||||
|
|
||||||
|
// Wait for user input
|
||||||
|
rWarning("Waiting for input..."); |
||||||
|
int y = getmaxy(stdscr) - 9; |
||||||
|
move(y, BORDER_SIZE); |
||||||
|
add_str(stdscr, "Enter seek request: ", Color::BrightWhite, true); |
||||||
|
refresh(); |
||||||
|
|
||||||
|
// Seek to choice
|
||||||
|
echo(); |
||||||
|
int choice = 0; |
||||||
|
scanw((char *)"%d", &choice); |
||||||
|
noecho(); |
||||||
|
pauseReplay(false); |
||||||
|
replay->seekTo(choice, false); |
||||||
|
|
||||||
|
// Clean up and turn off the blocking mode
|
||||||
|
move(y, 0); |
||||||
|
clrtoeol(); |
||||||
|
nodelay(stdscr, true); |
||||||
|
curs_set(false); |
||||||
|
refresh(); |
||||||
|
getch_timer.start(1000, this); |
||||||
|
|
||||||
|
} else if (c == 'x') { |
||||||
|
if (replay->hasFlag(REPLAY_FLAG_FULL_SPEED)) { |
||||||
|
replay->removeFlag(REPLAY_FLAG_FULL_SPEED); |
||||||
|
rWarning("replay at normal speed"); |
||||||
|
} else { |
||||||
|
replay->addFlag(REPLAY_FLAG_FULL_SPEED); |
||||||
|
rWarning("replay at full speed"); |
||||||
|
} |
||||||
|
} else if (c == 'e') { |
||||||
|
replay->seekToFlag(FindFlag::nextEngagement); |
||||||
|
} else if (c == 'd') { |
||||||
|
replay->seekToFlag(FindFlag::nextDisEngagement); |
||||||
|
} else if (c == 'm') { |
||||||
|
replay->seekTo(+60, true); |
||||||
|
} else if (c == 'M') { |
||||||
|
replay->seekTo(-60, true); |
||||||
|
} else if (c == 's') { |
||||||
|
replay->seekTo(+10, true); |
||||||
|
} else if (c == 'S') { |
||||||
|
replay->seekTo(-10, true); |
||||||
|
} else if (c == ' ') { |
||||||
|
pauseReplay(!replay->isPaused()); |
||||||
|
} else if (c == 'q' || c == 'Q') { |
||||||
|
replay->stop(); |
||||||
|
qApp->exit(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <array> |
||||||
|
#include <QBasicTimer> |
||||||
|
#include <QObject> |
||||||
|
#include <QSocketNotifier> |
||||||
|
#include <QTimer> |
||||||
|
#include <QTimerEvent> |
||||||
|
|
||||||
|
#include "selfdrive/ui/replay/replay.h" |
||||||
|
#include <ncurses.h> |
||||||
|
|
||||||
|
class ConsoleUI : public QObject { |
||||||
|
Q_OBJECT |
||||||
|
|
||||||
|
public: |
||||||
|
ConsoleUI(Replay *replay, QObject *parent = 0); |
||||||
|
~ConsoleUI(); |
||||||
|
|
||||||
|
private: |
||||||
|
void initWindows(); |
||||||
|
void handleKey(char c); |
||||||
|
void displayHelp(); |
||||||
|
void displayTimelineDesc(); |
||||||
|
void updateTimeline(); |
||||||
|
void updateSummary(); |
||||||
|
void updateStatus(); |
||||||
|
void pauseReplay(bool pause); |
||||||
|
|
||||||
|
enum Status { Waiting, Playing, Paused }; |
||||||
|
enum Win { Title, Stats, Log, LogBorder, DownloadBar, Timeline, TimelineDesc, Help, CarState, Max}; |
||||||
|
std::array<WINDOW*, Win::Max> w{}; |
||||||
|
SubMaster sm; |
||||||
|
Replay *replay; |
||||||
|
QBasicTimer getch_timer; |
||||||
|
QTimer sm_timer; |
||||||
|
QSocketNotifier notifier{0, QSocketNotifier::Read, this}; |
||||||
|
int max_width, max_height; |
||||||
|
Status status = Status::Waiting; |
||||||
|
|
||||||
|
signals: |
||||||
|
void updateProgressBarSignal(uint64_t cur, uint64_t total, bool success); |
||||||
|
void logMessageSignal(ReplyMsgType type, const QString &msg); |
||||||
|
|
||||||
|
private slots: |
||||||
|
void readyRead(); |
||||||
|
void timerEvent(QTimerEvent *ev); |
||||||
|
void updateProgressBar(uint64_t cur, uint64_t total, bool success); |
||||||
|
void logMessage(ReplyMsgType type, const QString &msg); |
||||||
|
}; |
@ -1,14 +1,34 @@ |
|||||||
#pragma once |
#pragma once |
||||||
|
|
||||||
#include <atomic> |
#include <atomic> |
||||||
|
#include <functional> |
||||||
#include <string> |
#include <string> |
||||||
|
|
||||||
|
enum class ReplyMsgType { |
||||||
|
Info, |
||||||
|
Debug, |
||||||
|
Warning, |
||||||
|
Critical |
||||||
|
}; |
||||||
|
|
||||||
|
typedef std::function<void(ReplyMsgType type, const std::string msg)> ReplayMessageHandler; |
||||||
|
void installMessageHandler(ReplayMessageHandler); |
||||||
|
void logMessage(ReplyMsgType type, const char* fmt, ...); |
||||||
|
|
||||||
|
#define rInfo(fmt, ...) ::logMessage(ReplyMsgType::Info, fmt, ## __VA_ARGS__) |
||||||
|
#define rDebug(fmt, ...) ::logMessage(ReplyMsgType::Debug, fmt, ## __VA_ARGS__) |
||||||
|
#define rWarning(fmt, ...) ::logMessage(ReplyMsgType::Warning, fmt, ## __VA_ARGS__) |
||||||
|
#define rError(fmt, ...) ::logMessage(ReplyMsgType::Critical , fmt, ## __VA_ARGS__) |
||||||
|
|
||||||
std::string sha256(const std::string &str); |
std::string sha256(const std::string &str); |
||||||
void precise_nano_sleep(long sleep_ns); |
void precise_nano_sleep(long sleep_ns); |
||||||
std::string decompressBZ2(const std::string &in, std::atomic<bool> *abort = nullptr); |
std::string decompressBZ2(const std::string &in, std::atomic<bool> *abort = nullptr); |
||||||
std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic<bool> *abort = nullptr); |
std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic<bool> *abort = nullptr); |
||||||
void enableHttpLogging(bool enable); |
|
||||||
std::string getUrlWithoutQuery(const std::string &url); |
std::string getUrlWithoutQuery(const std::string &url); |
||||||
size_t getRemoteFileSize(const std::string &url, std::atomic<bool> *abort = nullptr); |
size_t getRemoteFileSize(const std::string &url, std::atomic<bool> *abort = nullptr); |
||||||
std::string httpGet(const std::string &url, size_t chunk_size = 0, std::atomic<bool> *abort = nullptr); |
std::string httpGet(const std::string &url, size_t chunk_size = 0, std::atomic<bool> *abort = nullptr); |
||||||
|
|
||||||
|
typedef std::function<void(uint64_t cur, uint64_t total, bool success)> DownloadProgressHandler; |
||||||
|
void installDownloadProgressHandler(DownloadProgressHandler); |
||||||
bool httpDownload(const std::string &url, const std::string &file, size_t chunk_size = 0, std::atomic<bool> *abort = nullptr); |
bool httpDownload(const std::string &url, const std::string &file, size_t chunk_size = 0, std::atomic<bool> *abort = nullptr); |
||||||
|
std::string formattedDataSize(size_t size); |
||||||
|
Loading…
Reference in new issue