From 2444970eee67d6b742acf267119c41bdf06b2956 Mon Sep 17 00:00:00 2001 From: thftgr Date: Fri, 26 Jun 2026 21:11:35 +0900 Subject: [PATCH 01/61] restore c3 --- system/camerad/SConscript | 2 +- system/camerad/cameras/spectra.cc | 3 +- system/camerad/sensors/ar0231.cc | 136 +++++++++++++++++++ system/camerad/sensors/ar0231_registers.h | 121 +++++++++++++++++ system/camerad/sensors/sensor.h | 9 ++ system/hardware/base.h | 1 + system/hardware/pc/hardware.h | 8 ++ system/hardware/tici/amplifier.py | 92 +++++++++---- system/hardware/tici/hardware.h | 7 + system/hardware/tici/hardware.py | 4 +- system/hardware/tici/tests/test_amplifier.py | 5 +- 11 files changed, 354 insertions(+), 34 deletions(-) create mode 100644 system/camerad/sensors/ar0231.cc create mode 100644 system/camerad/sensors/ar0231_registers.h diff --git a/system/camerad/SConscript b/system/camerad/SConscript index c28330b32c4..7f97590fc6f 100644 --- a/system/camerad/SConscript +++ b/system/camerad/SConscript @@ -4,7 +4,7 @@ libs = [common, messaging, visionipc] if arch != "Darwin": camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/spectra.cc', - 'cameras/cdm.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc']) + 'cameras/cdm.cc', 'sensors/ar0231.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc']) env.Program('camerad', ['main.cc', camera_obj], LIBS=libs) if GetOption("extras") and arch == "x86_64": diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 0b5f5327f58..84de19aa93f 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -1046,7 +1046,8 @@ bool SpectraCamera::openSensor() { }; // Figure out which sensor we have - if (!init_sensor_lambda(new OS04C10) && + if (!init_sensor_lambda(new AR0231) && + !init_sensor_lambda(new OS04C10) && !init_sensor_lambda(new OX03C10)) { LOGE("** sensor %d FAILED bringup, disabling", cc.camera_num); enabled = false; diff --git a/system/camerad/sensors/ar0231.cc b/system/camerad/sensors/ar0231.cc new file mode 100644 index 00000000000..e4ae29f079c --- /dev/null +++ b/system/camerad/sensors/ar0231.cc @@ -0,0 +1,136 @@ +#include +#include + +#include "system/camerad/sensors/sensor.h" + +namespace { + +const size_t AR0231_REGISTERS_HEIGHT = 2; +// TODO: this extra height is universal and doesn't apply per camera +const size_t AR0231_STATS_HEIGHT = 2 + 8; + +const float sensor_analog_gains_AR0231[] = { + 1.0 / 8.0, 2.0 / 8.0, 2.0 / 7.0, 3.0 / 7.0, // 0, 1, 2, 3 + 3.0 / 6.0, 4.0 / 6.0, 4.0 / 5.0, 5.0 / 5.0, // 4, 5, 6, 7 + 5.0 / 4.0, 6.0 / 4.0, 6.0 / 3.0, 7.0 / 3.0, // 8, 9, 10, 11 + 7.0 / 2.0, 8.0 / 2.0, 8.0 / 1.0}; // 12, 13, 14, 15 = bypass + +} // namespace + +AR0231::AR0231() { + image_sensor = cereal::FrameData::ImageSensor::AR0231; + bayer_pattern = CAM_ISP_PATTERN_BAYER_GRGRGR; + pixel_size_mm = 0.003; + data_word = true; + frame_width = 1928; + frame_height = 1208; + frame_stride = (frame_width * 12 / 8) + 4; + extra_height = AR0231_REGISTERS_HEIGHT + AR0231_STATS_HEIGHT; + + registers_offset = 0; + frame_offset = AR0231_REGISTERS_HEIGHT; + stats_offset = AR0231_REGISTERS_HEIGHT + frame_height; + + start_reg_array.assign(std::begin(start_reg_array_ar0231), std::end(start_reg_array_ar0231)); + init_reg_array.assign(std::begin(init_array_ar0231), std::end(init_array_ar0231)); + probe_reg_addr = 0x3000; + probe_expected_data = 0x354; + bits_per_pixel = 12; + mipi_format = CAM_FORMAT_MIPI_RAW_12; + frame_data_type = 0x12; // Changing stats to 0x2C doesn't work, so change pixels to 0x12 instead + mclk_frequency = 19200000; //Hz + + readout_time_ns = 22850000; + + dc_gain_factor = 2.5; + dc_gain_min_weight = 0; + dc_gain_max_weight = 1; + dc_gain_on_grey = 0.2; + dc_gain_off_grey = 0.3; + exposure_time_min = 2; // with HDR, fastest ss + exposure_time_max = 0x0855; // with HDR, slowest ss, 40ms + analog_gain_min_idx = 0x1; // 0.25x + analog_gain_rec_idx = 0x6; // 0.8x + analog_gain_max_idx = 0xD; // 4.0x + analog_gain_cost_delta = 0; + analog_gain_cost_low = 0.1; + analog_gain_cost_high = 5.0; + for (int i = 0; i <= analog_gain_max_idx; i++) { + sensor_analog_gains[i] = sensor_analog_gains_AR0231[i]; + } + min_ev = exposure_time_min * sensor_analog_gains[analog_gain_min_idx]; + max_ev = exposure_time_max * dc_gain_factor * sensor_analog_gains[analog_gain_max_idx]; + target_grey_factor = 1.0; + + black_level = 168; + color_correct_matrix = { + 0x000000af, 0x00000ff9, 0x00000fd8, + 0x00000fbc, 0x000000bb, 0x00000009, + 0x00000fb6, 0x00000fe0, 0x000000ea, + }; + for (int i = 0; i < 65; i++) { + float fx = i / 64.0; + const float gamma_k = 0.75; + const float gamma_b = 0.125; + const float mp = 0.01; // ideally midpoint should be adaptive + const float rk = 9 - 100*mp; + // poly approximation for s curve + fx = (fx > mp) ? + ((rk * (fx-mp) * (1-(gamma_k*mp+gamma_b)) * (1+1/(rk*(1-mp))) / (1+rk*(fx-mp))) + gamma_k*mp + gamma_b) : + ((rk * (fx-mp) * (gamma_k*mp+gamma_b) * (1+1/(rk*mp)) / (1-rk*(fx-mp))) + gamma_k*mp + gamma_b); + gamma_lut_rgb.push_back((uint32_t)(fx*1023.0 + 0.5)); + } + prepare_gamma_lut(); + linearization_lut = { + 0x02000000, 0x02000000, 0x02000000, 0x02000000, + 0x020007ff, 0x020007ff, 0x020007ff, 0x020007ff, + 0x02000bff, 0x02000bff, 0x02000bff, 0x02000bff, + 0x020017ff, 0x020017ff, 0x020017ff, 0x020017ff, + 0x02001bff, 0x02001bff, 0x02001bff, 0x02001bff, + 0x020023ff, 0x020023ff, 0x020023ff, 0x020023ff, + 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff, + 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff, + 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff, + }; + linearization_pts = {0x07ff0bff, 0x17ff1bff, 0x23ff3fff, 0x3fff3fff}; + vignetting_lut = { + 0x00eaa755, 0x00cf2679, 0x00bc05e0, 0x00acc566, 0x00a1450a, 0x009984cc, 0x0095a4ad, 0x009584ac, 0x009944ca, 0x00a0c506, 0x00ac0560, 0x00bb25d9, 0x00ce2671, 0x00e90748, 0x01112889, 0x014a2a51, 0x01984cc2, + 0x00db06d8, 0x00c30618, 0x00afe57f, 0x00a0a505, 0x009524a9, 0x008d646b, 0x0089844c, 0x0089644b, 0x008d2469, 0x0094a4a5, 0x009fe4ff, 0x00af0578, 0x00c20610, 0x00d986cc, 0x00fda7ed, 0x01320990, 0x017aebd7, + 0x00d1868c, 0x00baa5d5, 0x00a7853c, 0x009844c2, 0x008cc466, 0x0085a42d, 0x0083641b, 0x0083641b, 0x0085842c, 0x008c4462, 0x0097a4bd, 0x00a6c536, 0x00b9a5cd, 0x00d06683, 0x00f1678b, 0x01226913, 0x0167ab3d, + 0x00cd0668, 0x00b625b1, 0x00a30518, 0x0093c49e, 0x00884442, 0x00830418, 0x0080e407, 0x0080c406, 0x0082e417, 0x0087c43e, 0x00932499, 0x00a22511, 0x00b525a9, 0x00cbe65f, 0x00eb0758, 0x011a68d3, 0x015daaed, + 0x00cc4662, 0x00b565ab, 0x00a24512, 0x00930498, 0x0087843c, 0x0082a415, 0x00806403, 0x00806403, 0x00828414, 0x00870438, 0x00926493, 0x00a1850c, 0x00b465a3, 0x00cb2659, 0x00ea2751, 0x011928c9, 0x015c2ae1, + 0x00cf667b, 0x00b885c4, 0x00a5652b, 0x009624b1, 0x008aa455, 0x00846423, 0x00822411, 0x00822411, 0x00844422, 0x008a2451, 0x009564ab, 0x00a48524, 0x00b785bc, 0x00ce4672, 0x00ee6773, 0x011e88f4, 0x0162eb17, + 0x00d6c6b6, 0x00bf65fb, 0x00ac4562, 0x009d04e8, 0x0091848c, 0x0089c44e, 0x00862431, 0x00860430, 0x0089844c, 0x00910488, 0x009c64e3, 0x00ab655b, 0x00be65f3, 0x00d566ab, 0x00f847c2, 0x012b2959, 0x01726b93, + 0x00e3e71f, 0x00ca0650, 0x00b705b8, 0x00a7a53d, 0x009c24e1, 0x009484a4, 0x00908484, 0x00908484, 0x009424a1, 0x009bc4de, 0x00a70538, 0x00b625b1, 0x00c90648, 0x00e26713, 0x0108e847, 0x013fe9ff, 0x018bcc5e, + 0x00f807c0, 0x00d966cb, 0x00c5862c, 0x00b625b1, 0x00aaa555, 0x00a30518, 0x009f04f8, 0x009f04f8, 0x00a2a515, 0x00aa2551, 0x00b585ac, 0x00c4a625, 0x00d846c2, 0x00f647b2, 0x0121a90d, 0x015e4af2, 0x01b8cdc6, + 0x011548aa, 0x00f1678b, 0x00d886c4, 0x00c86643, 0x00bce5e7, 0x00b545aa, 0x00b1658b, 0x00b1458a, 0x00b505a8, 0x00bc85e4, 0x00c7c63e, 0x00d786bc, 0x00efe77f, 0x0113489a, 0x0144ea27, 0x01888c44, 0x01fdcfee, + 0x013e49f2, 0x0113e89f, 0x00f5a7ad, 0x00e0c706, 0x00d30698, 0x00cb665b, 0x00c7663b, 0x00c7663b, 0x00cb0658, 0x00d2a695, 0x00dfe6ff, 0x00f467a3, 0x01122891, 0x013be9df, 0x01750ba8, 0x01cfae7d, 0x025912c8, + 0x01766bb3, 0x01446a23, 0x011fc8fe, 0x0105e82f, 0x00f467a3, 0x00e9874c, 0x00e46723, 0x00e44722, 0x00e92749, 0x00f3a79d, 0x0104c826, 0x011e48f2, 0x01424a12, 0x01738b9c, 0x01bf6dfb, 0x023611b0, 0x02ced676, + 0x01cf8e7c, 0x01866c33, 0x015aaad5, 0x013ae9d7, 0x01250928, 0x011768bb, 0x0110a885, 0x01108884, 0x0116e8b7, 0x01242921, 0x0139a9cd, 0x0158eac7, 0x01840c20, 0x01cb0e58, 0x0233719b, 0x02b9d5ce, 0x03645b22, + }; +} + +std::vector AR0231::getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const { + uint16_t analog_gain_reg = 0xFF00 | (new_exp_g << 4) | new_exp_g; + return { + {0x3366, analog_gain_reg}, + {0x3362, (uint16_t)(dc_gain_enabled ? 0x1 : 0x0)}, + {0x3012, (uint16_t)exposure_time}, + }; +} + +int AR0231::getSlaveAddress(int port) const { + assert(port >= 0 && port <= 2); + return (int[]){0x20, 0x30, 0x20}[port]; +} + +float AR0231::getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const { + // Cost of ev diff + float score = std::abs(desired_ev - (exp_t * exp_gain)) * 10; + // Cost of absolute gain + float m = exp_g_idx > analog_gain_rec_idx ? analog_gain_cost_high : analog_gain_cost_low; + score += std::abs(exp_g_idx - (int)analog_gain_rec_idx) * m; + // Cost of changing gain + score += std::abs(exp_g_idx - gain_idx) * (score + 1.0) / 10.0; + return score; +} diff --git a/system/camerad/sensors/ar0231_registers.h b/system/camerad/sensors/ar0231_registers.h new file mode 100644 index 00000000000..e0872a673a0 --- /dev/null +++ b/system/camerad/sensors/ar0231_registers.h @@ -0,0 +1,121 @@ +#pragma once + +const struct i2c_random_wr_payload start_reg_array_ar0231[] = {{0x301A, 0x91C}}; +const struct i2c_random_wr_payload stop_reg_array_ar0231[] = {{0x301A, 0x918}}; + +const struct i2c_random_wr_payload init_array_ar0231[] = { + {0x301A, 0x0018}, // RESET_REGISTER + + // **NOTE**: if this is changed, readout_time_ns must be updated in the Sensor config + + // CLOCK Settings + // input clock is 19.2 / 2 * 0x37 = 528 MHz + // pixclk is 528 / 6 = 88 MHz + // full roll time is 1000/(PIXCLK/(LINE_LENGTH_PCK*FRAME_LENGTH_LINES)) = 39.99 ms + // img roll time is 1000/(PIXCLK/(LINE_LENGTH_PCK*Y_OUTPUT_CONTROL)) = 22.85 ms + {0x302A, 0x0006}, // VT_PIX_CLK_DIV + {0x302C, 0x0001}, // VT_SYS_CLK_DIV + {0x302E, 0x0002}, // PRE_PLL_CLK_DIV + {0x3030, 0x0037}, // PLL_MULTIPLIER + {0x3036, 0x000C}, // OP_PIX_CLK_DIV + {0x3038, 0x0001}, // OP_SYS_CLK_DIV + + // FORMAT + {0x3040, 0xC000}, // READ_MODE + {0x3004, 0x0000}, // X_ADDR_START_ + {0x3008, 0x0787}, // X_ADDR_END_ + {0x3002, 0x0000}, // Y_ADDR_START_ + {0x3006, 0x04B7}, // Y_ADDR_END_ + {0x3032, 0x0000}, // SCALING_MODE + {0x30A2, 0x0001}, // X_ODD_INC_ + {0x30A6, 0x0001}, // Y_ODD_INC_ + {0x3402, 0x0788}, // X_OUTPUT_CONTROL + {0x3404, 0x04B8}, // Y_OUTPUT_CONTROL + {0x3064, 0x1982}, // SMIA_TEST + {0x30BA, 0x11F2}, // DIGITAL_CTRL + + // Enable external trigger and disable GPIO outputs + {0x30CE, 0x0120}, // SLAVE_SH_SYNC_MODE | FRAME_START_MODE + {0x340A, 0xE0}, // GPIO3_INPUT_DISABLE | GPIO2_INPUT_DISABLE | GPIO1_INPUT_DISABLE + {0x340C, 0x802}, // GPIO_HIDRV_EN | GPIO0_ISEL=2 + + // Readout timing + {0x300C, 0x0672}, // LINE_LENGTH_PCK (valid for 3-exposure HDR) + {0x300A, 0x0855}, // FRAME_LENGTH_LINES + {0x3042, 0x0000}, // EXTRA_DELAY + + // Readout Settings + {0x31AE, 0x0204}, // SERIAL_FORMAT, 4-lane MIPI + {0x31AC, 0x0C0C}, // DATA_FORMAT_BITS, 12 -> 12 + {0x3342, 0x1212}, // MIPI_F1_PDT_EDT + {0x3346, 0x1212}, // MIPI_F2_PDT_EDT + {0x334A, 0x1212}, // MIPI_F3_PDT_EDT + {0x334E, 0x1212}, // MIPI_F4_PDT_EDT + {0x3344, 0x0011}, // MIPI_F1_VDT_VC + {0x3348, 0x0111}, // MIPI_F2_VDT_VC + {0x334C, 0x0211}, // MIPI_F3_VDT_VC + {0x3350, 0x0311}, // MIPI_F4_VDT_VC + {0x31B0, 0x0053}, // FRAME_PREAMBLE + {0x31B2, 0x003B}, // LINE_PREAMBLE + {0x301A, 0x001C}, // RESET_REGISTER + + // Noise Corrections + {0x3092, 0x0C24}, // ROW_NOISE_CONTROL + {0x337A, 0x0C80}, // DBLC_SCALE0 + {0x3370, 0x03B1}, // DBLC + {0x3044, 0x0400}, // DARK_CONTROL + + // Enable temperature sensor + {0x30B4, 0x0007}, // TEMPSENS0_CTRL_REG + {0x30B8, 0x0007}, // TEMPSENS1_CTRL_REG + + // Enable dead pixel correction using + // the 1D line correction scheme + {0x31E0, 0x0003}, + + // HDR Settings + {0x3082, 0x0004}, // OPERATION_MODE_CTRL + {0x3238, 0x0444}, // EXPOSURE_RATIO + + {0x1008, 0x0361}, // FINE_INTEGRATION_TIME_MIN + {0x100C, 0x0589}, // FINE_INTEGRATION_TIME2_MIN + {0x100E, 0x07B1}, // FINE_INTEGRATION_TIME3_MIN + {0x1010, 0x0139}, // FINE_INTEGRATION_TIME4_MIN + + // TODO: do these have to be lower than LINE_LENGTH_PCK? + {0x3014, 0x08CB}, // FINE_INTEGRATION_TIME_ + {0x321E, 0x0894}, // FINE_INTEGRATION_TIME2 + + {0x31D0, 0x0000}, // COMPANDING, no good in 10 bit? + {0x33DA, 0x0000}, // COMPANDING + {0x318E, 0x0200}, // PRE_HDR_GAIN_EN + + // DLO Settings + {0x3100, 0x4000}, // DLO_CONTROL0 + {0x3280, 0x0CCC}, // T1 G1 + {0x3282, 0x0CCC}, // T1 R + {0x3284, 0x0CCC}, // T1 B + {0x3286, 0x0CCC}, // T1 G2 + {0x3288, 0x0FA0}, // T2 G1 + {0x328A, 0x0FA0}, // T2 R + {0x328C, 0x0FA0}, // T2 B + {0x328E, 0x0FA0}, // T2 G2 + + // Initial Gains + {0x3022, 0x0001}, // GROUPED_PARAMETER_HOLD_ + {0x3366, 0xFF77}, // ANALOG_GAIN (1x) + + {0x3060, 0x3333}, // ANALOG_COLOR_GAIN + + {0x3362, 0x0000}, // DC GAIN + + {0x305A, 0x00F8}, // red gain + {0x3058, 0x0122}, // blue gain + {0x3056, 0x009A}, // g1 gain + {0x305C, 0x009A}, // g2 gain + + {0x3022, 0x0000}, // GROUPED_PARAMETER_HOLD_ + + // Initial Integration Time + {0x3012, 0x0005}, +}; diff --git a/system/camerad/sensors/sensor.h b/system/camerad/sensors/sensor.h index 1a647d1528a..e10d8a493e1 100644 --- a/system/camerad/sensors/sensor.h +++ b/system/camerad/sensors/sensor.h @@ -10,6 +10,7 @@ #include "media/cam_sensor.h" #include "cereal/gen/cpp/log.capnp.h" +#include "system/camerad/sensors/ar0231_registers.h" #include "system/camerad/sensors/ox03c10_registers.h" #include "system/camerad/sensors/os04c10_registers.h" @@ -87,6 +88,14 @@ class SensorInfo { }; }; +class AR0231 : public SensorInfo { +public: + AR0231(); + std::vector getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const override; + float getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const override; + int getSlaveAddress(int port) const override; +}; + class OX03C10 : public SensorInfo { public: OX03C10(); diff --git a/system/hardware/base.h b/system/hardware/base.h index 3eded659acc..5088f5dfb05 100644 --- a/system/hardware/base.h +++ b/system/hardware/base.h @@ -22,6 +22,7 @@ class HardwareNone { } static void set_ir_power(int percentage) {} + static void config_cpu_rendering(bool) {} static bool PC() { return false; } static bool TICI() { return false; } diff --git a/system/hardware/pc/hardware.h b/system/hardware/pc/hardware.h index 71f58b188be..4a0b200a50d 100644 --- a/system/hardware/pc/hardware.h +++ b/system/hardware/pc/hardware.h @@ -11,4 +11,12 @@ class HardwarePC : public HardwareNone { static bool PC() { return true; } static bool TICI() { return util::getenv("TICI", 0) == 1; } static bool AGNOS() { return util::getenv("TICI", 0) == 1; } + + static void config_cpu_rendering(bool offscreen) { + if (offscreen) { + setenv("QT_QPA_PLATFORM", "offscreen", 1); + } + setenv("__GLX_VENDOR_LIBRARY_NAME", "mesa", 1); + setenv("LP_NUM_THREADS", "0", 1); + } }; diff --git a/system/hardware/tici/amplifier.py b/system/hardware/tici/amplifier.py index 09436e6ff46..ae5508c36ea 100755 --- a/system/hardware/tici/amplifier.py +++ b/system/hardware/tici/amplifier.py @@ -7,8 +7,23 @@ # https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf AmpConfig = namedtuple('AmpConfig', ['name', 'value', 'register', 'offset', 'mask']) - -CONFIG = [ +EQParams = namedtuple('EQParams', ['K', 'k1', 'k2', 'c1', 'c2']) + +def configs_from_eq_params(base, eq_params): + return [ + AmpConfig("K (high)", (eq_params.K >> 8), base, 0, 0xFF), + AmpConfig("K (low)", (eq_params.K & 0xFF), base + 1, 0, 0xFF), + AmpConfig("k1 (high)", (eq_params.k1 >> 8), base + 2, 0, 0xFF), + AmpConfig("k1 (low)", (eq_params.k1 & 0xFF), base + 3, 0, 0xFF), + AmpConfig("k2 (high)", (eq_params.k2 >> 8), base + 4, 0, 0xFF), + AmpConfig("k2 (low)", (eq_params.k2 & 0xFF), base + 5, 0, 0xFF), + AmpConfig("c1 (high)", (eq_params.c1 >> 8), base + 6, 0, 0xFF), + AmpConfig("c1 (low)", (eq_params.c1 & 0xFF), base + 7, 0, 0xFF), + AmpConfig("c2 (high)", (eq_params.c2 >> 8), base + 8, 0, 0xFF), + AmpConfig("c2 (low)", (eq_params.c2 & 0xFF), base + 9, 0, 0xFF), + ] + +BASE_CONFIG = [ AmpConfig("MCLK prescaler", 0b01, 0x10, 4, 0b00110000), AmpConfig("PM: enable speakers", 0b11, 0x4D, 4, 0b00110000), AmpConfig("PM: enable DACs", 0b11, 0x4D, 0, 0b00000011), @@ -44,31 +59,47 @@ AmpConfig("Enhanced volume smoothing disabled", 0b0, 0x49, 7, 0b10000000), AmpConfig("Volume adjustment smoothing disabled", 0b0, 0x49, 6, 0b01000000), AmpConfig("Zero-crossing detection disabled", 0b0, 0x49, 5, 0b00100000), - - AmpConfig("Left speaker output from left DAC", 0b1, 0x2B, 0, 0b11111111), - AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), - AmpConfig("Left Speaker Mixer Gain", 0b00, 0x2D, 0, 0b00000011), - AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100), - AmpConfig("Left speaker output volume", 0x17, 0x3D, 0, 0b00011111), - AmpConfig("Right speaker output volume", 0x17, 0x3E, 0, 0b00011111), - - AmpConfig("DAI2 EQ enable", 0b0, 0x49, 1, 0b00000010), - AmpConfig("DAI2: DC blocking", 0b0, 0x20, 0, 0b00000001), - AmpConfig("ALC enable", 0b0, 0x43, 7, 0b10000000), - AmpConfig("DAI2 EQ attenuation", 0x2, 0x32, 0, 0b00001111), - AmpConfig("Excursion limiter upper corner freq", 0b001, 0x41, 4, 0b01110000), - AmpConfig("Excursion limiter threshold", 0b100, 0x42, 0, 0b00001111), - AmpConfig("Distortion limit (THDCLP)", 0x0, 0x46, 4, 0b11110000), - AmpConfig("Distortion limiter release time constant", 0b1, 0x46, 0, 0b00000001), - AmpConfig("Left DAC input mixer: DAI1 left", 0b0, 0x22, 7, 0b10000000), - AmpConfig("Left DAC input mixer: DAI1 right", 0b0, 0x22, 6, 0b01000000), - AmpConfig("Left DAC input mixer: DAI2 left", 0b1, 0x22, 5, 0b00100000), - AmpConfig("Left DAC input mixer: DAI2 right", 0b0, 0x22, 4, 0b00010000), - AmpConfig("Right DAC input mixer: DAI2 left", 0b0, 0x22, 1, 0b00000010), - AmpConfig("Right DAC input mixer: DAI2 right", 0b1, 0x22, 0, 0b00000001), - AmpConfig("Volume adjustment smoothing disabled", 0b1, 0x49, 6, 0b01000000), ] +CONFIGS = { + "tici": [ + AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), + AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100), + AmpConfig("Right speaker output volume", 0x1c, 0x3E, 0, 0b00011111), + AmpConfig("DAI2 EQ enable", 0b1, 0x49, 1, 0b00000010), + + *configs_from_eq_params(0x84, EQParams(0x274F, 0xC0FF, 0x3BF9, 0x0B3C, 0x1656)), + *configs_from_eq_params(0x8E, EQParams(0x1009, 0xC6BF, 0x2952, 0x1C97, 0x30DF)), + *configs_from_eq_params(0x98, EQParams(0x0F75, 0xCBE5, 0x0ED2, 0x2528, 0x3E42)), + *configs_from_eq_params(0xA2, EQParams(0x091F, 0x3D4C, 0xCE11, 0x1266, 0x2807)), + *configs_from_eq_params(0xAC, EQParams(0x0A9E, 0x3F20, 0xE573, 0x0A8B, 0x3A3B)), + ], + "tizi": [ + AmpConfig("Left speaker output from left DAC", 0b1, 0x2B, 0, 0b11111111), + AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), + AmpConfig("Left Speaker Mixer Gain", 0b00, 0x2D, 0, 0b00000011), + AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100), + AmpConfig("Left speaker output volume", 0x17, 0x3D, 0, 0b00011111), + AmpConfig("Right speaker output volume", 0x17, 0x3E, 0, 0b00011111), + + AmpConfig("DAI2 EQ enable", 0b0, 0x49, 1, 0b00000010), + AmpConfig("DAI2: DC blocking", 0b0, 0x20, 0, 0b00000001), + AmpConfig("ALC enable", 0b0, 0x43, 7, 0b10000000), + AmpConfig("DAI2 EQ attenuation", 0x2, 0x32, 0, 0b00001111), + AmpConfig("Excursion limiter upper corner freq", 0b001, 0x41, 4, 0b01110000), + AmpConfig("Excursion limiter threshold", 0b100, 0x42, 0, 0b00001111), + AmpConfig("Distortion limit (THDCLP)", 0x0, 0x46, 4, 0b11110000), + AmpConfig("Distortion limiter release time constant", 0b1, 0x46, 0, 0b00000001), + AmpConfig("Left DAC input mixer: DAI1 left", 0b0, 0x22, 7, 0b10000000), + AmpConfig("Left DAC input mixer: DAI1 right", 0b0, 0x22, 6, 0b01000000), + AmpConfig("Left DAC input mixer: DAI2 left", 0b1, 0x22, 5, 0b00100000), + AmpConfig("Left DAC input mixer: DAI2 right", 0b0, 0x22, 4, 0b00010000), + AmpConfig("Right DAC input mixer: DAI2 left", 0b0, 0x22, 1, 0b00000010), + AmpConfig("Right DAC input mixer: DAI2 right", 0b1, 0x22, 0, 0b00000001), + AmpConfig("Volume adjustment smoothing disabled", 0b1, 0x49, 6, 0b01000000), + ], +} + class Amplifier: AMP_I2C_BUS = 0 AMP_ADDRESS = 0x10 @@ -109,15 +140,20 @@ def set_configs(self, configs: list[AmpConfig]) -> bool: def set_global_shutdown(self, amp_disabled: bool) -> bool: return self.set_configs([self._get_shutdown_config(amp_disabled), ]) - def initialize_configuration(self) -> bool: + def initialize_configuration(self, model: str) -> bool: cfgs = [ self._get_shutdown_config(True), - *CONFIG, + *BASE_CONFIG, + *CONFIGS[model], self._get_shutdown_config(False), ] return self.set_configs(cfgs) if __name__ == "__main__": + with open("/sys/firmware/devicetree/base/model") as f: + model = f.read().strip('\x00') + model = model.split('comma ')[-1] + amp = Amplifier() - amp.initialize_configuration() + amp.initialize_configuration(model) diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index a06ce43b356..8e318b8b8e3 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -90,4 +90,11 @@ class HardwareTici : public HardwareNone { return ret; } + + static void config_cpu_rendering(bool offscreen) { + if (offscreen) { + setenv("QT_QPA_PLATFORM", "eglfs", 1); + } + setenv("LP_NUM_THREADS", "0", 1); + } }; diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 10b6d7646cf..26df5111dad 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -299,7 +299,7 @@ def set_power_save(self, powersave_enabled): if self.amplifier is not None: self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled) if not powersave_enabled: - self.amplifier.initialize_configuration() + self.amplifier.initialize_configuration(self.get_device_type()) # *** CPU config *** @@ -337,7 +337,7 @@ def get_gpu_usage_percent(self): def initialize_hardware(self): if self.amplifier is not None: - self.amplifier.initialize_configuration() + self.amplifier.initialize_configuration(self.get_device_type()) # Allow hardwared to write engagement status to kmsg os.system("sudo chmod a+w /dev/kmsg") diff --git a/system/hardware/tici/tests/test_amplifier.py b/system/hardware/tici/tests/test_amplifier.py index 9ce00c3ff26..3f75436db18 100644 --- a/system/hardware/tici/tests/test_amplifier.py +++ b/system/hardware/tici/tests/test_amplifier.py @@ -5,6 +5,7 @@ from panda import Panda from openpilot.system.hardware import TICI, HARDWARE +from openpilot.system.hardware.tici.hardware import Tici from openpilot.system.hardware.tici.amplifier import Amplifier @@ -38,7 +39,7 @@ def _check_for_i2c_errors(self, expected): def test_init(self): amp = Amplifier(debug=True) - r = amp.initialize_configuration() + r = amp.initialize_configuration(Tici().get_device_type()) assert r assert self._check_for_i2c_errors(False) @@ -60,7 +61,7 @@ def test_init_while_siren_play(self): time.sleep(random.randint(0, 5)) amp = Amplifier(debug=True) - r = amp.initialize_configuration() + r = amp.initialize_configuration(Tici().get_device_type()) assert r if self._check_for_i2c_errors(True): From d3d4470159e9eda5a72edbd79b9d93808028c32d Mon Sep 17 00:00:00 2001 From: thftgr Date: Fri, 26 Jun 2026 21:34:22 +0900 Subject: [PATCH 02/61] Update `get_expected_signature` to use `panda` instance for MCU type retrieval --- selfdrive/pandad/pandad.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index df2b4f7ee89..730836a360b 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -6,16 +6,16 @@ import signal import subprocess -from panda import Panda, PandaDFU, PandaProtocolMismatch, McuType, FW_PATH +from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE from openpilot.common.swaglog import cloudlog -def get_expected_signature() -> bytes: +def get_expected_signature(panda: Panda) -> bytes: try: - fn = os.path.join(FW_PATH, McuType.H7.config.app_fn) + fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn) return Panda.get_signature_from_firmware(fn) except Exception: cloudlog.exception("Error computing expected signature") @@ -29,7 +29,7 @@ def flash_panda(panda_serial: str) -> Panda: HARDWARE.recover_internal_panda() raise - fw_signature = get_expected_signature() + fw_signature = get_expected_signature(panda) internal_panda = panda.is_internal() panda_version = "bootstub" if panda.bootstub else panda.get_version() From 93ea7e886f0a1a63b4c5cc6f6fb4aa7cb6745f62 Mon Sep 17 00:00:00 2001 From: thftgr Date: Fri, 26 Jun 2026 22:08:18 +0900 Subject: [PATCH 03/61] Add USB support to `pandad` with fallback to SPI communication --- selfdrive/pandad/SConscript | 4 +- selfdrive/pandad/panda.cc | 18 ++- selfdrive/pandad/panda.h | 2 +- selfdrive/pandad/panda_comms.cc | 224 ++++++++++++++++++++++++++++++++ selfdrive/pandad/panda_comms.h | 37 +++++- 5 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 selfdrive/pandad/panda_comms.cc diff --git a/selfdrive/pandad/SConscript b/selfdrive/pandad/SConscript index fd59db98537..343ca4b79aa 100644 --- a/selfdrive/pandad/SConscript +++ b/selfdrive/pandad/SConscript @@ -1,8 +1,8 @@ Import('env', 'arch', 'common', 'messaging') if arch != "Darwin": - libs = [common, messaging, 'pthread'] - panda = env.Library('panda', ['panda.cc', 'spi.cc']) + libs = [common, messaging, 'pthread', 'usb-1.0'] + panda = env.Library('panda', ['panda.cc', 'panda_comms.cc', 'spi.cc']) env.Program('pandad', ['main.cc', 'pandad.cc', 'panda_safety.cc'], LIBS=[panda] + libs) diff --git a/selfdrive/pandad/panda.cc b/selfdrive/pandad/panda.cc index edc2228c0c7..a21fb4abe80 100644 --- a/selfdrive/pandad/panda.cc +++ b/selfdrive/pandad/panda.cc @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -13,8 +14,13 @@ const bool PANDAD_MAXOUT = getenv("PANDAD_MAXOUT") != nullptr; Panda::Panda(std::string serial) { - handle = std::make_unique(serial); - LOGW("connected to %s over SPI", serial.c_str()); + try { + handle = std::make_unique(serial); + LOGW("connected to %s over USB", serial.c_str()); + } catch (const std::exception &e) { + handle = std::make_unique(serial); + LOGW("connected to %s over SPI", serial.c_str()); + } hw_type = get_hw_type(); can_reset_communications(); @@ -33,7 +39,13 @@ std::string Panda::hw_serial() { } std::vector Panda::list() { - return PandaSpiHandle::list(); + std::vector serials = PandaUsbHandle::list(); + for (const auto &s : PandaSpiHandle::list()) { + if (std::find(serials.begin(), serials.end(), s) == serials.end()) { + serials.push_back(s); + } + } + return serials; } void Panda::set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param) { diff --git a/selfdrive/pandad/panda.h b/selfdrive/pandad/panda.h index 1a066dc5fb7..979c2a5da17 100644 --- a/selfdrive/pandad/panda.h +++ b/selfdrive/pandad/panda.h @@ -45,7 +45,7 @@ struct can_frame { class Panda { private: - std::unique_ptr handle; + std::unique_ptr handle; public: Panda(std::string serial); diff --git a/selfdrive/pandad/panda_comms.cc b/selfdrive/pandad/panda_comms.cc new file mode 100644 index 00000000000..ba5da331d3f --- /dev/null +++ b/selfdrive/pandad/panda_comms.cc @@ -0,0 +1,224 @@ +#include "selfdrive/pandad/panda_comms.h" + +#include + +#include +#include + +#include "common/swaglog.h" + +static libusb_context *init_usb_ctx() { + libusb_context *context = nullptr; + int err = libusb_init(&context); + if (err != 0) { + LOGE("libusb initialization error"); + return nullptr; + } + +#if LIBUSB_API_VERSION >= 0x01000106 + libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO); +#else + libusb_set_debug(context, 3); +#endif + return context; +} + +PandaUsbHandle::PandaUsbHandle(std::string serial) { + ssize_t num_devices; + libusb_device **dev_list = nullptr; + int err = 0; + ctx = init_usb_ctx(); + if (!ctx) { goto fail; } + + num_devices = libusb_get_device_list(ctx, &dev_list); + if (num_devices < 0) { goto fail; } + for (size_t i = 0; i < num_devices; ++i) { + libusb_device_descriptor desc; + libusb_get_device_descriptor(dev_list[i], &desc); + if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { + int ret = libusb_open(dev_list[i], &dev_handle); + if (dev_handle == nullptr || ret < 0) { goto fail; } + + unsigned char desc_serial[26] = {0}; + ret = libusb_get_string_descriptor_ascii(dev_handle, desc.iSerialNumber, desc_serial, sizeof(desc_serial)); + if (ret < 0) { goto fail; } + + hw_serial = std::string((char *)desc_serial, ret); + if (serial.empty() || serial == hw_serial) { + break; + } + libusb_close(dev_handle); + dev_handle = nullptr; + } + } + if (dev_handle == nullptr) { goto fail; } + libusb_free_device_list(dev_list, 1); + dev_list = nullptr; + + if (libusb_kernel_driver_active(dev_handle, 0) == 1) { + libusb_detach_kernel_driver(dev_handle, 0); + } + + err = libusb_set_configuration(dev_handle, 1); + if (err != 0) { goto fail; } + + err = libusb_claim_interface(dev_handle, 0); + if (err != 0) { goto fail; } + + return; + +fail: + if (dev_list != nullptr) { + libusb_free_device_list(dev_list, 1); + } + cleanup(); + throw std::runtime_error("Error connecting to panda over USB"); +} + +PandaUsbHandle::~PandaUsbHandle() { + std::lock_guard lk(hw_lock); + cleanup(); + connected = false; +} + +void PandaUsbHandle::cleanup() { + if (dev_handle != nullptr) { + libusb_release_interface(dev_handle, 0); + libusb_close(dev_handle); + dev_handle = nullptr; + } + + if (ctx != nullptr) { + libusb_exit(ctx); + ctx = nullptr; + } +} + +std::vector PandaUsbHandle::list() { + static std::unique_ptr context(init_usb_ctx(), libusb_exit); + ssize_t num_devices; + libusb_device **dev_list = nullptr; + std::vector serials; + if (!context) { return serials; } + + num_devices = libusb_get_device_list(context.get(), &dev_list); + if (num_devices < 0) { + LOGE("libusb can't get device list"); + goto finish; + } + + for (size_t i = 0; i < num_devices; ++i) { + libusb_device *device = dev_list[i]; + libusb_device_descriptor desc; + libusb_get_device_descriptor(device, &desc); + if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { + libusb_device_handle *handle = nullptr; + int ret = libusb_open(device, &handle); + if (ret < 0) { goto finish; } + + unsigned char desc_serial[26] = {0}; + ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, desc_serial, sizeof(desc_serial)); + libusb_close(handle); + if (ret < 0) { goto finish; } + + serials.push_back(std::string((char *)desc_serial, ret)); + } + } + +finish: + if (dev_list != nullptr) { + libusb_free_device_list(dev_list, 1); + } + return serials; +} + +void PandaUsbHandle::handle_usb_issue(int err, const char func[]) { + LOGE_100("usb error %d \"%s\" in %s", err, libusb_strerror((enum libusb_error)err), func); + if (err == LIBUSB_ERROR_NO_DEVICE) { + LOGE("lost connection"); + connected = false; + } +} + +int PandaUsbHandle::control_write(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected) { + return LIBUSB_ERROR_NO_DEVICE; + } + + std::lock_guard lk(hw_lock); + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, nullptr, 0, timeout); + if (err < 0) { handle_usb_issue(err, __func__); } + } while (err < 0 && connected); + + return err; +} + +int PandaUsbHandle::control_read(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected) { + return LIBUSB_ERROR_NO_DEVICE; + } + + std::lock_guard lk(hw_lock); + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, wLength, timeout); + if (err < 0) { handle_usb_issue(err, __func__); } + } while (err < 0 && connected); + + return err; +} + +int PandaUsbHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected) { + return 0; + } + + std::lock_guard lk(hw_lock); + do { + // Panda can NAK when the receive buffer is full. After 5ms, drop the messages. + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + + if (err == LIBUSB_ERROR_TIMEOUT) { + LOGW("Transmit buffer full"); + break; + } else if (err != 0 || length != transferred) { + handle_usb_issue(err, __func__); + } + } while (err != 0 && connected); + + return transferred; +} + +int PandaUsbHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected) { + return 0; + } + + std::lock_guard lk(hw_lock); + do { + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + + if (err == LIBUSB_ERROR_TIMEOUT) { + break; + } else if (err == LIBUSB_ERROR_OVERFLOW) { + comms_healthy = false; + LOGE_100("overflow got 0x%x", transferred); + } else if (err != 0) { + handle_usb_issue(err, __func__); + } + } while (err != 0 && connected); + + return transferred; +} diff --git a/selfdrive/pandad/panda_comms.h b/selfdrive/pandad/panda_comms.h index cdfb5019b62..a16a65ac9a2 100644 --- a/selfdrive/pandad/panda_comms.h +++ b/selfdrive/pandad/panda_comms.h @@ -6,17 +6,52 @@ #include #include +struct libusb_context; +struct libusb_device_handle; #define TIMEOUT 0 #define SPI_BUF_SIZE 2048 -class PandaSpiHandle { +class PandaCommsHandle { public: + PandaCommsHandle() {} + virtual ~PandaCommsHandle() {} + virtual void cleanup() = 0; + std::string hw_serial; std::atomic connected = true; std::atomic comms_healthy = true; + virtual int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT) = 0; + virtual int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT) = 0; + virtual int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT) = 0; + virtual int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT) = 0; +}; + +class PandaUsbHandle : public PandaCommsHandle { +public: + PandaUsbHandle(std::string serial); + ~PandaUsbHandle(); + + int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT); + int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT); + int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + void cleanup(); + + static std::vector list(); + +private: + libusb_context *ctx = nullptr; + libusb_device_handle *dev_handle = nullptr; + std::recursive_mutex hw_lock; + + void handle_usb_issue(int err, const char func[]); +}; + +class PandaSpiHandle : public PandaCommsHandle { +public: PandaSpiHandle(std::string serial); ~PandaSpiHandle(); From 27a8188e41fae317213104c6a7f26a764570bbcc Mon Sep 17 00:00:00 2001 From: thftgr Date: Fri, 26 Jun 2026 22:15:57 +0900 Subject: [PATCH 04/61] Expand modem support for EC20 and simplify ICCID checks --- system/hardware/tici/modem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/hardware/tici/modem.py b/system/hardware/tici/modem.py index 39266be1225..3a3492b3be1 100644 --- a/system/hardware/tici/modem.py +++ b/system/hardware/tici/modem.py @@ -258,7 +258,7 @@ def _init_at_channel(self) -> bool: return bool(r) and not r[0].startswith("AT") def _configure_modem(self, modem_version: str): - if not modem_version.startswith("EG25"): + if not modem_version.startswith(("EC20", "EG25")): return cmds = [ # clear initial EPS bearer APN (some carriers reject the default) @@ -288,7 +288,7 @@ def _do_initializing(self): return State.INITIALIZING identity = self._read_identity() - if not identity["iccid"] or not identity["imei"]: + if not identity["imei"]: logging.warning(f"identity read incomplete: {identity}, retrying") return State.INITIALIZING @@ -391,7 +391,7 @@ def _params_changed(self) -> bool: return False def _check_iccid(self, state): - if state in (State.INITIALIZING, State.DISCONNECTING) or not self.S["iccid"]: + if state in (State.INITIALIZING, State.DISCONNECTING): return iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F") if iccid and iccid != self.S["iccid"]: From 66df50d80f560ec3730bbcf26d3ef0e84293e6dc Mon Sep 17 00:00:00 2001 From: thftgr Date: Fri, 26 Jun 2026 22:38:10 +0900 Subject: [PATCH 05/61] Add SIM state tracking and optimize ICCID polling frequency --- system/hardware/tici/hardware.py | 3 ++- system/hardware/tici/modem.py | 37 ++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 26df5111dad..839ca1a028a 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -136,11 +136,12 @@ def get_network_type(self): def get_sim_info(self): ms = self.get_modem_state() sim_id = ms.get('iccid', '') + sim_state = ms.get('sim_state') or ("READY" if sim_id else "ABSENT") return { 'sim_id': sim_id, 'mcc_mnc': ms.get('mcc_mnc') or None, 'network_type': ["Unknown"], - 'sim_state': ["ABSENT"] if not sim_id else ["READY"], + 'sim_state': [sim_state], 'data_connected': ms.get('connected', False), } diff --git a/system/hardware/tici/modem.py b/system/hardware/tici/modem.py index 3a3492b3be1..9eac96144f1 100644 --- a/system/hardware/tici/modem.py +++ b/system/hardware/tici/modem.py @@ -56,6 +56,7 @@ "state": "INITIALIZING", "connected": False, "ip_address": "", "iccid": "", "mcc_mnc": "", "imei": "", "modem_version": "", + "sim_state": "ABSENT", "signal_strength": 0, "signal_quality": 0, "network_type": "unknown", "operator": "", "band": "", "channel": 0, "registration": "unknown", "temperatures": [], "extra": "", @@ -72,6 +73,7 @@ class State(Enum): STATE_WAIT = 1.0 # seconds to wait after each state handler returns +ICCID_CHECK_INTERVAL = 60.0 class PPPSession: @@ -176,6 +178,7 @@ def __init__(self): self._sim_change = False self._apn = "" # blank = network-provided via PCO self._roaming_allowed = True + self._last_iccid_check = 0.0 self.running = True self.S = INITIAL_STATE.copy() @@ -211,7 +214,7 @@ def _publish_state(self, **kwargs): os.chmod(f.name, 0o644) os.replace(f.name, STATE_PATH) - def _at(self, cmd): + def _at(self, cmd, log_errors=True): """Send AT command, return response lines. [] on error or if LPA holds port.""" fd = os.open(AT_LOCK, os.O_CREAT | os.O_RDWR, 0o666) try: @@ -238,14 +241,15 @@ def _at(self, cmd): lines.append(line) return lines except (RuntimeError, TimeoutError, OSError) as e: - logging.info(f"AT {cmd} failed: {e}") + if log_errors: + logging.info(f"AT {cmd} failed: {e}") return [] finally: fcntl.flock(fd, fcntl.LOCK_UN) os.close(fd) - def _atv(self, cmd, pfx): - for line in self._at(cmd): + def _atv(self, cmd, pfx, log_errors=True): + for line in self._at(cmd, log_errors=log_errors): if pfx in line and ":" in line: return line.split(":", 1)[1].strip() return None @@ -300,33 +304,39 @@ def _do_initializing(self): # blank APN lets the carrier supply one via PCO self._at(f'AT+CGDCONT={DIAL_CID},"IP","{self._apn}"') logging.info(f"APN '{self._apn or '(network-provided)'}' written to CID {DIAL_CID}, roaming={'on' if self._roaming_allowed else 'off'}") + if identity["sim_state"] == "ABSENT": + logging.info("SIM absent") self._sim_change = False # clear since we just re-read identity with the new SIM self._publish_state(**identity) return State.SEARCHING def _read_identity(self): - def first_line(cmd): - r = self._at(cmd) + def first_line(cmd, log_errors=True): + r = self._at(cmd, log_errors=log_errors) return r[0].strip() if r else "" imei = first_line("AT+CGSN") if not (imei.isdigit() and 14 <= len(imei) <= 17): # 3GPP TS 23.003 imei = "" - iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F") + iccid = (self._atv("AT+QCCID", "+QCCID:", log_errors=False) or "").rstrip("F") if not iccid.isdigit(): iccid = "" - imsi = first_line("AT+CIMI") + imsi = first_line("AT+CIMI", log_errors=False) mcc_mnc = imsi[:6] if imsi.isdigit() and len(imsi) >= 6 else "" + sim_state = "READY" if iccid or mcc_mnc else "ABSENT" modem_version = first_line("AT+GMR") - logging.info(f"imei={imei} iccid={iccid} mcc_mnc={mcc_mnc} ver={modem_version}") - return {"imei": imei, "iccid": iccid, "mcc_mnc": mcc_mnc, "modem_version": modem_version} + logging.info(f"imei={imei} iccid={iccid} mcc_mnc={mcc_mnc} sim_state={sim_state} ver={modem_version}") + return {"imei": imei, "iccid": iccid, "mcc_mnc": mcc_mnc, "modem_version": modem_version, "sim_state": sim_state} def _do_searching(self): + if self.S["sim_state"] == "ABSENT": + return self._searching_idle() + new_roaming = self._is_roaming_allowed() if new_roaming != self._roaming_allowed: logging.info(f"roaming changed: {self._roaming_allowed} -> {new_roaming}") @@ -393,7 +403,12 @@ def _params_changed(self) -> bool: def _check_iccid(self, state): if state in (State.INITIALIZING, State.DISCONNECTING): return - iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F") + now = time.monotonic() + if now - self._last_iccid_check < ICCID_CHECK_INTERVAL: + return + self._last_iccid_check = now + + iccid = (self._atv("AT+QCCID", "+QCCID:", log_errors=False) or "").rstrip("F") if iccid and iccid != self.S["iccid"]: logging.warning(f"iccid changed: {self.S['iccid']} -> {iccid}") self._sim_change = True From f6e86ea9045250f6ab8c6acbabbe0d327b3a525a Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 00:26:15 +0900 Subject: [PATCH 06/61] add carrot-agnos --- system/hardware/tici/agnos.json | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 07e2079ec9f..385455cb5f7 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,7 +1,7 @@ [ { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz", + "url": "https://agnosupdate.carrotpilot.app/18.4/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz", "hash": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb", "hash_raw": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb", "size": 3282256, @@ -12,7 +12,7 @@ }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz", + "url": "https://agnosupdate.carrotpilot.app/18.4/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz", "hash": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85", "hash_raw": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85", "size": 98124, @@ -23,7 +23,7 @@ }, { "name": "abl", - "url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz", + "url": "https://agnosupdate.carrotpilot.app/18.4/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz", "hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", "hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", "size": 274432, @@ -34,7 +34,7 @@ }, { "name": "aop", - "url": "https://commadist.azureedge.net/agnosupdate/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz", + "url": "https://agnosupdate.carrotpilot.app/18.4/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz", "hash": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1", "hash_raw": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1", "size": 184364, @@ -45,7 +45,7 @@ }, { "name": "devcfg", - "url": "https://commadist.azureedge.net/agnosupdate/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz", + "url": "https://agnosupdate.carrotpilot.app/18.4/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz", "hash": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc", "hash_raw": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc", "size": 40336, @@ -56,29 +56,29 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz", - "hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", - "hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", - "size": 17487872, + "url": "https://agnosupdate.carrotpilot.app/18.4/boot-a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df.img.xz", + "hash": "a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df", + "hash_raw": "a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df", + "size": 17864704, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8" + "ondevice_hash": "e4dac469c76307917b24d72cf42723c584ef38532d15e99f2293eb44e09e8193" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz", - "hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113", - "hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", + "url": "https://agnosupdate.carrotpilot.app/18.4/system-edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d.img.xz", + "hash": "77d8fbf70aae7e6d8ce6acc6a28cd862c155a7b1bf940064ee335b90a5ee91c1", + "hash_raw": "edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342", + "ondevice_hash": "92e8c1d099ceff3a46fe3299c74765013a56c873ce695d9c54c660fc20bbc898", "alt": { - "hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img", + "hash": "edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d", + "url": "https://agnosupdate.carrotpilot.app/18.4/system-edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d.img", "size": 4718592000 } } -] \ No newline at end of file +] From e0d3e22711ee01f9987aa2340898ffbb2a6a4a1c Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 02:49:14 +0900 Subject: [PATCH 07/61] Reorder `start_carrot_recovery` for proper initialization sequence --- launch_chffrplus.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index c353b90823d..31d0042ab0e 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -125,13 +125,13 @@ function launch { mkdir -p "$PYDEPS" export PYTHONPATH="$PYDEPS:$PWD${PYTHONPATH:+:$PYTHONPATH}" - start_carrot_recovery - # hardware specific init if [ -f /AGNOS ]; then agnos_init fi + start_carrot_recovery + FORCE_REBUILD=0 invalidate_modeld_build_if_needed invalidate_native_build_if_needed From b6fc94a93f3c5e9140b230b3bb33056803b706d6 Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 02:53:29 +0900 Subject: [PATCH 08/61] Install `jeepney` dependency if missing during initialization --- launch_chffrplus.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 31d0042ab0e..6109df35fe6 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -125,6 +125,13 @@ function launch { mkdir -p "$PYDEPS" export PYTHONPATH="$PYDEPS:$PWD${PYTHONPATH:+:$PYTHONPATH}" + if python3 -c "import jeepney" > /dev/null 2>&1; then + echo "jeepney already installed." + else + echo "jeepney installing to pydeps." + python3 -m pip install --target "$PYDEPS" --upgrade jeepney + fi + # hardware specific init if [ -f /AGNOS ]; then agnos_init From 3a38470d260275702e480826a03ec5ab29fdd8bb Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 10:28:02 +0900 Subject: [PATCH 09/61] Update Agnos URLs and add `agnos-tici.json` --- system/hardware/tici/agnos-tici.json | 84 ++++++++++++++++++++++++++++ system/hardware/tici/agnos.json | 32 +++++------ 2 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 system/hardware/tici/agnos-tici.json diff --git a/system/hardware/tici/agnos-tici.json b/system/hardware/tici/agnos-tici.json new file mode 100644 index 00000000000..c0cce92e9ec --- /dev/null +++ b/system/hardware/tici/agnos-tici.json @@ -0,0 +1,84 @@ +[ + { + "name": "xbl", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz", + "hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", + "hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", + "size": 3282256, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089" + }, + { + "name": "xbl_config", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz", + "hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", + "hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", + "size": 98124, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091" + }, + { + "name": "abl", + "url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz", + "hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", + "hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", + "size": 274432, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6" + }, + { + "name": "aop", + "url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz", + "hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", + "hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", + "size": 184364, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180" + }, + { + "name": "devcfg", + "url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz", + "hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", + "hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", + "size": 40336, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f" + }, + { + "name": "boot", + "url": "https://agnosupdate.carrotpilot.app/18.4/boot-a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df.img.xz", + "hash": "a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df", + "hash_raw": "a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df", + "size": 17864704, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "e4dac469c76307917b24d72cf42723c584ef38532d15e99f2293eb44e09e8193" + }, + { + "name": "system", + "url": "https://agnosupdate.carrotpilot.app/18.4/system-edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d.img.xz", + "hash": "77d8fbf70aae7e6d8ce6acc6a28cd862c155a7b1bf940064ee335b90a5ee91c1", + "hash_raw": "edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d", + "size": 4718592000, + "sparse": true, + "full_check": false, + "has_ab": true, + "ondevice_hash": "92e8c1d099ceff3a46fe3299c74765013a56c873ce695d9c54c660fc20bbc898", + "alt": { + "hash": "edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d", + "url": "https://agnosupdate.carrotpilot.app/18.4/system-edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d.img", + "size": 4718592000 + } + } +] diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 385455cb5f7..02c5fed23c1 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,7 +1,7 @@ [ { "name": "xbl", - "url": "https://agnosupdate.carrotpilot.app/18.4/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz", "hash": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb", "hash_raw": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb", "size": 3282256, @@ -12,7 +12,7 @@ }, { "name": "xbl_config", - "url": "https://agnosupdate.carrotpilot.app/18.4/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz", "hash": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85", "hash_raw": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85", "size": 98124, @@ -23,7 +23,7 @@ }, { "name": "abl", - "url": "https://agnosupdate.carrotpilot.app/18.4/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz", + "url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz", "hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", "hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", "size": 274432, @@ -34,7 +34,7 @@ }, { "name": "aop", - "url": "https://agnosupdate.carrotpilot.app/18.4/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz", + "url": "https://commadist.azureedge.net/agnosupdate/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz", "hash": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1", "hash_raw": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1", "size": 184364, @@ -45,7 +45,7 @@ }, { "name": "devcfg", - "url": "https://agnosupdate.carrotpilot.app/18.4/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz", + "url": "https://commadist.azureedge.net/agnosupdate/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz", "hash": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc", "hash_raw": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc", "size": 40336, @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://agnosupdate.carrotpilot.app/18.4/boot-a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df.img.xz", - "hash": "a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df", - "hash_raw": "a227ab7a6ec9f1f5b47c2d003ec75b7d54672e80b867843ee0b87eb735c775df", - "size": 17864704, + "url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz", + "hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", + "hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", + "size": 17487872, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "e4dac469c76307917b24d72cf42723c584ef38532d15e99f2293eb44e09e8193" + "ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8" }, { "name": "system", - "url": "https://agnosupdate.carrotpilot.app/18.4/system-edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d.img.xz", - "hash": "77d8fbf70aae7e6d8ce6acc6a28cd862c155a7b1bf940064ee335b90a5ee91c1", - "hash_raw": "edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d", + "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz", + "hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113", + "hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "92e8c1d099ceff3a46fe3299c74765013a56c873ce695d9c54c660fc20bbc898", + "ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342", "alt": { - "hash": "edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d", - "url": "https://agnosupdate.carrotpilot.app/18.4/system-edda90aeecb40a620db657521b1c38dfac77e610b06700671fca9fca82278b8d.img", + "hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", + "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img", "size": 4718592000 } } From b0390970b0d108059c6c86cfa9f564f5e9bb8930 Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 10:42:54 +0900 Subject: [PATCH 10/61] Adjust Agnos manifest selection based on model type --- launch_chffrplus.sh | 5 +++++ system/updated/updated.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 6109df35fe6..8fc96313677 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -21,6 +21,11 @@ function agnos_init { if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then AGNOS_PY="$DIR/system/hardware/tici/agnos.py" MANIFEST="$DIR/system/hardware/tici/agnos.json" + MODEL="$(tr -d '\000\r\n' 2>/dev/null < /sys/firmware/devicetree/base/model | tr '[:upper:]' '[:lower:]')" + MODEL="${MODEL#comma }" + if [ "$MODEL" = "c3" ] || [ "$MODEL" = "tici" ]; then + MANIFEST="$DIR/system/hardware/tici/agnos-tici.json" + fi if $AGNOS_PY --verify $MANIFEST; then sudo reboot fi diff --git a/system/updated/updated.py b/system/updated/updated.py index e47131347f0..d6b6cc9b469 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -221,6 +221,13 @@ def handle_agnos_update() -> None: set_offroad_alert("Offroad_NeosUpdate", True) manifest_path = os.path.join(OVERLAY_MERGED, "system/hardware/tici/agnos.json") + try: + with open("/sys/firmware/devicetree/base/model") as f: + model = f.read().replace("\x00", "").strip().lower().removeprefix("comma ") + if model in ("c3", "tici"): + manifest_path = os.path.join(OVERLAY_MERGED, "system/hardware/tici/agnos-tici.json") + except OSError: + pass target_slot_number = get_target_slot_number() flash_agnos_update(manifest_path, target_slot_number, cloudlog) set_offroad_alert("Offroad_NeosUpdate", False) From 39ad5f6c1baf0d685912018673ed231a69961419 Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 13:14:09 +0900 Subject: [PATCH 11/61] Move `jeepney` installation to AGNOS update block --- launch_chffrplus.sh | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 8fc96313677..c961b208cd8 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -19,6 +19,24 @@ function agnos_init { # Check if AGNOS update is required if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then + echo "Waiting for internet..." + + timeout=60 + while [ $timeout -gt 0 ]; do + if getent hosts pypi.org >/dev/null 2>&1; then + break + fi + sleep 1 + timeout=$((timeout-1)) + done + + if python3 -c "import jeepney" > /dev/null 2>&1; then + echo "jeepney already installed." + else + echo "jeepney installing to pydeps." + python3 -m pip install --target "$PYDEPS" --upgrade jeepney + fi + AGNOS_PY="$DIR/system/hardware/tici/agnos.py" MANIFEST="$DIR/system/hardware/tici/agnos.json" MODEL="$(tr -d '\000\r\n' 2>/dev/null < /sys/firmware/devicetree/base/model | tr '[:upper:]' '[:lower:]')" @@ -130,13 +148,6 @@ function launch { mkdir -p "$PYDEPS" export PYTHONPATH="$PYDEPS:$PWD${PYTHONPATH:+:$PYTHONPATH}" - if python3 -c "import jeepney" > /dev/null 2>&1; then - echo "jeepney already installed." - else - echo "jeepney installing to pydeps." - python3 -m pip install --target "$PYDEPS" --upgrade jeepney - fi - # hardware specific init if [ -f /AGNOS ]; then agnos_init From 6483898cd90c3f5d754106cdb09867d24adc83f2 Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 13:31:13 +0900 Subject: [PATCH 12/61] Adjust internet wait logic in AGNOS version check --- launch_chffrplus.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index c961b208cd8..120609a62b4 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -21,13 +21,15 @@ function agnos_init { if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then echo "Waiting for internet..." - timeout=60 - while [ $timeout -gt 0 ]; do + timeout=0 + while [ $timeout -lt 120 ]; do if getent hosts pypi.org >/dev/null 2>&1; then break fi - sleep 1 - timeout=$((timeout-1)) + echo "Waiting for internet... (${timeout})" + sleep 5 + timeout=$((timeout+5)) + done if python3 -c "import jeepney" > /dev/null 2>&1; then From db90d81c3120d016f35c78abd3f2f1aed518be89 Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 13:45:54 +0900 Subject: [PATCH 13/61] Add debug logs for AGNOS updater variables to improve visibility --- launch_chffrplus.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 120609a62b4..25abe3b8057 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -49,6 +49,9 @@ function agnos_init { if $AGNOS_PY --verify $MANIFEST; then sudo reboot fi + echo "AGNOS_PY=${AGNOS_PY}" + echo "MANIFEST=${MANIFEST}" + echo "MODEL=${MODEL}" $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST fi } From 1ca5a935a87a145bea56633b37d38504001c197d Mon Sep 17 00:00:00 2001 From: thftgr Date: Sat, 27 Jun 2026 13:53:06 +0900 Subject: [PATCH 14/61] Add debug logs for device model and manifest path in AGNOS updater --- system/updated/updated.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/system/updated/updated.py b/system/updated/updated.py index d6b6cc9b469..ae8c8c05418 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -224,9 +224,13 @@ def handle_agnos_update() -> None: try: with open("/sys/firmware/devicetree/base/model") as f: model = f.read().replace("\x00", "").strip().lower().removeprefix("comma ") + print(f"[agnos] device model: {model}", flush=True) + if model in ("c3", "tici"): manifest_path = os.path.join(OVERLAY_MERGED, "system/hardware/tici/agnos-tici.json") - except OSError: + print(f"[agnos] manifest_path: {manifest_path}", flush=True) + except OSError as e: + print(f"[agnos] model read failed: {e}", flush=True) pass target_slot_number = get_target_slot_number() flash_agnos_update(manifest_path, target_slot_number, cloudlog) From 42ce0d0e609d086dcc1921a922a5127314fd39b7 Mon Sep 17 00:00:00 2001 From: thftgr Date: Sun, 28 Jun 2026 08:45:38 +0900 Subject: [PATCH 15/61] Add debug log for AGNOS updater completion --- launch_chffrplus.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 25abe3b8057..0f09e635f2f 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -53,6 +53,7 @@ function agnos_init { echo "MANIFEST=${MANIFEST}" echo "MODEL=${MODEL}" $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST + echo "end updater $AGNOS_PY $MANIFEST" fi } From 3e2669190be316e93697807ca434d3bbb0ae84d9 Mon Sep 17 00:00:00 2001 From: thftgr Date: Sun, 28 Jun 2026 08:54:24 +0900 Subject: [PATCH 16/61] Reorder `start_carrot_recovery` call for improved initialization logic --- launch_chffrplus.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 0f09e635f2f..69d1ea56b16 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -153,13 +153,13 @@ function launch { PYDEPS="$DIR/pydeps" mkdir -p "$PYDEPS" export PYTHONPATH="$PYDEPS:$PWD${PYTHONPATH:+:$PYTHONPATH}" + start_carrot_recovery # hardware specific init if [ -f /AGNOS ]; then agnos_init fi - start_carrot_recovery FORCE_REBUILD=0 invalidate_modeld_build_if_needed From 7e9513fb8fa26827bc3523a3bc946f846135dd43 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Mon, 29 Jun 2026 21:04:28 +0900 Subject: [PATCH 17/61] fix screen action (#417) --- launch_chffrplus.sh | 5 +- system/ui/lib/application.py | 97 ++++++++++++++++++++++++++++-------- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 69d1ea56b16..2424038e459 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -52,7 +52,10 @@ function agnos_init { echo "AGNOS_PY=${AGNOS_PY}" echo "MANIFEST=${MANIFEST}" echo "MODEL=${MODEL}" - $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST + if ! python3 $DIR/system/ui/updater.py $AGNOS_PY $MANIFEST; then + echo "python updater failed, falling back to bundled updater" + $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST + fi echo "end updater $AGNOS_PY $MANIFEST" fi } diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 70ce9c16fe7..36714604528 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -6,6 +6,7 @@ import time import signal import sys +import struct import pyray as rl import threading import platform @@ -33,6 +34,15 @@ MAX_TOUCH_SLOTS = 2 TOUCH_HISTORY_TIMEOUT = 3.0 # Seconds before touch points fade out +TOUCH_EVENT_DEVICE = "/dev/input/by-path/platform-894000.i2c-event" +EVENT_FORMAT = "llHHi" +EVENT_SIZE = struct.calcsize(EVENT_FORMAT) +EV_SYN, EV_KEY, EV_ABS = 0x00, 0x01, 0x03 +SYN_REPORT = 0x00 +BTN_TOUCH = 0x14a +ABS_MT_SLOT = 0x2f +ABS_MT_TRACKING_ID = 0x39 + BIG_UI = os.getenv("BIG", "0") == "1" ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1" SHOW_FPS = os.getenv("SHOW_FPS") == "1" @@ -146,6 +156,10 @@ def __init__(self, scale: float = 1.0): self._events: deque[MouseEvent] = deque(maxlen=MOUSE_THREAD_RATE) # bound event list self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOTS + self._slot_active: list[bool] = [False] * MAX_TOUCH_SLOTS + self._cur_slot = 0 + self._saw_mt = False + self._rk = Ratekeeper(MOUSE_THREAD_RATE, print_delay_threshold=None) self._lock = threading.Lock() self._exit_event = threading.Event() @@ -169,34 +183,73 @@ def stop(self): self._thread.join() def _run_thread(self): - while not self._exit_event.is_set(): - rl.poll_input_events() - self._handle_mouse_event() - self._rk.keep_time() + touch_fd = self._open_touch_device() + try: + while not self._exit_event.is_set(): + rl.poll_input_events() + if touch_fd is not None: + self._read_touch_events(touch_fd) + else: + self._handle_mouse_event() + self._rk.keep_time() + finally: + if touch_fd is not None: + os.close(touch_fd) + + def _open_touch_device(self) -> int | None: + if PC: + return None + try: + return os.open(TOUCH_EVENT_DEVICE, os.O_RDONLY | os.O_NONBLOCK) + except OSError as e: + cloudlog.warning(f"mouse: using raylib touch, can't open {TOUCH_EVENT_DEVICE}: {e}") + return None + + def _read_touch_events(self, fd: int) -> None: + try: + data = os.read(fd, EVENT_SIZE * 64) + except (BlockingIOError, OSError): + return + + for off in range(0, len(data) - EVENT_SIZE + 1, EVENT_SIZE): + _sec, _usec, etype, code, value = struct.unpack(EVENT_FORMAT, data[off:off + EVENT_SIZE]) + if etype == EV_ABS: + if code == ABS_MT_SLOT: + self._cur_slot = value + elif code == ABS_MT_TRACKING_ID: + self._saw_mt = True + if 0 <= self._cur_slot < MAX_TOUCH_SLOTS: + self._slot_active[self._cur_slot] = value != -1 + elif etype == EV_KEY and code == BTN_TOUCH and not self._saw_mt: + self._slot_active[0] = value != 0 + elif etype == EV_SYN and code == SYN_REPORT: + for slot in range(MAX_TOUCH_SLOTS): + self._append_event(slot, self._slot_active[slot]) def _handle_mouse_event(self): - # TODO: read touch events from evdev directly to get real kernel timestamps. - # Polling at 140Hz with time.monotonic() causes timing jitter that makes scroll - # velocity oscillate (alternating high/low). Real timestamps would also let us - # detect swipe-stop-lift via event gaps instead of the fragile decel heuristic. for slot in range(MAX_TOUCH_SLOTS): + self._append_event(slot, rl.is_mouse_button_down(slot)) + + def _append_event(self, slot: int, down: bool) -> None: + prev = self._prev_mouse_event[slot] + prev_down = prev.left_down if prev is not None else False + pressed = down and not prev_down + released = prev_down and not down + + if down: mouse_pos = rl.get_touch_position(slot) x = mouse_pos.x / self._scale if self._scale != 1.0 else mouse_pos.x y = mouse_pos.y / self._scale if self._scale != 1.0 else mouse_pos.y - ev = MouseEvent( - MousePos(x, y), - slot, - rl.is_mouse_button_pressed(slot), # noqa: TID251 - rl.is_mouse_button_released(slot), # noqa: TID251 - rl.is_mouse_button_down(slot), - time.monotonic(), - ) - # Only add changes - prev = self._prev_mouse_event[slot] - if prev is None or ev[:-1] != prev[:-1]: - with self._lock: - self._events.append(ev) - self._prev_mouse_event[slot] = ev + pos = MousePos(x, y) + else: + pos = prev.pos if prev is not None else MousePos(0.0, 0.0) + + ev = MouseEvent(pos, slot, pressed, released, down, time.monotonic()) + + if prev is None or ev[:-1] != prev[:-1]: + with self._lock: + self._events.append(ev) + self._prev_mouse_event[slot] = ev class GuiApplication: From 28468bb0e943b2ff5490d36ad747b6f5a1c235df Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 21:35:50 +0900 Subject: [PATCH 18/61] Use IFE path for AR0231 cameras to avoid CAM-ICP/BPS recovery --- system/camerad/cameras/spectra.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 84de19aa93f..502b6a4c70f 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -280,6 +280,12 @@ void SpectraCamera::camera_open(VisionIpcServer *v) { if (!enabled) return; + if (sensor->image_sensor == cereal::FrameData::ImageSensor::AR0231 && + cc.output_type == ISP_BPS_PROCESSED) { + LOGW("AR0231 camera %d: using IFE path to avoid CAM-ICP/BPS recovery", cc.camera_num); + cc.output_type = ISP_IFE_PROCESSED; + } + buf.out_img_width = sensor->frame_width / sensor->out_scale; buf.out_img_height = (sensor->hdr_offset > 0 ? (sensor->frame_height - sensor->hdr_offset) / 2 : sensor->frame_height) / sensor->out_scale; From d7a9fbbc02ebb71fc101c55a755b7328909d03dd Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 21:48:21 +0900 Subject: [PATCH 19/61] "Remove redundant AR0231 IFE path handling and placeholder entries in BPS files" --- system/camerad/cameras/bps_blobs.h | 3 --- system/camerad/cameras/spectra.cc | 6 ------ 2 files changed, 9 deletions(-) diff --git a/system/camerad/cameras/bps_blobs.h b/system/camerad/cameras/bps_blobs.h index ee31698e7f0..ab8ac7545f3 100644 --- a/system/camerad/cameras/bps_blobs.h +++ b/system/camerad/cameras/bps_blobs.h @@ -8,21 +8,18 @@ unsigned char bps_cfg[4][768] = { -{ /* placeholder */ }, { /* placeholder */ }, {0x3, 0x0, 0x0, 0x0, 0x88, 0x7, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x50, 0xB, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x88, 0x7, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0xC0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x60, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, {0x3, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xF, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x40, 0x5, 0x0, 0x0, 0xF8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x5, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x80, 0x5, 0x0, 0x0, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, }; unsigned char bps_striping_output[4][0xc58] = { -{ /* placeholder */ }, { /* placeholder */ }, {0x5, 0x0, 0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x5B, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x5B, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x4, 0x5C, 0x2, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x70, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x89, 0x23, 0x0, 0x1, 0x0, 0x0, 0x0, 0xCC, 0x5, 0x87, 0x7, 0x0, 0x0, 0x0, 0x0, 0xD0, 0x5, 0x87, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x2, 0xA4, 0xFD, 0x50, 0xB1, 0x9, 0x0, 0x8, 0x0, 0x33, 0x0, 0x8, 0x2, 0xA4, 0xFD, 0x50, 0xB1, 0x9, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x2, 0xA6, 0xFD, 0x68, 0xC0, 0x9, 0x0, 0x8, 0x0, 0x34, 0x0, 0xD0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0x14, 0x4, 0xD3, 0x5, 0x0, 0x0, 0x0, 0x0, 0x18, 0x4, 0xCF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x50, 0x0, 0xA4, 0xFD, 0x10, 0xAA, 0x5, 0x0, 0x8, 0x0, 0x33, 0x0, 0x50, 0x0, 0xA4, 0xFD, 0x10, 0xAA, 0x5, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x0, 0xA6, 0xFD, 0x88, 0xA4, 0x5, 0x0, 0x8, 0x0, 0x34, 0x0, 0x18, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0x5C, 0x2, 0x1B, 0x4, 0x0, 0x0, 0x0, 0x0, 0x60, 0x2, 0x17, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x98, 0xFE, 0xA4, 0xFD, 0x50, 0x8B, 0x7, 0x0, 0x8, 0x0, 0x33, 0x0, 0x98, 0xFE, 0xA4, 0xFD, 0x50, 0x8B, 0x7, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9E, 0xFE, 0xA6, 0xFD, 0x28, 0x71, 0x7, 0x0, 0x8, 0x0, 0x34, 0x0, 0x60, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0xC0, 0x0, 0x63, 0x2, 0x0, 0x0, 0x0, 0x0, 0xC4, 0x0, 0x5F, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x9C, 0x1, 0xCE, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFC, 0xFC, 0xA4, 0xFD, 0x20, 0xA9, 0xE, 0x0, 0x8, 0x0, 0x33, 0x0, 0xFC, 0xFC, 0xA4, 0xFD, 0x20, 0xA9, 0xE, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xFD, 0xA6, 0xFD, 0xA8, 0x7B, 0xE, 0x0, 0x8, 0x0, 0x34, 0x0, 0xC4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x98, 0x7, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3C, 0xFC, 0xA4, 0xFD, 0x20, 0xBF, 0x13, 0x0, 0x8, 0x0, 0x33, 0x0, 0x3C, 0xFC, 0xA4, 0xFD, 0x20, 0xBF, 0x13, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3E, 0xFC, 0xA6, 0xFD, 0xA8, 0xA6, 0x13, 0x0, 0x8, 0x0, 0x34, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE0, 0x9C, 0x3, 0x0, 0xEC, 0x3, 0x1, 0x0, 0x5D, 0x59, 0xAB, 0x0, 0xC8, 0xFC, 0xFB, 0x40, 0x3, 0x0, 0x0, 0x0, 0x0, 0xFC, 0x7F, 0x40, 0xAC, 0x1F, 0x84, 0x40, 0x0, 0x20, 0x4E, 0x0, 0xAB, 0xAA, 0xAA, 0xAA, 0x40, 0xD9, 0xFB, 0x40, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC8, 0xED, 0x80, 0x40, 0x11, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x9F, 0x9, 0x0, 0x0, 0xB8, 0x2B, 0x6B, 0x15, 0xC0, 0x2A, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFC, 0x7F, 0x40, 0x0, 0x5, 0x1, 0x0, 0x8, 0xFC, 0x7F, 0x40, 0x74, 0xFC, 0x7F, 0x40, 0x5C, 0xFC, 0x7F, 0x40, 0xCC, 0xA8, 0x81, 0x40, 0x8, 0xFC, 0x7F, 0x40, 0x64, 0x2A, 0x84, 0x40, 0x3, 0x0, 0x0, 0x0, 0x30, 0xEE, 0x80, 0x40, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x8, 0x29, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x2A, 0x84, 0x40, 0x8, 0x29, 0x84, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x9, 0x0, 0x0, 0xF8, 0x22, 0xFC, 0x40, 0xB8, 0x16, 0x80, 0x40, 0xD0, 0x34, 0x84, 0x40, 0x1, 0xB0, 0xAB, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xD0, 0x1D, 0x3, 0x41, 0x30, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0xA4, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0xC, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x34, 0x31, 0x32, 0x31, 0x36, 0x30, 0xEE, 0x80, 0x40, 0xA8, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x58, 0x7, 0x80, 0x40, 0x4C, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x40, 0xC0, 0x4C, 0x0, 0x41, 0x4C, 0x0, 0x0, 0x0, 0x9C, 0xFC, 0x7F, 0x40, 0x78, 0x22, 0x1, 0x0, 0xC4, 0x15, 0x1, 0x0, 0xC0, 0x22, 0x1, 0x0, 0x0, 0xA1, 0x0, 0x0, 0x0, 0xA1, 0x0, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xB4, 0x7, 0x80, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xB0, 0x7, 0x80, 0x40, 0x0, 0x0, 0x0, 0x0}, {0x7, 0x0, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x7B, 0x1, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF0, 0x5, 0xF8, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF8, 0x2, 0x7C, 0x1, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF0, 0x5, 0xF8, 0x2, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x70, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x3, 0x0, 0x88, 0x7, 0xB8, 0x4, 0x14, 0x0, 0x88, 0x7, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x74, 0x16, 0x80, 0x40, 0x52, 0x5, 0x1, 0x0, 0x1A, 0x3, 0x0, 0x0, 0xC4, 0xED, 0x80, 0x40, 0x98, 0x1, 0x0, 0x0, 0x11, 0x7B, 0x9C, 0x7C, 0x11, 0x7B, 0x9C, 0x7C, 0x14, 0x69, 0x69, 0xD, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE8, 0x8A, 0xFC, 0x40, 0xB2, 0x4, 0x1, 0x0, 0x11, 0x7B, 0x9C, 0x7C, 0xBC, 0x3, 0x1, 0x0, 0xD8, 0xE3, 0xE4, 0x3, 0xC8, 0xFC, 0xFB, 0x40, 0x3, 0x0, 0x0, 0x0, 0x60, 0xF2, 0x7F, 0x40, 0x0, 0x58, 0x3E, 0x0, 0x1, 0x20, 0x4E, 0x0, 0xC4, 0x8, 0x7F, 0xA, 0x0, 0x0, 0x0, 0x0, 0xC8, 0x8, 0x7F, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x64, 0x4, 0x3F, 0x5, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x84, 0x3, 0x8, 0xFD, 0x50, 0x2C, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0x84, 0x3, 0x8, 0xFD, 0x50, 0x2C, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8A, 0x3, 0xA, 0xFD, 0xC8, 0x4A, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC8, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xC, 0x7, 0xCB, 0x8, 0x0, 0x0, 0x0, 0x0, 0x10, 0x7, 0xC7, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x88, 0x3, 0x63, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xCC, 0x1, 0x8, 0xFD, 0xD0, 0xA, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0xCC, 0x1, 0x8, 0xFD, 0xD0, 0xA, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD2, 0x1, 0xA, 0xFD, 0xA8, 0x14, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0x10, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0x54, 0x5, 0x13, 0x7, 0x0, 0x0, 0x0, 0x0, 0x58, 0x5, 0xF, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xAC, 0x2, 0x87, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x8, 0xFD, 0xD0, 0xD1, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x14, 0x0, 0x8, 0xFD, 0xD0, 0xD1, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1A, 0x0, 0xA, 0xFD, 0x8, 0xC7, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x58, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0x9C, 0x3, 0x5B, 0x5, 0x0, 0x0, 0x0, 0x0, 0xA0, 0x3, 0x57, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xD0, 0x1, 0xAB, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5C, 0xFE, 0x8, 0xFD, 0x50, 0x81, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0x5C, 0xFE, 0x8, 0xFD, 0x50, 0x81, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x62, 0xFE, 0xA, 0xFD, 0xE8, 0x61, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0xA0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xE4, 0x1, 0xA3, 0x3, 0x0, 0x0, 0x0, 0x0, 0xE8, 0x1, 0x9F, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xF4, 0x0, 0xCF, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA4, 0xFC, 0x8, 0xFD, 0x50, 0x19, 0x14, 0x0, 0x9, 0x0, 0x38, 0x0, 0xA4, 0xFC, 0x8, 0xFD, 0x50, 0x19, 0x14, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xAA, 0xFC, 0xA, 0xFD, 0x48, 0xE5, 0x13, 0x0, 0x9, 0x0, 0x38, 0x0, 0xE8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xC0, 0x0, 0xEB, 0x1, 0x0, 0x0, 0x0, 0x0, 0xC4, 0x0, 0xE7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x62, 0x0, 0xF3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x91, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x91, 0x0, 0x0, 0x0, 0x48, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x24, 0x1, 0x92, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x92, 0x0, 0x49, 0x0, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x24, 0x1, 0x92, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xFB, 0x8, 0xFD, 0x40, 0x10, 0x1D, 0x0, 0x9, 0x0, 0x38, 0x0, 0x80, 0xFB, 0x8, 0xFD, 0x40, 0x10, 0x1D, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x86, 0xFB, 0xA, 0xFD, 0x88, 0xCE, 0x1C, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xC5, 0x6, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x61, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x62, 0x0, 0x31, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xFA, 0x8, 0xFD, 0x40, 0x60, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC0, 0xFA, 0x8, 0xFD, 0x40, 0x60, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC2, 0xFA, 0xA, 0xFD, 0x68, 0x3F, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x8B, 0x4, 0x0, 0xEC, 0x3, 0x1, 0x0, 0x58, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x50, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0xFC, 0xC7, 0x10, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x10, 0x41, 0x0, 0x0, 0x0, 0x0, 0x57, 0xC, 0x0, 0x0, 0x69, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x77, 0x0, 0x0, 0x0, 0x7C, 0x0, 0x0, 0x0, 0x8C, 0x1, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, 0xA4, 0x51, 0x1, 0x0, 0x58, 0x3, 0x0, 0x0, 0x8, 0xFC, 0x7F, 0x40, 0x64, 0x2A, 0x84, 0x40, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x10, 0x41, 0x57, 0xC, 0x0, 0x0, 0x58, 0xC, 0x0, 0x0, 0xFC, 0xC7, 0x10, 0x41, 0x8A, 0x1, 0x0, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x1, 0xB0, 0xAB, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xD0, 0x1D, 0x3, 0x41, 0x30, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0xA4, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x40, 0x5, 0x0, 0x0, 0x4C, 0x17, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x35, 0x33, 0x32, 0x31, 0x36, 0x68, 0x8, 0x80, 0x40, 0xA8, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x58, 0x7, 0x80, 0x40, 0xA0, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x40, 0xC0, 0x4C, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x9C, 0xFC, 0x7F, 0x40, 0x58, 0x7, 0x80, 0x40, 0xC4, 0x15, 0x1, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xE0, 0xCF, 0x0, 0x0, 0xE0, 0xCF, 0x0, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xB4, 0x7, 0x80, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xB0, 0x7, 0x80, 0x40, 0x0, 0x0, 0x0, 0x0}, }; unsigned char bps_settings[4][684] = { -{ /* placeholder */ }, { /* placeholder */ }, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 502b6a4c70f..84de19aa93f 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -280,12 +280,6 @@ void SpectraCamera::camera_open(VisionIpcServer *v) { if (!enabled) return; - if (sensor->image_sensor == cereal::FrameData::ImageSensor::AR0231 && - cc.output_type == ISP_BPS_PROCESSED) { - LOGW("AR0231 camera %d: using IFE path to avoid CAM-ICP/BPS recovery", cc.camera_num); - cc.output_type = ISP_IFE_PROCESSED; - } - buf.out_img_width = sensor->frame_width / sensor->out_scale; buf.out_img_height = (sensor->hdr_offset > 0 ? (sensor->frame_height - sensor->hdr_offset) / 2 : sensor->frame_height) / sensor->out_scale; From 2cfa49c0cbcc7833e849057659e1a77995361a89 Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 21:53:09 +0900 Subject: [PATCH 20/61] "Add logic for BPS blob index mapping and warnings for mismatched sensor indices" --- system/camerad/cameras/bps_blobs.h | 3 +++ system/camerad/cameras/spectra.cc | 41 +++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/system/camerad/cameras/bps_blobs.h b/system/camerad/cameras/bps_blobs.h index ab8ac7545f3..ee31698e7f0 100644 --- a/system/camerad/cameras/bps_blobs.h +++ b/system/camerad/cameras/bps_blobs.h @@ -8,18 +8,21 @@ unsigned char bps_cfg[4][768] = { +{ /* placeholder */ }, { /* placeholder */ }, {0x3, 0x0, 0x0, 0x0, 0x88, 0x7, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x50, 0xB, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x88, 0x7, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0xC0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x60, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, {0x3, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xF, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x40, 0x5, 0x0, 0x0, 0xF8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x5, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x80, 0x5, 0x0, 0x0, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, }; unsigned char bps_striping_output[4][0xc58] = { +{ /* placeholder */ }, { /* placeholder */ }, {0x5, 0x0, 0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x5B, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x5B, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x4, 0x5C, 0x2, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x70, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x89, 0x23, 0x0, 0x1, 0x0, 0x0, 0x0, 0xCC, 0x5, 0x87, 0x7, 0x0, 0x0, 0x0, 0x0, 0xD0, 0x5, 0x87, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x2, 0xA4, 0xFD, 0x50, 0xB1, 0x9, 0x0, 0x8, 0x0, 0x33, 0x0, 0x8, 0x2, 0xA4, 0xFD, 0x50, 0xB1, 0x9, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x2, 0xA6, 0xFD, 0x68, 0xC0, 0x9, 0x0, 0x8, 0x0, 0x34, 0x0, 0xD0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0x14, 0x4, 0xD3, 0x5, 0x0, 0x0, 0x0, 0x0, 0x18, 0x4, 0xCF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x50, 0x0, 0xA4, 0xFD, 0x10, 0xAA, 0x5, 0x0, 0x8, 0x0, 0x33, 0x0, 0x50, 0x0, 0xA4, 0xFD, 0x10, 0xAA, 0x5, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x0, 0xA6, 0xFD, 0x88, 0xA4, 0x5, 0x0, 0x8, 0x0, 0x34, 0x0, 0x18, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0x5C, 0x2, 0x1B, 0x4, 0x0, 0x0, 0x0, 0x0, 0x60, 0x2, 0x17, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x98, 0xFE, 0xA4, 0xFD, 0x50, 0x8B, 0x7, 0x0, 0x8, 0x0, 0x33, 0x0, 0x98, 0xFE, 0xA4, 0xFD, 0x50, 0x8B, 0x7, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9E, 0xFE, 0xA6, 0xFD, 0x28, 0x71, 0x7, 0x0, 0x8, 0x0, 0x34, 0x0, 0x60, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0xC0, 0x0, 0x63, 0x2, 0x0, 0x0, 0x0, 0x0, 0xC4, 0x0, 0x5F, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x9C, 0x1, 0xCE, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFC, 0xFC, 0xA4, 0xFD, 0x20, 0xA9, 0xE, 0x0, 0x8, 0x0, 0x33, 0x0, 0xFC, 0xFC, 0xA4, 0xFD, 0x20, 0xA9, 0xE, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xFD, 0xA6, 0xFD, 0xA8, 0x7B, 0xE, 0x0, 0x8, 0x0, 0x34, 0x0, 0xC4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x98, 0x7, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3C, 0xFC, 0xA4, 0xFD, 0x20, 0xBF, 0x13, 0x0, 0x8, 0x0, 0x33, 0x0, 0x3C, 0xFC, 0xA4, 0xFD, 0x20, 0xBF, 0x13, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3E, 0xFC, 0xA6, 0xFD, 0xA8, 0xA6, 0x13, 0x0, 0x8, 0x0, 0x34, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE0, 0x9C, 0x3, 0x0, 0xEC, 0x3, 0x1, 0x0, 0x5D, 0x59, 0xAB, 0x0, 0xC8, 0xFC, 0xFB, 0x40, 0x3, 0x0, 0x0, 0x0, 0x0, 0xFC, 0x7F, 0x40, 0xAC, 0x1F, 0x84, 0x40, 0x0, 0x20, 0x4E, 0x0, 0xAB, 0xAA, 0xAA, 0xAA, 0x40, 0xD9, 0xFB, 0x40, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC8, 0xED, 0x80, 0x40, 0x11, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x9F, 0x9, 0x0, 0x0, 0xB8, 0x2B, 0x6B, 0x15, 0xC0, 0x2A, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFC, 0x7F, 0x40, 0x0, 0x5, 0x1, 0x0, 0x8, 0xFC, 0x7F, 0x40, 0x74, 0xFC, 0x7F, 0x40, 0x5C, 0xFC, 0x7F, 0x40, 0xCC, 0xA8, 0x81, 0x40, 0x8, 0xFC, 0x7F, 0x40, 0x64, 0x2A, 0x84, 0x40, 0x3, 0x0, 0x0, 0x0, 0x30, 0xEE, 0x80, 0x40, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x8, 0x29, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x2A, 0x84, 0x40, 0x8, 0x29, 0x84, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x9, 0x0, 0x0, 0xF8, 0x22, 0xFC, 0x40, 0xB8, 0x16, 0x80, 0x40, 0xD0, 0x34, 0x84, 0x40, 0x1, 0xB0, 0xAB, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xD0, 0x1D, 0x3, 0x41, 0x30, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0xA4, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0xC, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x34, 0x31, 0x32, 0x31, 0x36, 0x30, 0xEE, 0x80, 0x40, 0xA8, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x58, 0x7, 0x80, 0x40, 0x4C, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x40, 0xC0, 0x4C, 0x0, 0x41, 0x4C, 0x0, 0x0, 0x0, 0x9C, 0xFC, 0x7F, 0x40, 0x78, 0x22, 0x1, 0x0, 0xC4, 0x15, 0x1, 0x0, 0xC0, 0x22, 0x1, 0x0, 0x0, 0xA1, 0x0, 0x0, 0x0, 0xA1, 0x0, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xB4, 0x7, 0x80, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xB0, 0x7, 0x80, 0x40, 0x0, 0x0, 0x0, 0x0}, {0x7, 0x0, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x7B, 0x1, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF0, 0x5, 0xF8, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF8, 0x2, 0x7C, 0x1, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF0, 0x5, 0xF8, 0x2, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x70, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x3, 0x0, 0x88, 0x7, 0xB8, 0x4, 0x14, 0x0, 0x88, 0x7, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x74, 0x16, 0x80, 0x40, 0x52, 0x5, 0x1, 0x0, 0x1A, 0x3, 0x0, 0x0, 0xC4, 0xED, 0x80, 0x40, 0x98, 0x1, 0x0, 0x0, 0x11, 0x7B, 0x9C, 0x7C, 0x11, 0x7B, 0x9C, 0x7C, 0x14, 0x69, 0x69, 0xD, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE8, 0x8A, 0xFC, 0x40, 0xB2, 0x4, 0x1, 0x0, 0x11, 0x7B, 0x9C, 0x7C, 0xBC, 0x3, 0x1, 0x0, 0xD8, 0xE3, 0xE4, 0x3, 0xC8, 0xFC, 0xFB, 0x40, 0x3, 0x0, 0x0, 0x0, 0x60, 0xF2, 0x7F, 0x40, 0x0, 0x58, 0x3E, 0x0, 0x1, 0x20, 0x4E, 0x0, 0xC4, 0x8, 0x7F, 0xA, 0x0, 0x0, 0x0, 0x0, 0xC8, 0x8, 0x7F, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x64, 0x4, 0x3F, 0x5, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x84, 0x3, 0x8, 0xFD, 0x50, 0x2C, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0x84, 0x3, 0x8, 0xFD, 0x50, 0x2C, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8A, 0x3, 0xA, 0xFD, 0xC8, 0x4A, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC8, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xC, 0x7, 0xCB, 0x8, 0x0, 0x0, 0x0, 0x0, 0x10, 0x7, 0xC7, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x88, 0x3, 0x63, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xCC, 0x1, 0x8, 0xFD, 0xD0, 0xA, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0xCC, 0x1, 0x8, 0xFD, 0xD0, 0xA, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD2, 0x1, 0xA, 0xFD, 0xA8, 0x14, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0x10, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0x54, 0x5, 0x13, 0x7, 0x0, 0x0, 0x0, 0x0, 0x58, 0x5, 0xF, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xAC, 0x2, 0x87, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x8, 0xFD, 0xD0, 0xD1, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x14, 0x0, 0x8, 0xFD, 0xD0, 0xD1, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1A, 0x0, 0xA, 0xFD, 0x8, 0xC7, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x58, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0x9C, 0x3, 0x5B, 0x5, 0x0, 0x0, 0x0, 0x0, 0xA0, 0x3, 0x57, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xD0, 0x1, 0xAB, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5C, 0xFE, 0x8, 0xFD, 0x50, 0x81, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0x5C, 0xFE, 0x8, 0xFD, 0x50, 0x81, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x62, 0xFE, 0xA, 0xFD, 0xE8, 0x61, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0xA0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xE4, 0x1, 0xA3, 0x3, 0x0, 0x0, 0x0, 0x0, 0xE8, 0x1, 0x9F, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xF4, 0x0, 0xCF, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA4, 0xFC, 0x8, 0xFD, 0x50, 0x19, 0x14, 0x0, 0x9, 0x0, 0x38, 0x0, 0xA4, 0xFC, 0x8, 0xFD, 0x50, 0x19, 0x14, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xAA, 0xFC, 0xA, 0xFD, 0x48, 0xE5, 0x13, 0x0, 0x9, 0x0, 0x38, 0x0, 0xE8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xC0, 0x0, 0xEB, 0x1, 0x0, 0x0, 0x0, 0x0, 0xC4, 0x0, 0xE7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x62, 0x0, 0xF3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x91, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x91, 0x0, 0x0, 0x0, 0x48, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x24, 0x1, 0x92, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x92, 0x0, 0x49, 0x0, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x24, 0x1, 0x92, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xFB, 0x8, 0xFD, 0x40, 0x10, 0x1D, 0x0, 0x9, 0x0, 0x38, 0x0, 0x80, 0xFB, 0x8, 0xFD, 0x40, 0x10, 0x1D, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x86, 0xFB, 0xA, 0xFD, 0x88, 0xCE, 0x1C, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xC5, 0x6, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x61, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x62, 0x0, 0x31, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xFA, 0x8, 0xFD, 0x40, 0x60, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC0, 0xFA, 0x8, 0xFD, 0x40, 0x60, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC2, 0xFA, 0xA, 0xFD, 0x68, 0x3F, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x8B, 0x4, 0x0, 0xEC, 0x3, 0x1, 0x0, 0x58, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x50, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0xFC, 0xC7, 0x10, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x10, 0x41, 0x0, 0x0, 0x0, 0x0, 0x57, 0xC, 0x0, 0x0, 0x69, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x77, 0x0, 0x0, 0x0, 0x7C, 0x0, 0x0, 0x0, 0x8C, 0x1, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, 0xA4, 0x51, 0x1, 0x0, 0x58, 0x3, 0x0, 0x0, 0x8, 0xFC, 0x7F, 0x40, 0x64, 0x2A, 0x84, 0x40, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x10, 0x41, 0x57, 0xC, 0x0, 0x0, 0x58, 0xC, 0x0, 0x0, 0xFC, 0xC7, 0x10, 0x41, 0x8A, 0x1, 0x0, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x1, 0xB0, 0xAB, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xD0, 0x1D, 0x3, 0x41, 0x30, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0xA4, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x40, 0x5, 0x0, 0x0, 0x4C, 0x17, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x35, 0x33, 0x32, 0x31, 0x36, 0x68, 0x8, 0x80, 0x40, 0xA8, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x58, 0x7, 0x80, 0x40, 0xA0, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x40, 0xC0, 0x4C, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x9C, 0xFC, 0x7F, 0x40, 0x58, 0x7, 0x80, 0x40, 0xC4, 0x15, 0x1, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xE0, 0xCF, 0x0, 0x0, 0xE0, 0xCF, 0x0, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xB4, 0x7, 0x80, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xB0, 0x7, 0x80, 0x40, 0x0, 0x0, 0x0, 0x0}, }; unsigned char bps_settings[4][684] = { +{ /* placeholder */ }, { /* placeholder */ }, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 84de19aa93f..034ffde0be1 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -20,6 +20,36 @@ #include "system/camerad/cameras/bps_blobs.h" +namespace { + +constexpr int BPS_BLOB_COUNT = sizeof(bps_cfg) / sizeof(bps_cfg[0]); +static_assert(BPS_BLOB_COUNT == sizeof(bps_settings) / sizeof(bps_settings[0])); +static_assert(BPS_BLOB_COUNT == sizeof(bps_striping_output) / sizeof(bps_striping_output[0])); + +uint32_t bps_blob_sensor_id(int idx) { + return bps_cfg[idx][12] | + (bps_cfg[idx][13] << 8) | + (bps_cfg[idx][14] << 16) | + (static_cast(bps_cfg[idx][15]) << 24); +} + +bool bps_blob_present(int idx) { + return bps_cfg[idx][0] != 0; +} + +int get_bps_blob_index(const SensorInfo *sensor) { + const int sensor_idx = sensor->num(); + for (int i = 0; i < BPS_BLOB_COUNT; ++i) { + if (bps_blob_present(i) && bps_blob_sensor_id(i) == static_cast(sensor_idx)) { + return i; + } + } + return sensor_idx; +} + +} // namespace + + // ************** low level camera helpers **************** int do_cam_control(int fd, int op_code, void *handle, int size) { @@ -1167,12 +1197,17 @@ void SpectraCamera::configICP() { */ int cfg_handle; + const int bps_idx = get_bps_blob_index(sensor); + assert(bps_idx >= 0 && bps_idx < BPS_BLOB_COUNT); + if (bps_idx != sensor->num()) { + LOGW("camera %d sensor %d: using BPS blob index %d", cc.camera_num, sensor->num(), bps_idx); + } uint32_t cfg_size = sizeof(bps_cfg[0]) / sizeof(bps_cfg[0][0]); void *cfg = alloc_w_mmu_hdl(m->video0_fd, cfg_size, (uint32_t*)&cfg_handle, 0x1, CAM_MEM_FLAG_HW_READ_WRITE | CAM_MEM_FLAG_UMD_ACCESS | CAM_MEM_FLAG_HW_SHARED_ACCESS, m->icp_device_iommu); - memcpy(cfg, bps_cfg[sensor->num()], cfg_size); + memcpy(cfg, bps_cfg[bps_idx], cfg_size); struct cam_icp_acquire_dev_info icp_info = { .scratch_mem_size = 0x0, @@ -1207,7 +1242,7 @@ void SpectraCamera::configICP() { // BPSIQSettings struct uint32_t settings_size = sizeof(bps_settings[0]) / sizeof(bps_settings[0][0]); bps_iq.init(m, settings_size, 0x20, true, m->icp_device_iommu); - memcpy(bps_iq.ptr, bps_settings[sensor->num()], settings_size); + memcpy(bps_iq.ptr, bps_settings[bps_idx], settings_size); // for cdm register writes, just make it bigger than you need bps_cdm_program_array.init(m, 0x1000, 0x20, true, m->icp_device_iommu); @@ -1215,7 +1250,7 @@ void SpectraCamera::configICP() { // striping lib output uint32_t striping_size = sizeof(bps_striping_output[0]) / sizeof(bps_striping_output[0][0]); bps_striping.init(m, striping_size, 0x20, true, m->icp_device_iommu); - memcpy(bps_striping.ptr, bps_striping_output[sensor->num()], striping_size); + memcpy(bps_striping.ptr, bps_striping_output[bps_idx], striping_size); // used internally by the BPS, we just allocate it. // size comes from the BPSStripingLib From b57b10d6057579829df1cce4c568056707802a67 Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 22:00:05 +0900 Subject: [PATCH 21/61] "Fix `get_bps_blob_index` call by using `sensor.get()` for proper pointer handling" --- system/camerad/cameras/spectra.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 034ffde0be1..ba50bc37d39 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -1197,7 +1197,7 @@ void SpectraCamera::configICP() { */ int cfg_handle; - const int bps_idx = get_bps_blob_index(sensor); + const int bps_idx = get_bps_blob_index(sensor.get()); assert(bps_idx >= 0 && bps_idx < BPS_BLOB_COUNT); if (bps_idx != sensor->num()) { LOGW("camera %d sensor %d: using BPS blob index %d", cc.camera_num, sensor->num(), bps_idx); From 99671488cfe2a7bc34d922ea822f9dc40dfc72d5 Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 22:28:51 +0900 Subject: [PATCH 22/61] "Add warning and enforce IFE path for AR0231 to prevent CAM-ICP/BPS recovery issues" --- system/camerad/cameras/spectra.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index ba50bc37d39..2eefa4a4851 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -310,6 +310,12 @@ void SpectraCamera::camera_open(VisionIpcServer *v) { if (!enabled) return; + if (sensor->image_sensor == cereal::FrameData::ImageSensor::AR0231 && + cc.output_type == ISP_BPS_PROCESSED) { + LOGW("AR0231 camera %d: using IFE path to avoid CAM-ICP/BPS recovery", cc.camera_num); + cc.output_type = ISP_IFE_PROCESSED; + } + buf.out_img_width = sensor->frame_width / sensor->out_scale; buf.out_img_height = (sensor->hdr_offset > 0 ? (sensor->frame_height - sensor->hdr_offset) / 2 : sensor->frame_height) / sensor->out_scale; From cd1c82e7fb6176aee5f35a0f767e03d95a2fcb45 Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 23:05:06 +0900 Subject: [PATCH 23/61] Add helper function for BPS blob index selection and update related logic --- system/camerad/cameras/spectra.cc | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 84de19aa93f..b3ab17637b1 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -22,6 +22,13 @@ // ************** low level camera helpers **************** +int get_bps_blob_index(const SensorInfo *sensor) { + if (sensor->image_sensor == cereal::FrameData::ImageSensor::AR0231) { + return 2; + } + return sensor->num(); +} + int do_cam_control(int fd, int op_code, void *handle, int size) { struct cam_control camcontrol = {0}; camcontrol.op_code = op_code; @@ -1168,11 +1175,13 @@ void SpectraCamera::configICP() { int cfg_handle; + const int bps_idx = get_bps_blob_index(sensor.get()); + LOGW("camera %d sensor %d: using BPS blob index %d", cc.camera_num, sensor->num(), bps_idx); uint32_t cfg_size = sizeof(bps_cfg[0]) / sizeof(bps_cfg[0][0]); void *cfg = alloc_w_mmu_hdl(m->video0_fd, cfg_size, (uint32_t*)&cfg_handle, 0x1, CAM_MEM_FLAG_HW_READ_WRITE | CAM_MEM_FLAG_UMD_ACCESS | CAM_MEM_FLAG_HW_SHARED_ACCESS, m->icp_device_iommu); - memcpy(cfg, bps_cfg[sensor->num()], cfg_size); + memcpy(cfg, bps_cfg[bps_idx], cfg_size); struct cam_icp_acquire_dev_info icp_info = { .scratch_mem_size = 0x0, @@ -1207,7 +1216,7 @@ void SpectraCamera::configICP() { // BPSIQSettings struct uint32_t settings_size = sizeof(bps_settings[0]) / sizeof(bps_settings[0][0]); bps_iq.init(m, settings_size, 0x20, true, m->icp_device_iommu); - memcpy(bps_iq.ptr, bps_settings[sensor->num()], settings_size); + memcpy(bps_iq.ptr, bps_settings[bps_idx], settings_size); // for cdm register writes, just make it bigger than you need bps_cdm_program_array.init(m, 0x1000, 0x20, true, m->icp_device_iommu); @@ -1215,7 +1224,7 @@ void SpectraCamera::configICP() { // striping lib output uint32_t striping_size = sizeof(bps_striping_output[0]) / sizeof(bps_striping_output[0][0]); bps_striping.init(m, striping_size, 0x20, true, m->icp_device_iommu); - memcpy(bps_striping.ptr, bps_striping_output[sensor->num()], striping_size); + memcpy(bps_striping.ptr, bps_striping_output[bps_idx], striping_size); // used internally by the BPS, we just allocate it. // size comes from the BPSStripingLib From caaef6726becc6c305c0564598218542558c958d Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 23:12:50 +0900 Subject: [PATCH 24/61] Improve first frame sync logic for Spectra camera and add detailed debug logs --- system/camerad/cameras/spectra.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index b3ab17637b1..5d3c728ce3e 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -1523,7 +1523,8 @@ bool SpectraCamera::waitForFrameReady(uint64_t request_id) { } bool SpectraCamera::processFrame(int buf_idx, uint64_t request_id, uint64_t frame_id_raw, uint64_t timestamp) { - if (!syncFirstFrame(cc.camera_num, request_id, frame_id_raw, timestamp, cc.staggered_sof)) { + const bool staggered_sof = cc.staggered_sof && sensor->image_sensor != cereal::FrameData::ImageSensor::AR0231; + if (!syncFirstFrame(cc.camera_num, request_id, frame_id_raw, timestamp, staggered_sof)) { return false; } @@ -1580,7 +1581,12 @@ bool SpectraCamera::syncFirstFrame(int camera_id, uint64_t request_id, uint64_t // Timeout in case the timestamps never line up if (raw_id > 40) { - LOGE("camera first frame sync timed out"); + LOGE("camera first frame sync timed out: camera %d request %lu raw_id %lu timestamp %lu cams %zu/%d", + camera_id, request_id, raw_id, timestamp, camera_sync_data.size(), enabled_camera_count); + for (const auto &[cam, sync_data] : camera_sync_data) { + LOGE("camera %d first frame sync data: frame_id_offset %lu timestamp %lu staggered %d", + cam, sync_data.frame_id_offset, sync_data.timestamp, sync_data.staggered); + } first_frame_synced = true; } From 889fe3a3a7281ccee3c4fe254c0ea2a332297eaf Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 23:47:31 +0900 Subject: [PATCH 25/61] Revert ""Add warning and enforce IFE path for AR0231 to prevent CAM-ICP/BPS recovery issues"" This reverts commit 99671488cfe2a7bc34d922ea822f9dc40dfc72d5. --- system/camerad/cameras/spectra.cc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 2eefa4a4851..ba50bc37d39 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -310,12 +310,6 @@ void SpectraCamera::camera_open(VisionIpcServer *v) { if (!enabled) return; - if (sensor->image_sensor == cereal::FrameData::ImageSensor::AR0231 && - cc.output_type == ISP_BPS_PROCESSED) { - LOGW("AR0231 camera %d: using IFE path to avoid CAM-ICP/BPS recovery", cc.camera_num); - cc.output_type = ISP_IFE_PROCESSED; - } - buf.out_img_width = sensor->frame_width / sensor->out_scale; buf.out_img_height = (sensor->hdr_offset > 0 ? (sensor->frame_height - sensor->hdr_offset) / 2 : sensor->frame_height) / sensor->out_scale; From 343b6d23ffa73f5fb368e66a270d5ec5f4c89d91 Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 23:47:47 +0900 Subject: [PATCH 26/61] Revert ""Fix `get_bps_blob_index` call by using `sensor.get()` for proper pointer handling"" This reverts commit b57b10d6057579829df1cce4c568056707802a67. --- system/camerad/cameras/spectra.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index ba50bc37d39..034ffde0be1 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -1197,7 +1197,7 @@ void SpectraCamera::configICP() { */ int cfg_handle; - const int bps_idx = get_bps_blob_index(sensor.get()); + const int bps_idx = get_bps_blob_index(sensor); assert(bps_idx >= 0 && bps_idx < BPS_BLOB_COUNT); if (bps_idx != sensor->num()) { LOGW("camera %d sensor %d: using BPS blob index %d", cc.camera_num, sensor->num(), bps_idx); From 3aaa4b5d73c114d11d35a7edefe2b7bab5788c7e Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 23:47:50 +0900 Subject: [PATCH 27/61] Revert ""Add logic for BPS blob index mapping and warnings for mismatched sensor indices"" This reverts commit 2cfa49c0cbcc7833e849057659e1a77995361a89. --- system/camerad/cameras/bps_blobs.h | 3 --- system/camerad/cameras/spectra.cc | 41 +++--------------------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/system/camerad/cameras/bps_blobs.h b/system/camerad/cameras/bps_blobs.h index ee31698e7f0..ab8ac7545f3 100644 --- a/system/camerad/cameras/bps_blobs.h +++ b/system/camerad/cameras/bps_blobs.h @@ -8,21 +8,18 @@ unsigned char bps_cfg[4][768] = { -{ /* placeholder */ }, { /* placeholder */ }, {0x3, 0x0, 0x0, 0x0, 0x88, 0x7, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x50, 0xB, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x88, 0x7, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0xC0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x60, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, {0x3, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xF, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x40, 0x5, 0x0, 0x0, 0xF8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x5, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x80, 0x5, 0x0, 0x0, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, }; unsigned char bps_striping_output[4][0xc58] = { -{ /* placeholder */ }, { /* placeholder */ }, {0x5, 0x0, 0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x5B, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x5B, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x4, 0x5C, 0x2, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x70, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x89, 0x23, 0x0, 0x1, 0x0, 0x0, 0x0, 0xCC, 0x5, 0x87, 0x7, 0x0, 0x0, 0x0, 0x0, 0xD0, 0x5, 0x87, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x2, 0xA4, 0xFD, 0x50, 0xB1, 0x9, 0x0, 0x8, 0x0, 0x33, 0x0, 0x8, 0x2, 0xA4, 0xFD, 0x50, 0xB1, 0x9, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x2, 0xA6, 0xFD, 0x68, 0xC0, 0x9, 0x0, 0x8, 0x0, 0x34, 0x0, 0xD0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0x14, 0x4, 0xD3, 0x5, 0x0, 0x0, 0x0, 0x0, 0x18, 0x4, 0xCF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x50, 0x0, 0xA4, 0xFD, 0x10, 0xAA, 0x5, 0x0, 0x8, 0x0, 0x33, 0x0, 0x50, 0x0, 0xA4, 0xFD, 0x10, 0xAA, 0x5, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x0, 0xA6, 0xFD, 0x88, 0xA4, 0x5, 0x0, 0x8, 0x0, 0x34, 0x0, 0x18, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0x5C, 0x2, 0x1B, 0x4, 0x0, 0x0, 0x0, 0x0, 0x60, 0x2, 0x17, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x98, 0xFE, 0xA4, 0xFD, 0x50, 0x8B, 0x7, 0x0, 0x8, 0x0, 0x33, 0x0, 0x98, 0xFE, 0xA4, 0xFD, 0x50, 0x8B, 0x7, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9E, 0xFE, 0xA6, 0xFD, 0x28, 0x71, 0x7, 0x0, 0x8, 0x0, 0x34, 0x0, 0x60, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0xC0, 0x0, 0x63, 0x2, 0x0, 0x0, 0x0, 0x0, 0xC4, 0x0, 0x5F, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x9C, 0x1, 0xCE, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFC, 0xFC, 0xA4, 0xFD, 0x20, 0xA9, 0xE, 0x0, 0x8, 0x0, 0x33, 0x0, 0xFC, 0xFC, 0xA4, 0xFD, 0x20, 0xA9, 0xE, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xFD, 0xA6, 0xFD, 0xA8, 0x7B, 0xE, 0x0, 0x8, 0x0, 0x34, 0x0, 0xC4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x98, 0x7, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3C, 0xFC, 0xA4, 0xFD, 0x20, 0xBF, 0x13, 0x0, 0x8, 0x0, 0x33, 0x0, 0x3C, 0xFC, 0xA4, 0xFD, 0x20, 0xBF, 0x13, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3E, 0xFC, 0xA6, 0xFD, 0xA8, 0xA6, 0x13, 0x0, 0x8, 0x0, 0x34, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE0, 0x9C, 0x3, 0x0, 0xEC, 0x3, 0x1, 0x0, 0x5D, 0x59, 0xAB, 0x0, 0xC8, 0xFC, 0xFB, 0x40, 0x3, 0x0, 0x0, 0x0, 0x0, 0xFC, 0x7F, 0x40, 0xAC, 0x1F, 0x84, 0x40, 0x0, 0x20, 0x4E, 0x0, 0xAB, 0xAA, 0xAA, 0xAA, 0x40, 0xD9, 0xFB, 0x40, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC8, 0xED, 0x80, 0x40, 0x11, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x9F, 0x9, 0x0, 0x0, 0xB8, 0x2B, 0x6B, 0x15, 0xC0, 0x2A, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFC, 0x7F, 0x40, 0x0, 0x5, 0x1, 0x0, 0x8, 0xFC, 0x7F, 0x40, 0x74, 0xFC, 0x7F, 0x40, 0x5C, 0xFC, 0x7F, 0x40, 0xCC, 0xA8, 0x81, 0x40, 0x8, 0xFC, 0x7F, 0x40, 0x64, 0x2A, 0x84, 0x40, 0x3, 0x0, 0x0, 0x0, 0x30, 0xEE, 0x80, 0x40, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x8, 0x29, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x2A, 0x84, 0x40, 0x8, 0x29, 0x84, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x9, 0x0, 0x0, 0xF8, 0x22, 0xFC, 0x40, 0xB8, 0x16, 0x80, 0x40, 0xD0, 0x34, 0x84, 0x40, 0x1, 0xB0, 0xAB, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xD0, 0x1D, 0x3, 0x41, 0x30, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0xA4, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0xC, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x34, 0x31, 0x32, 0x31, 0x36, 0x30, 0xEE, 0x80, 0x40, 0xA8, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x58, 0x7, 0x80, 0x40, 0x4C, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x40, 0xC0, 0x4C, 0x0, 0x41, 0x4C, 0x0, 0x0, 0x0, 0x9C, 0xFC, 0x7F, 0x40, 0x78, 0x22, 0x1, 0x0, 0xC4, 0x15, 0x1, 0x0, 0xC0, 0x22, 0x1, 0x0, 0x0, 0xA1, 0x0, 0x0, 0x0, 0xA1, 0x0, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xB4, 0x7, 0x80, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xB0, 0x7, 0x80, 0x40, 0x0, 0x0, 0x0, 0x0}, {0x7, 0x0, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x7B, 0x1, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF0, 0x5, 0xF8, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF8, 0x2, 0x7C, 0x1, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF0, 0x5, 0xF8, 0x2, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x70, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x3, 0x0, 0x88, 0x7, 0xB8, 0x4, 0x14, 0x0, 0x88, 0x7, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x74, 0x16, 0x80, 0x40, 0x52, 0x5, 0x1, 0x0, 0x1A, 0x3, 0x0, 0x0, 0xC4, 0xED, 0x80, 0x40, 0x98, 0x1, 0x0, 0x0, 0x11, 0x7B, 0x9C, 0x7C, 0x11, 0x7B, 0x9C, 0x7C, 0x14, 0x69, 0x69, 0xD, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE8, 0x8A, 0xFC, 0x40, 0xB2, 0x4, 0x1, 0x0, 0x11, 0x7B, 0x9C, 0x7C, 0xBC, 0x3, 0x1, 0x0, 0xD8, 0xE3, 0xE4, 0x3, 0xC8, 0xFC, 0xFB, 0x40, 0x3, 0x0, 0x0, 0x0, 0x60, 0xF2, 0x7F, 0x40, 0x0, 0x58, 0x3E, 0x0, 0x1, 0x20, 0x4E, 0x0, 0xC4, 0x8, 0x7F, 0xA, 0x0, 0x0, 0x0, 0x0, 0xC8, 0x8, 0x7F, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x64, 0x4, 0x3F, 0x5, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x84, 0x3, 0x8, 0xFD, 0x50, 0x2C, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0x84, 0x3, 0x8, 0xFD, 0x50, 0x2C, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8A, 0x3, 0xA, 0xFD, 0xC8, 0x4A, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC8, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xC, 0x7, 0xCB, 0x8, 0x0, 0x0, 0x0, 0x0, 0x10, 0x7, 0xC7, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x88, 0x3, 0x63, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xCC, 0x1, 0x8, 0xFD, 0xD0, 0xA, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0xCC, 0x1, 0x8, 0xFD, 0xD0, 0xA, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD2, 0x1, 0xA, 0xFD, 0xA8, 0x14, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0x10, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0x54, 0x5, 0x13, 0x7, 0x0, 0x0, 0x0, 0x0, 0x58, 0x5, 0xF, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xAC, 0x2, 0x87, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x8, 0xFD, 0xD0, 0xD1, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x14, 0x0, 0x8, 0xFD, 0xD0, 0xD1, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1A, 0x0, 0xA, 0xFD, 0x8, 0xC7, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x58, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0x9C, 0x3, 0x5B, 0x5, 0x0, 0x0, 0x0, 0x0, 0xA0, 0x3, 0x57, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xD0, 0x1, 0xAB, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5C, 0xFE, 0x8, 0xFD, 0x50, 0x81, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0x5C, 0xFE, 0x8, 0xFD, 0x50, 0x81, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x62, 0xFE, 0xA, 0xFD, 0xE8, 0x61, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0xA0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xE4, 0x1, 0xA3, 0x3, 0x0, 0x0, 0x0, 0x0, 0xE8, 0x1, 0x9F, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xF4, 0x0, 0xCF, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA4, 0xFC, 0x8, 0xFD, 0x50, 0x19, 0x14, 0x0, 0x9, 0x0, 0x38, 0x0, 0xA4, 0xFC, 0x8, 0xFD, 0x50, 0x19, 0x14, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xAA, 0xFC, 0xA, 0xFD, 0x48, 0xE5, 0x13, 0x0, 0x9, 0x0, 0x38, 0x0, 0xE8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xC0, 0x0, 0xEB, 0x1, 0x0, 0x0, 0x0, 0x0, 0xC4, 0x0, 0xE7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x62, 0x0, 0xF3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x91, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x91, 0x0, 0x0, 0x0, 0x48, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x24, 0x1, 0x92, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x92, 0x0, 0x49, 0x0, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x24, 0x1, 0x92, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xFB, 0x8, 0xFD, 0x40, 0x10, 0x1D, 0x0, 0x9, 0x0, 0x38, 0x0, 0x80, 0xFB, 0x8, 0xFD, 0x40, 0x10, 0x1D, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x86, 0xFB, 0xA, 0xFD, 0x88, 0xCE, 0x1C, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xC5, 0x6, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x61, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x62, 0x0, 0x31, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xFA, 0x8, 0xFD, 0x40, 0x60, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC0, 0xFA, 0x8, 0xFD, 0x40, 0x60, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC2, 0xFA, 0xA, 0xFD, 0x68, 0x3F, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x8B, 0x4, 0x0, 0xEC, 0x3, 0x1, 0x0, 0x58, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x50, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0xFC, 0xC7, 0x10, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x10, 0x41, 0x0, 0x0, 0x0, 0x0, 0x57, 0xC, 0x0, 0x0, 0x69, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x77, 0x0, 0x0, 0x0, 0x7C, 0x0, 0x0, 0x0, 0x8C, 0x1, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, 0xA4, 0x51, 0x1, 0x0, 0x58, 0x3, 0x0, 0x0, 0x8, 0xFC, 0x7F, 0x40, 0x64, 0x2A, 0x84, 0x40, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x10, 0x41, 0x57, 0xC, 0x0, 0x0, 0x58, 0xC, 0x0, 0x0, 0xFC, 0xC7, 0x10, 0x41, 0x8A, 0x1, 0x0, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x1, 0xB0, 0xAB, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xD0, 0x1D, 0x3, 0x41, 0x30, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0xA4, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x40, 0x5, 0x0, 0x0, 0x4C, 0x17, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x35, 0x33, 0x32, 0x31, 0x36, 0x68, 0x8, 0x80, 0x40, 0xA8, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x58, 0x7, 0x80, 0x40, 0xA0, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x40, 0xC0, 0x4C, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x9C, 0xFC, 0x7F, 0x40, 0x58, 0x7, 0x80, 0x40, 0xC4, 0x15, 0x1, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xE0, 0xCF, 0x0, 0x0, 0xE0, 0xCF, 0x0, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xB4, 0x7, 0x80, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xB0, 0x7, 0x80, 0x40, 0x0, 0x0, 0x0, 0x0}, }; unsigned char bps_settings[4][684] = { -{ /* placeholder */ }, { /* placeholder */ }, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 034ffde0be1..84de19aa93f 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -20,36 +20,6 @@ #include "system/camerad/cameras/bps_blobs.h" -namespace { - -constexpr int BPS_BLOB_COUNT = sizeof(bps_cfg) / sizeof(bps_cfg[0]); -static_assert(BPS_BLOB_COUNT == sizeof(bps_settings) / sizeof(bps_settings[0])); -static_assert(BPS_BLOB_COUNT == sizeof(bps_striping_output) / sizeof(bps_striping_output[0])); - -uint32_t bps_blob_sensor_id(int idx) { - return bps_cfg[idx][12] | - (bps_cfg[idx][13] << 8) | - (bps_cfg[idx][14] << 16) | - (static_cast(bps_cfg[idx][15]) << 24); -} - -bool bps_blob_present(int idx) { - return bps_cfg[idx][0] != 0; -} - -int get_bps_blob_index(const SensorInfo *sensor) { - const int sensor_idx = sensor->num(); - for (int i = 0; i < BPS_BLOB_COUNT; ++i) { - if (bps_blob_present(i) && bps_blob_sensor_id(i) == static_cast(sensor_idx)) { - return i; - } - } - return sensor_idx; -} - -} // namespace - - // ************** low level camera helpers **************** int do_cam_control(int fd, int op_code, void *handle, int size) { @@ -1197,17 +1167,12 @@ void SpectraCamera::configICP() { */ int cfg_handle; - const int bps_idx = get_bps_blob_index(sensor); - assert(bps_idx >= 0 && bps_idx < BPS_BLOB_COUNT); - if (bps_idx != sensor->num()) { - LOGW("camera %d sensor %d: using BPS blob index %d", cc.camera_num, sensor->num(), bps_idx); - } uint32_t cfg_size = sizeof(bps_cfg[0]) / sizeof(bps_cfg[0][0]); void *cfg = alloc_w_mmu_hdl(m->video0_fd, cfg_size, (uint32_t*)&cfg_handle, 0x1, CAM_MEM_FLAG_HW_READ_WRITE | CAM_MEM_FLAG_UMD_ACCESS | CAM_MEM_FLAG_HW_SHARED_ACCESS, m->icp_device_iommu); - memcpy(cfg, bps_cfg[bps_idx], cfg_size); + memcpy(cfg, bps_cfg[sensor->num()], cfg_size); struct cam_icp_acquire_dev_info icp_info = { .scratch_mem_size = 0x0, @@ -1242,7 +1207,7 @@ void SpectraCamera::configICP() { // BPSIQSettings struct uint32_t settings_size = sizeof(bps_settings[0]) / sizeof(bps_settings[0][0]); bps_iq.init(m, settings_size, 0x20, true, m->icp_device_iommu); - memcpy(bps_iq.ptr, bps_settings[bps_idx], settings_size); + memcpy(bps_iq.ptr, bps_settings[sensor->num()], settings_size); // for cdm register writes, just make it bigger than you need bps_cdm_program_array.init(m, 0x1000, 0x20, true, m->icp_device_iommu); @@ -1250,7 +1215,7 @@ void SpectraCamera::configICP() { // striping lib output uint32_t striping_size = sizeof(bps_striping_output[0]) / sizeof(bps_striping_output[0][0]); bps_striping.init(m, striping_size, 0x20, true, m->icp_device_iommu); - memcpy(bps_striping.ptr, bps_striping_output[bps_idx], striping_size); + memcpy(bps_striping.ptr, bps_striping_output[sensor->num()], striping_size); // used internally by the BPS, we just allocate it. // size comes from the BPSStripingLib From 42c36116c6aa3f7fffca874c9b19fc2f7be4cd67 Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 23:47:54 +0900 Subject: [PATCH 28/61] Revert ""Remove redundant AR0231 IFE path handling and placeholder entries in BPS files"" This reverts commit d7a9fbbc02ebb71fc101c55a755b7328909d03dd. --- system/camerad/cameras/bps_blobs.h | 3 +++ system/camerad/cameras/spectra.cc | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/system/camerad/cameras/bps_blobs.h b/system/camerad/cameras/bps_blobs.h index ab8ac7545f3..ee31698e7f0 100644 --- a/system/camerad/cameras/bps_blobs.h +++ b/system/camerad/cameras/bps_blobs.h @@ -8,18 +8,21 @@ unsigned char bps_cfg[4][768] = { +{ /* placeholder */ }, { /* placeholder */ }, {0x3, 0x0, 0x0, 0x0, 0x88, 0x7, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x50, 0xB, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x88, 0x7, 0x0, 0x0, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0xC0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x60, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, {0x3, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xF, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0xF0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x80, 0xA, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x40, 0x5, 0x0, 0x0, 0xF8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x5, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x80, 0x5, 0x0, 0x0, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, }; unsigned char bps_striping_output[4][0xc58] = { +{ /* placeholder */ }, { /* placeholder */ }, {0x5, 0x0, 0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x5B, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x5B, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x4, 0x5C, 0x2, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x70, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x89, 0x23, 0x0, 0x1, 0x0, 0x0, 0x0, 0xCC, 0x5, 0x87, 0x7, 0x0, 0x0, 0x0, 0x0, 0xD0, 0x5, 0x87, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x2, 0xA4, 0xFD, 0x50, 0xB1, 0x9, 0x0, 0x8, 0x0, 0x33, 0x0, 0x8, 0x2, 0xA4, 0xFD, 0x50, 0xB1, 0x9, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x2, 0xA6, 0xFD, 0x68, 0xC0, 0x9, 0x0, 0x8, 0x0, 0x34, 0x0, 0xD0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0x14, 0x4, 0xD3, 0x5, 0x0, 0x0, 0x0, 0x0, 0x18, 0x4, 0xCF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x50, 0x0, 0xA4, 0xFD, 0x10, 0xAA, 0x5, 0x0, 0x8, 0x0, 0x33, 0x0, 0x50, 0x0, 0xA4, 0xFD, 0x10, 0xAA, 0x5, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x0, 0xA6, 0xFD, 0x88, 0xA4, 0x5, 0x0, 0x8, 0x0, 0x34, 0x0, 0x18, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0x5C, 0x2, 0x1B, 0x4, 0x0, 0x0, 0x0, 0x0, 0x60, 0x2, 0x17, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x98, 0xFE, 0xA4, 0xFD, 0x50, 0x8B, 0x7, 0x0, 0x8, 0x0, 0x33, 0x0, 0x98, 0xFE, 0xA4, 0xFD, 0x50, 0x8B, 0x7, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9E, 0xFE, 0xA6, 0xFD, 0x28, 0x71, 0x7, 0x0, 0x8, 0x0, 0x34, 0x0, 0x60, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1C, 0x8, 0x0, 0xC0, 0x0, 0x63, 0x2, 0x0, 0x0, 0x0, 0x0, 0xC4, 0x0, 0x5F, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x9B, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x9C, 0x1, 0xCE, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFC, 0xFC, 0xA4, 0xFD, 0x20, 0xA9, 0xE, 0x0, 0x8, 0x0, 0x33, 0x0, 0xFC, 0xFC, 0xA4, 0xFD, 0x20, 0xA9, 0xE, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xFD, 0xA6, 0xFD, 0xA8, 0x7B, 0xE, 0x0, 0x8, 0x0, 0x34, 0x0, 0xC4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x98, 0x7, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3C, 0xFC, 0xA4, 0xFD, 0x20, 0xBF, 0x13, 0x0, 0x8, 0x0, 0x33, 0x0, 0x3C, 0xFC, 0xA4, 0xFD, 0x20, 0xBF, 0x13, 0x0, 0x8, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3E, 0xFC, 0xA6, 0xFD, 0xA8, 0xA6, 0x13, 0x0, 0x8, 0x0, 0x34, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE0, 0x9C, 0x3, 0x0, 0xEC, 0x3, 0x1, 0x0, 0x5D, 0x59, 0xAB, 0x0, 0xC8, 0xFC, 0xFB, 0x40, 0x3, 0x0, 0x0, 0x0, 0x0, 0xFC, 0x7F, 0x40, 0xAC, 0x1F, 0x84, 0x40, 0x0, 0x20, 0x4E, 0x0, 0xAB, 0xAA, 0xAA, 0xAA, 0x40, 0xD9, 0xFB, 0x40, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC8, 0xED, 0x80, 0x40, 0x11, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x9F, 0x9, 0x0, 0x0, 0xB8, 0x2B, 0x6B, 0x15, 0xC0, 0x2A, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFC, 0x7F, 0x40, 0x0, 0x5, 0x1, 0x0, 0x8, 0xFC, 0x7F, 0x40, 0x74, 0xFC, 0x7F, 0x40, 0x5C, 0xFC, 0x7F, 0x40, 0xCC, 0xA8, 0x81, 0x40, 0x8, 0xFC, 0x7F, 0x40, 0x64, 0x2A, 0x84, 0x40, 0x3, 0x0, 0x0, 0x0, 0x30, 0xEE, 0x80, 0x40, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x8, 0x29, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x2A, 0x84, 0x40, 0x8, 0x29, 0x84, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x9, 0x0, 0x0, 0xF8, 0x22, 0xFC, 0x40, 0xB8, 0x16, 0x80, 0x40, 0xD0, 0x34, 0x84, 0x40, 0x1, 0xB0, 0xAB, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xD0, 0x1D, 0x3, 0x41, 0x30, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0xA4, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0xC, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x34, 0x31, 0x32, 0x31, 0x36, 0x30, 0xEE, 0x80, 0x40, 0xA8, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x58, 0x7, 0x80, 0x40, 0x4C, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x40, 0xC0, 0x4C, 0x0, 0x41, 0x4C, 0x0, 0x0, 0x0, 0x9C, 0xFC, 0x7F, 0x40, 0x78, 0x22, 0x1, 0x0, 0xC4, 0x15, 0x1, 0x0, 0xC0, 0x22, 0x1, 0x0, 0x0, 0xA1, 0x0, 0x0, 0x0, 0xA1, 0x0, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xB4, 0x7, 0x80, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xB0, 0x7, 0x80, 0x40, 0x0, 0x0, 0x0, 0x0}, {0x7, 0x0, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF7, 0x2, 0x0, 0x0, 0x7B, 0x1, 0x0, 0x0, 0xEF, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF0, 0x5, 0xF8, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF8, 0x2, 0x7C, 0x1, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xF0, 0x5, 0xF8, 0x2, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x70, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x3, 0x0, 0x88, 0x7, 0xB8, 0x4, 0x14, 0x0, 0x88, 0x7, 0xB8, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x74, 0x16, 0x80, 0x40, 0x52, 0x5, 0x1, 0x0, 0x1A, 0x3, 0x0, 0x0, 0xC4, 0xED, 0x80, 0x40, 0x98, 0x1, 0x0, 0x0, 0x11, 0x7B, 0x9C, 0x7C, 0x11, 0x7B, 0x9C, 0x7C, 0x14, 0x69, 0x69, 0xD, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE8, 0x8A, 0xFC, 0x40, 0xB2, 0x4, 0x1, 0x0, 0x11, 0x7B, 0x9C, 0x7C, 0xBC, 0x3, 0x1, 0x0, 0xD8, 0xE3, 0xE4, 0x3, 0xC8, 0xFC, 0xFB, 0x40, 0x3, 0x0, 0x0, 0x0, 0x60, 0xF2, 0x7F, 0x40, 0x0, 0x58, 0x3E, 0x0, 0x1, 0x20, 0x4E, 0x0, 0xC4, 0x8, 0x7F, 0xA, 0x0, 0x0, 0x0, 0x0, 0xC8, 0x8, 0x7F, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x64, 0x4, 0x3F, 0x5, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x84, 0x3, 0x8, 0xFD, 0x50, 0x2C, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0x84, 0x3, 0x8, 0xFD, 0x50, 0x2C, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8A, 0x3, 0xA, 0xFD, 0xC8, 0x4A, 0x15, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC8, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xC, 0x7, 0xCB, 0x8, 0x0, 0x0, 0x0, 0x0, 0x10, 0x7, 0xC7, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x88, 0x3, 0x63, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xCC, 0x1, 0x8, 0xFD, 0xD0, 0xA, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0xCC, 0x1, 0x8, 0xFD, 0xD0, 0xA, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD2, 0x1, 0xA, 0xFD, 0xA8, 0x14, 0xC, 0x0, 0x9, 0x0, 0x38, 0x0, 0x10, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0x54, 0x5, 0x13, 0x7, 0x0, 0x0, 0x0, 0x0, 0x58, 0x5, 0xF, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xAC, 0x2, 0x87, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x8, 0xFD, 0xD0, 0xD1, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x14, 0x0, 0x8, 0xFD, 0xD0, 0xD1, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1A, 0x0, 0xA, 0xFD, 0x8, 0xC7, 0x8, 0x0, 0x9, 0x0, 0x38, 0x0, 0x58, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0x9C, 0x3, 0x5B, 0x5, 0x0, 0x0, 0x0, 0x0, 0xA0, 0x3, 0x57, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xD0, 0x1, 0xAB, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5C, 0xFE, 0x8, 0xFD, 0x50, 0x81, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0x5C, 0xFE, 0x8, 0xFD, 0x50, 0x81, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x62, 0xFE, 0xA, 0xFD, 0xE8, 0x61, 0xB, 0x0, 0x9, 0x0, 0x38, 0x0, 0xA0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xE4, 0x1, 0xA3, 0x3, 0x0, 0x0, 0x0, 0x0, 0xE8, 0x1, 0x9F, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0xF4, 0x0, 0xCF, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0xB7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xDB, 0x0, 0x0, 0x0, 0x6D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xDC, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xB8, 0x1, 0xDC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xA4, 0xFC, 0x8, 0xFD, 0x50, 0x19, 0x14, 0x0, 0x9, 0x0, 0x38, 0x0, 0xA4, 0xFC, 0x8, 0xFD, 0x50, 0x19, 0x14, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xAA, 0xFC, 0xA, 0xFD, 0x48, 0xE5, 0x13, 0x0, 0x9, 0x0, 0x38, 0x0, 0xE8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x34, 0xA, 0x0, 0xC0, 0x0, 0xEB, 0x1, 0x0, 0x0, 0x0, 0x0, 0xC4, 0x0, 0xE7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x62, 0x0, 0xF3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x91, 0x0, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x23, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x91, 0x0, 0x0, 0x0, 0x48, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x24, 0x1, 0x92, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x92, 0x0, 0x49, 0x0, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x24, 0x1, 0x92, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xFB, 0x8, 0xFD, 0x40, 0x10, 0x1D, 0x0, 0x9, 0x0, 0x38, 0x0, 0x80, 0xFB, 0x8, 0xFD, 0x40, 0x10, 0x1D, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x86, 0xFB, 0xA, 0xFD, 0x88, 0xCE, 0x1C, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xC5, 0x6, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xB, 0x0, 0x0, 0x61, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x62, 0x0, 0x31, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xC4, 0x0, 0x62, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0xFA, 0x8, 0xFD, 0x40, 0x60, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0xC0, 0xFA, 0x8, 0xFD, 0x40, 0x60, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC2, 0xFA, 0xA, 0xFD, 0x68, 0x3F, 0x24, 0x0, 0x9, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x8B, 0x4, 0x0, 0xEC, 0x3, 0x1, 0x0, 0x58, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x50, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0xFC, 0xC7, 0x10, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x10, 0x41, 0x0, 0x0, 0x0, 0x0, 0x57, 0xC, 0x0, 0x0, 0x69, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x6E, 0x0, 0x0, 0x0, 0x77, 0x0, 0x0, 0x0, 0x7C, 0x0, 0x0, 0x0, 0x8C, 0x1, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, 0xA4, 0x51, 0x1, 0x0, 0x58, 0x3, 0x0, 0x0, 0x8, 0xFC, 0x7F, 0x40, 0x64, 0x2A, 0x84, 0x40, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x10, 0x41, 0x57, 0xC, 0x0, 0x0, 0x58, 0xC, 0x0, 0x0, 0xFC, 0xC7, 0x10, 0x41, 0x8A, 0x1, 0x0, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x1, 0xB0, 0xAB, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xD0, 0x1D, 0x3, 0x41, 0x30, 0x40, 0x1, 0x0, 0xD0, 0x34, 0x84, 0x40, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0xA4, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x40, 0x5, 0x0, 0x0, 0x4C, 0x17, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x35, 0x33, 0x32, 0x31, 0x36, 0x68, 0x8, 0x80, 0x40, 0xA8, 0xFC, 0x7F, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x58, 0x7, 0x80, 0x40, 0xA0, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x8C, 0xB, 0x80, 0x40, 0xC0, 0x4C, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x9C, 0xFC, 0x7F, 0x40, 0x58, 0x7, 0x80, 0x40, 0xC4, 0x15, 0x1, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xE0, 0xCF, 0x0, 0x0, 0xE0, 0xCF, 0x0, 0x0, 0xC0, 0x22, 0x1, 0x0, 0xB4, 0x7, 0x80, 0x40, 0xAC, 0x7, 0x80, 0x40, 0xB0, 0x7, 0x80, 0x40, 0x0, 0x0, 0x0, 0x0}, }; unsigned char bps_settings[4][684] = { +{ /* placeholder */ }, { /* placeholder */ }, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 84de19aa93f..502b6a4c70f 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -280,6 +280,12 @@ void SpectraCamera::camera_open(VisionIpcServer *v) { if (!enabled) return; + if (sensor->image_sensor == cereal::FrameData::ImageSensor::AR0231 && + cc.output_type == ISP_BPS_PROCESSED) { + LOGW("AR0231 camera %d: using IFE path to avoid CAM-ICP/BPS recovery", cc.camera_num); + cc.output_type = ISP_IFE_PROCESSED; + } + buf.out_img_width = sensor->frame_width / sensor->out_scale; buf.out_img_height = (sensor->hdr_offset > 0 ? (sensor->frame_height - sensor->hdr_offset) / 2 : sensor->frame_height) / sensor->out_scale; From 3150317532b8f333d32cd405ba0cc84ac5be3270 Mon Sep 17 00:00:00 2001 From: thftgr Date: Tue, 30 Jun 2026 23:48:02 +0900 Subject: [PATCH 29/61] Revert "Use IFE path for AR0231 cameras to avoid CAM-ICP/BPS recovery" This reverts commit 28468bb0e943b2ff5490d36ad747b6f5a1c235df. --- system/camerad/cameras/spectra.cc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 502b6a4c70f..84de19aa93f 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -280,12 +280,6 @@ void SpectraCamera::camera_open(VisionIpcServer *v) { if (!enabled) return; - if (sensor->image_sensor == cereal::FrameData::ImageSensor::AR0231 && - cc.output_type == ISP_BPS_PROCESSED) { - LOGW("AR0231 camera %d: using IFE path to avoid CAM-ICP/BPS recovery", cc.camera_num); - cc.output_type = ISP_IFE_PROCESSED; - } - buf.out_img_width = sensor->frame_width / sensor->out_scale; buf.out_img_height = (sensor->hdr_offset > 0 ? (sensor->frame_height - sensor->hdr_offset) / 2 : sensor->frame_height) / sensor->out_scale; From ae03c4d7c3e6d36fbfe02e6c8391d8074273caf5 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 12:36:52 +0900 Subject: [PATCH 30/61] add yt --- selfdrive/carrot/server/config.py | 2 + selfdrive/carrot/server/features/__init__.py | 2 + .../carrot/server/features/youtube_live.py | 86 +++ .../carrot/server/services/youtube_live.py | 617 ++++++++++++++++++ .../web/css/pages/settings/youtube_live.css | 139 ++++ selfdrive/carrot/web/index.html | 8 +- .../web/js/pages/setting_youtube_live.js | 362 ++++++++++ selfdrive/carrot/web/js/translations/en.js | 34 + selfdrive/carrot/web/js/translations/ko.js | 34 + selfdrive/carrot/web/js/translations/zh.js | 34 + selfdrive/carrot_settings.json | 42 ++ 11 files changed, 1357 insertions(+), 3 deletions(-) create mode 100644 selfdrive/carrot/server/features/youtube_live.py create mode 100644 selfdrive/carrot/server/services/youtube_live.py create mode 100644 selfdrive/carrot/web/css/pages/settings/youtube_live.css create mode 100644 selfdrive/carrot/web/js/pages/setting_youtube_live.js diff --git a/selfdrive/carrot/server/config.py b/selfdrive/carrot/server/config.py index 66223e899a1..4e18ddb1e45 100644 --- a/selfdrive/carrot/server/config.py +++ b/selfdrive/carrot/server/config.py @@ -23,6 +23,8 @@ CARROT_WEB_SETTINGS_PATH = os.path.join(CARROT_STATE_DIR, "web_settings.json") CARROT_SETTING_FAVORITES_PATH = os.path.join(CARROT_STATE_DIR, "setting_favorites.json") CARROT_SETTING_PROFILES_PATH = os.path.join(CARROT_STATE_DIR, "setting_profiles.json") +CARROT_YOUTUBE_LIVE_STATE_PATH = os.path.join(CARROT_STATE_DIR, "youtube_live.json") +CARROT_YOUTUBE_LIVE_SECRET_PATH = os.path.join(CARROT_STATE_DIR, "youtube_live_secret.json") # Dashcam DASHCAM_ROOT = "/data/media/0/realdata" diff --git a/selfdrive/carrot/server/features/__init__.py b/selfdrive/carrot/server/features/__init__.py index 0f83b705f4b..3c1cec5ddd4 100644 --- a/selfdrive/carrot/server/features/__init__.py +++ b/selfdrive/carrot/server/features/__init__.py @@ -19,6 +19,7 @@ vision_test, web_settings, ws, + youtube_live, ) @@ -39,5 +40,6 @@ def register_all(app: web.Application) -> None: dashcam.register(app) screenrecord.register(app) tools.register(app) + youtube_live.register(app) vision_test.register(app) vision_diag.register(app) diff --git a/selfdrive/carrot/server/features/youtube_live.py b/selfdrive/carrot/server/features/youtube_live.py new file mode 100644 index 00000000000..4108bcba42a --- /dev/null +++ b/selfdrive/carrot/server/features/youtube_live.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import json + +from aiohttp import web + +from ..services.youtube_live import YouTubeLiveService + + +YOUTUBE_LIVE_APP_KEY = "youtube_live_service" + + +def _service(request: web.Request) -> YouTubeLiveService: + service = request.app.get(YOUTUBE_LIVE_APP_KEY) + if not isinstance(service, YouTubeLiveService): + raise web.HTTPServiceUnavailable(text="youtube live service unavailable") + return service + + +async def youtube_live_context(app: web.Application): + service = YouTubeLiveService() + app[YOUTUBE_LIVE_APP_KEY] = service + await service.start() + try: + yield + finally: + await service.stop() + app.pop(YOUTUBE_LIVE_APP_KEY, None) + + +async def api_status(request: web.Request) -> web.Response: + return web.json_response({"ok": True, **_service(request).status()}) + + +async def api_set_stream_key(request: web.Request) -> web.Response: + try: + body = await request.json() + except Exception: + return web.json_response({"ok": False, "error": "invalid json"}, status=400) + key = body.get("stream_key", body.get("key", "")) + try: + status = _service(request).set_stream_key(str(key or "")) + except ValueError as exc: + return web.json_response({"ok": False, "error": str(exc)}, status=400) + return web.json_response({"ok": True, **status}) + + +async def api_clear_stream_key(request: web.Request) -> web.Response: + return web.json_response({"ok": True, **_service(request).clear_stream_key()}) + + +async def api_stop(request: web.Request) -> web.Response: + status = await _service(request).stop_stream() + return web.json_response({"ok": True, **status}) + + +async def api_test(request: web.Request) -> web.Response: + result = _service(request).test_config() + return web.json_response(result, status=200 if result.get("ok") else 409) + + +async def api_diagnostics(request: web.Request) -> web.Response: + return web.json_response({"ok": True, "diagnostics": _service(request).diagnostics()}) + + +async def api_download_diagnostics(request: web.Request) -> web.Response: + body = json.dumps(_service(request).diagnostics(), ensure_ascii=False, indent=2, sort_keys=True) + return web.Response( + text=body, + content_type="application/json", + headers={ + "Cache-Control": "no-store", + "Content-Disposition": "attachment; filename=carrot-youtube-live-diagnostics.json", + }, + ) + + +def register(app: web.Application) -> None: + app.cleanup_ctx.append(youtube_live_context) + app.router.add_get("/api/youtube_live/status", api_status) + app.router.add_get("/api/youtube_live/diagnostics", api_diagnostics) + app.router.add_get("/api/youtube_live/diagnostics/download", api_download_diagnostics) + app.router.add_post("/api/youtube_live/stream_key", api_set_stream_key) + app.router.add_delete("/api/youtube_live/stream_key", api_clear_stream_key) + app.router.add_post("/api/youtube_live/stop", api_stop) + app.router.add_post("/api/youtube_live/test", api_test) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py new file mode 100644 index 00000000000..a1d13928d2b --- /dev/null +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -0,0 +1,617 @@ +from __future__ import annotations + +import asyncio +import json +import os +import shutil +import time +from collections import deque +from pathlib import Path +from typing import Any + +from ..config import CARROT_YOUTUBE_LIVE_SECRET_PATH, CARROT_YOUTUBE_LIVE_STATE_PATH + + +YOUTUBE_LIVE_PARAM = "CarrotYouTubeLive" +YOUTUBE_QUALITY_PARAM = "CarrotYouTubeQuality" +PHASE1_SOURCE_SERVICE = "qRoadEncodeData" +PHASE1_QUALITY = "standard" +YOUTUBE_RTMPS_BASE = "rtmps://a.rtmps.youtube.com/live2" +NO_FRAME_STOP_SECONDS = 8.0 +START_BACKOFF_SECONDS = 5.0 +STATUS_WRITE_MIN_INTERVAL = 1.0 +BACKOFF_BASE_SECONDS = 3.0 +BACKOFF_MAX_SECONDS = 60.0 + +_PROCESS_MATCHES = { + "carrot_cluster": "selfdrive.carrot.cluster_autorun", + "webrtcd": "system.webrtc.webrtcd", + "stream_encoderd": "system/loggerd/encoderd\x00--stream", +} + + +def _now() -> float: + return time.time() + + +def _read_json(path: Path) -> dict[str, Any]: + try: + raw = json.loads(path.read_text(encoding="utf-8")) + return raw if isinstance(raw, dict) else {} + except Exception: + return {} + + +def _write_json_atomic(path: Path, payload: dict[str, Any], *, mode: int | None = None) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temp_path = path.with_suffix(path.suffix + ".tmp") + temp_path.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8") + if mode is not None: + try: + os.chmod(temp_path, mode) + except OSError: + pass + temp_path.replace(path) + if mode is not None: + try: + os.chmod(path, mode) + except OSError: + pass + + +def _mask_stream_key(stream_key: str) -> str: + key = stream_key.strip() + if not key: + return "" + if len(key) <= 8: + return "*" * len(key) + return f"{key[:4]}...{key[-4:]}" + + +def _extract_stream_key(value: str) -> str: + raw = str(value or "").strip() + if not raw: + return "" + if raw.startswith("rtmp://") or raw.startswith("rtmps://"): + return raw.rstrip("/").rsplit("/", 1)[-1].strip() + return raw + + +class YouTubeLiveService: + def __init__( + self, + *, + state_path: str = CARROT_YOUTUBE_LIVE_STATE_PATH, + secret_path: str = CARROT_YOUTUBE_LIVE_SECRET_PATH, + ) -> None: + self.state_path = Path(state_path) + self.secret_path = Path(secret_path) + self._task: asyncio.Task | None = None + self._stop_event = asyncio.Event() + self._process: asyncio.subprocess.Process | None = None + self._stderr_task: asyncio.Task | None = None + self._stderr_tail: deque[str] = deque(maxlen=20) + self._messaging: Any | None = None + self._socket: Any | None = None + self._params: Any | None = None + self._last_status_write = 0.0 + self._started_at = 0.0 + self._last_frame_at = 0.0 + self._last_frame_id: int | None = None + self._bytes_sent = 0 + self._session_started_bytes = 0 + self._restart_count = 0 + self._consecutive_failures = 0 + self._next_retry_at = 0.0 + self._last_error = "" + self._state = "disabled" + persisted = _read_json(self.state_path) + try: + self._bytes_sent = int(persisted.get("bytes_sent") or 0) + self._restart_count = int(persisted.get("restart_count") or 0) + except Exception: + pass + + async def start(self) -> None: + if self._task is not None and not self._task.done(): + return + self._stop_event.clear() + self._task = asyncio.create_task(self._run(), name="carrot-youtube-live") + + async def stop(self) -> None: + self._stop_event.set() + task = self._task + if task is not None: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + await self._stop_process() + self._task = None + self._write_status(force=True) + + async def stop_stream(self) -> dict[str, Any]: + await self._stop_process() + self._set_state("disabled") + return self.status() + + def status(self) -> dict[str, Any]: + stream_key = self.get_stream_key() + pid = self._process.pid if self._process and self._process.returncode is None else None + now = _now() + uptime = int(now - self._started_at) if self._started_at and pid else 0 + elapsed = max(1.0, now - self._started_at) if self._started_at and pid else 1.0 + last_frame_age_ms = int((now - self._last_frame_at) * 1000) if self._last_frame_at else None + resource_status = self._resource_status() + warnings = self._warnings(resource_status) + return { + "state": self._state, + "enabled": self._param_enabled(), + "configured": bool(stream_key), + "masked_key": _mask_stream_key(stream_key), + "source": PHASE1_SOURCE_SERVICE, + "quality": PHASE1_QUALITY, + "requested_quality": self._param_int(YOUTUBE_QUALITY_PARAM, 0), + "phase": 1, + "running": bool(pid), + "pid": pid, + "ffmpeg_available": bool(shutil.which("ffmpeg")), + "started_at": self._started_at if pid else 0, + "uptime_sec": uptime, + "bytes_sent": self._bytes_sent, + "total_mb": round(max(0, self._bytes_sent) / (1024 * 1024), 2), + "session_bytes": max(0, self._bytes_sent - self._session_started_bytes), + "session_mb": round(max(0, self._bytes_sent - self._session_started_bytes) / (1024 * 1024), 2), + "estimated_kbps": int(((self._bytes_sent - self._session_started_bytes) * 8 / 1000) / elapsed) if pid else 0, + "restart_count": self._restart_count, + "consecutive_failures": self._consecutive_failures, + "next_retry_at": self._next_retry_at, + "retry_in_sec": max(0, int(self._next_retry_at - now)) if self._next_retry_at else 0, + "last_error": self._last_error, + "last_frame_at": self._last_frame_at, + "last_frame_age_ms": last_frame_age_ms, + "last_frame_id": self._last_frame_id, + "stderr_tail": list(self._stderr_tail)[-5:], + "resource_status": resource_status, + "warnings": warnings, + } + + def test_config(self) -> dict[str, Any]: + stream_key = self.get_stream_key() + ffmpeg_path = shutil.which("ffmpeg") + ok = bool(stream_key and ffmpeg_path) + status = self.status() + return { + "ok": ok, + "configured": bool(stream_key), + "ffmpeg_available": bool(ffmpeg_path), + "ffmpeg_path": ffmpeg_path or "", + "source": PHASE1_SOURCE_SERVICE, + "quality": PHASE1_QUALITY, + "resource_status": status["resource_status"], + "warnings": status["warnings"], + "stderr_tail": list(self._stderr_tail), + "message": "ready" if ok else "missing stream key or ffmpeg", + } + + def diagnostics(self) -> dict[str, Any]: + status = self.status() + stream_key = self.get_stream_key() + return { + "generated_at": _now(), + "status": status, + "config": { + "stream_key_configured": bool(stream_key), + "stream_key_masked": _mask_stream_key(stream_key), + "source": PHASE1_SOURCE_SERVICE, + "quality": PHASE1_QUALITY, + "rtmps_ingest": f"{YOUTUBE_RTMPS_BASE}/{_mask_stream_key(stream_key)}" if stream_key else "", + }, + "params": { + YOUTUBE_LIVE_PARAM: self._param_enabled(), + YOUTUBE_QUALITY_PARAM: self._param_int(YOUTUBE_QUALITY_PARAM, 0), + "ClusterHud": self._param_int("ClusterHud", 0), + "DisableDM": self._param_int("DisableDM", 0), + "IsOnroad": self._param_bool("IsOnroad", False), + }, + "ffmpeg": { + "available": bool(shutil.which("ffmpeg")), + "path": shutil.which("ffmpeg") or "", + "stderr_tail": list(self._stderr_tail), + }, + "processes": self._process_status(), + "state_path": str(self.state_path), + } + + def get_stream_key(self) -> str: + payload = _read_json(self.secret_path) + return str(payload.get("stream_key") or "").strip() + + def set_stream_key(self, value: str) -> dict[str, Any]: + stream_key = _extract_stream_key(value) + if not stream_key: + raise ValueError("stream key is required") + _write_json_atomic(self.secret_path, {"stream_key": stream_key, "updated_at": _now()}, mode=0o600) + self._last_error = "" + self._write_status(force=True) + return self.status() + + def clear_stream_key(self) -> dict[str, Any]: + try: + self.secret_path.unlink() + except FileNotFoundError: + pass + self._write_status(force=True) + return self.status() + + async def _run(self) -> None: + while not self._stop_event.is_set(): + try: + await self._tick() + except asyncio.CancelledError: + raise + except Exception as exc: + self._last_error = str(exc) + self._set_state("error") + await self._stop_process() + await asyncio.sleep(START_BACKOFF_SECONDS) + await asyncio.sleep(0.02 if self._process else 0.25) + + async def _tick(self) -> None: + if not self._param_enabled(): + if self._process is not None: + await self._stop_process() + self._set_state("disabled") + return + + stream_key = self.get_stream_key() + if not stream_key: + await self._stop_process() + self._set_state("needs_setup") + return + + if not shutil.which("ffmpeg"): + await self._stop_process() + self._last_error = "ffmpeg not found" + self._set_state("error") + return + + if self._param_int(YOUTUBE_QUALITY_PARAM, 0) > 0: + await self._stop_process() + self._last_error = "high quality YouTube Live is planned for Phase 3" + self._set_state("error") + return + + if self._next_retry_at and _now() < self._next_retry_at: + self._set_state("backoff") + return + + payload, frame_id = self._recv_payload() + if not payload: + if self._process is not None and self._last_frame_at and _now() - self._last_frame_at > NO_FRAME_STOP_SECONDS: + self._last_error = "no qRoadEncodeData frames" + await self._stop_process() + self._schedule_backoff("frame timeout") + self._set_state("waiting_frame" if self._process is None else "live") + return + + self._last_frame_at = _now() + self._last_frame_id = frame_id + + if self._process is None or self._process.returncode is not None: + await self._start_process(stream_key) + + await self._write_frame(payload) + self._set_state("live") + + def _set_state(self, state: str) -> None: + self._state = state + self._write_status() + + def _write_status(self, *, force: bool = False) -> None: + now = _now() + if not force and now - self._last_status_write < STATUS_WRITE_MIN_INTERVAL: + return + self._last_status_write = now + try: + _write_json_atomic(self.state_path, {"updated_at": now, **self.status()}) + except Exception: + pass + + def _param_enabled(self) -> bool: + try: + params = self._get_params() + return bool(params and params.get_bool(YOUTUBE_LIVE_PARAM)) + except Exception: + return False + + def _param_bool(self, name: str, default: bool = False) -> bool: + try: + params = self._get_params() + if params is None: + return default + if hasattr(params, "get_bool"): + return bool(params.get_bool(name)) + raw = params.get(name) + if isinstance(raw, bytes): + raw = raw.decode("utf-8", errors="replace") + return str(raw).strip() in ("1", "true", "True") + except Exception: + return default + + def _param_int(self, name: str, default: int = 0) -> int: + try: + params = self._get_params() + if params is None: + return default + if hasattr(params, "get_int"): + return int(params.get_int(name)) + raw = params.get(name) + if isinstance(raw, bytes): + raw = raw.decode("utf-8", errors="replace") + return int(str(raw).strip()) + except Exception: + return default + + def _get_params(self) -> Any | None: + if self._params is not None: + return self._params + try: + from openpilot.common.params import Params + self._params = Params() + except Exception: + self._params = None + return self._params + + def _get_messaging(self) -> Any | None: + if self._messaging is not None: + return self._messaging + try: + from cereal import messaging + self._messaging = messaging + except Exception as exc: + self._last_error = f"messaging unavailable: {exc}" + self._messaging = None + return self._messaging + + def _get_socket(self) -> Any | None: + if self._socket is not None: + return self._socket + messaging = self._get_messaging() + if messaging is None: + return None + try: + self._socket = messaging.sub_sock(PHASE1_SOURCE_SERVICE, conflate=True) + except Exception as exc: + self._last_error = f"{PHASE1_SOURCE_SERVICE} socket failed: {exc}" + self._socket = None + return self._socket + + def _recv_payload(self) -> tuple[bytes, int | None]: + messaging = self._get_messaging() + sock = self._get_socket() + if messaging is None or sock is None: + return b"", None + try: + msg = messaging.recv_one_or_none(sock) + if msg is None: + return b"", None + which = msg.which() + frame = getattr(msg, which, None) + if frame is None: + return b"", None + header = bytes(getattr(frame, "header", b"") or b"") + data = bytes(getattr(frame, "data", b"") or b"") + frame_id = None + idx = getattr(frame, "idx", None) + if idx is not None: + try: + frame_id = int(getattr(idx, "frameId", 0) or 0) or None + except Exception: + frame_id = None + if frame_id is None: + try: + frame_id = int(getattr(frame, "frameId", 0) or 0) or None + except Exception: + frame_id = None + return header + data, frame_id + except Exception as exc: + self._last_error = f"{PHASE1_SOURCE_SERVICE} recv failed: {exc}" + return b"", None + + async def _start_process(self, stream_key: str) -> None: + await self._stop_process() + self._set_state("starting") + self._stderr_tail.clear() + self._started_at = _now() + self._session_started_bytes = self._bytes_sent + rtmp_url = f"{YOUTUBE_RTMPS_BASE}/{stream_key}" + cmd = [ + "ffmpeg", + "-hide_banner", + "-loglevel", + "warning", + "-fflags", + "nobuffer", + "-f", + "h264", + "-framerate", + "20", + "-i", + "pipe:0", + "-f", + "lavfi", + "-i", + "anullsrc=r=44100:cl=stereo", + "-c:v", + "copy", + "-c:a", + "aac", + "-b:a", + "128k", + "-f", + "flv", + rtmp_url, + ] + self._process = await asyncio.create_subprocess_exec( + *cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.PIPE, + ) + self._restart_count += 1 + self._next_retry_at = 0.0 + self._stderr_task = asyncio.create_task(self._read_stderr(self._process), name="carrot-youtube-live-stderr") + + async def _write_frame(self, payload: bytes) -> None: + proc = self._process + if proc is None or proc.stdin is None: + return + if proc.returncode is not None: + self._last_error = f"ffmpeg exited code={proc.returncode}" + self._schedule_backoff("ffmpeg exited") + await self._stop_process() + return + try: + proc.stdin.write(payload) + await proc.stdin.drain() + self._bytes_sent += len(payload) + self._consecutive_failures = 0 + except (BrokenPipeError, ConnectionResetError) as exc: + self._last_error = f"ffmpeg pipe closed: {exc}" + self._schedule_backoff("ffmpeg pipe closed") + await self._stop_process() + + async def _read_stderr(self, proc: asyncio.subprocess.Process) -> None: + if proc.stderr is None: + return + while True: + line = await proc.stderr.readline() + if not line: + break + text = line.decode("utf-8", errors="replace").strip() + if text: + self._stderr_tail.append(text[-300:]) + self._last_error = text[-300:] + + async def _stop_process(self) -> None: + proc = self._process + self._process = None + stderr_task = self._stderr_task + self._stderr_task = None + if proc is None: + if stderr_task is not None and not stderr_task.done(): + stderr_task.cancel() + return + self._set_state("stopping") + if proc.stdin is not None: + try: + proc.stdin.close() + await proc.stdin.wait_closed() + except Exception: + pass + try: + await asyncio.wait_for(proc.wait(), timeout=2.0) + except asyncio.TimeoutError: + try: + proc.terminate() + except ProcessLookupError: + pass + try: + await asyncio.wait_for(proc.wait(), timeout=3.0) + except asyncio.TimeoutError: + try: + proc.kill() + except ProcessLookupError: + pass + await proc.wait() + if stderr_task is not None and not stderr_task.done(): + stderr_task.cancel() + try: + await stderr_task + except asyncio.CancelledError: + pass + self._started_at = 0.0 + + def _schedule_backoff(self, reason: str = "") -> None: + self._consecutive_failures += 1 + delay = min(BACKOFF_MAX_SECONDS, BACKOFF_BASE_SECONDS * (2 ** max(0, self._consecutive_failures - 1))) + self._next_retry_at = _now() + delay + if reason and not self._last_error: + self._last_error = reason + + def _process_status(self) -> dict[str, Any]: + result = {} + for name, match in _PROCESS_MATCHES.items(): + pids = _find_matching_pids(match) + result[name] = { + "running": bool(pids), + "pids": pids[:8], + } + return result + + def _resource_status(self) -> dict[str, Any]: + processes = self._process_status() + cluster_param = self._param_int("ClusterHud", 0) + disable_dm = self._param_int("DisableDM", 0) + return { + "cluster": { + "enabled": cluster_param in (1, 2), + "param": cluster_param, + **processes.get("carrot_cluster", {"running": False, "pids": []}), + }, + "carrot_vision": { + "enabled": disable_dm == 2, + "disable_dm": disable_dm, + "webrtcd_running": bool(processes.get("webrtcd", {}).get("running")), + "stream_encoderd_running": bool(processes.get("stream_encoderd", {}).get("running")), + "webrtcd_pids": list(processes.get("webrtcd", {}).get("pids") or []), + "stream_encoderd_pids": list(processes.get("stream_encoderd", {}).get("pids") or []), + }, + } + + def _warnings(self, resource_status: dict[str, Any]) -> list[str]: + warnings = [] + if not self._param_enabled(): + return warnings + cluster = resource_status.get("cluster") if isinstance(resource_status, dict) else {} + vision = resource_status.get("carrot_vision") if isinstance(resource_status, dict) else {} + if cluster and cluster.get("enabled"): + warnings.append("Cluster HUD is enabled; monitor encoder load and thermal headroom while streaming.") + if vision and vision.get("enabled"): + warnings.append("Carrot Vision is enabled; YouTube Live shares camera/encoder/network resources.") + if self._param_int(YOUTUBE_QUALITY_PARAM, 0) > 0: + warnings.append("High quality mode is planned for Phase 3; Phase 2 keeps qRoadEncodeData only.") + return warnings + + +def _pid_cmdline(pid: int) -> str: + try: + return Path(f"/proc/{int(pid)}/cmdline").read_bytes().decode(errors="replace") + except Exception: + return "" + + +def _pid_alive(pid: int, match: str = "") -> bool: + if pid <= 0: + return False + try: + os.kill(pid, 0) + except OSError: + return False + return not match or match in _pid_cmdline(pid) + + +def _find_matching_pids(match: str) -> list[int]: + if not Path("/proc").exists(): + return [] + matches = [] + for proc_path in Path("/proc").glob("[0-9]*"): + try: + pid = int(proc_path.name) + except ValueError: + continue + if _pid_alive(pid, match): + matches.append(pid) + return sorted(matches) diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css new file mode 100644 index 00000000000..a2321684a37 --- /dev/null +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -0,0 +1,139 @@ +.youtube-live-section { + margin-top: var(--sp-lg); +} + +.youtube-live-card { + display: grid; + gap: var(--sp-md); + padding-top: var(--sp-lg); + padding-bottom: var(--sp-lg); +} + +.youtube-live-card__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--sp-md); +} + +.youtube-live-card__summary { + min-width: 0; +} + +.youtube-live-card__title { + color: var(--md-on-surface); + font-size: var(--fs-title-sm); + font-weight: 800; + letter-spacing: 0; +} + +.youtube-live-card__desc, +.youtube-live-metric__label { + color: var(--md-on-surface-var); + font-size: var(--fs-body-sm); + letter-spacing: 0; +} + +.youtube-live-pill { + flex: 0 0 auto; + max-width: 44%; + border: 1px solid color-mix(in srgb, var(--md-outline-var) 38%, transparent); + border-radius: 999px; + padding: 5px 10px; + color: var(--md-on-surface); + background: color-mix(in srgb, var(--md-surface) 72%, transparent); + font-size: var(--fs-label); + font-weight: 800; + line-height: 1.2; + text-align: center; + overflow-wrap: anywhere; +} + +.youtube-live-section[data-live-state="live"] .youtube-live-pill { + color: var(--md-primary); + border-color: color-mix(in srgb, var(--md-primary) 42%, transparent); + background: color-mix(in srgb, var(--md-primary) 14%, transparent); +} + +.youtube-live-section[data-live-state="error"] .youtube-live-pill { + color: var(--md-error); + border-color: color-mix(in srgb, var(--md-error) 45%, transparent); + background: color-mix(in srgb, var(--md-error) 10%, transparent); +} + +.youtube-live-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.youtube-live-metric { + min-width: 0; + border: 1px solid color-mix(in srgb, var(--md-outline-var) 22%, transparent); + border-radius: var(--r-md); + padding: 8px 10px; + background: color-mix(in srgb, var(--md-surface) 55%, transparent); +} + +.youtube-live-metric__value { + margin-top: 2px; + color: var(--md-on-surface); + font-size: var(--fs-body); + font-weight: 800; + letter-spacing: 0; + overflow-wrap: anywhere; +} + +.youtube-live-error { + border-left: 3px solid var(--md-error); + padding: 8px 10px; + color: var(--md-error); + background: color-mix(in srgb, var(--md-error) 8%, transparent); + font-size: var(--fs-body-sm); + overflow-wrap: anywhere; +} + +.youtube-live-warnings { + display: grid; + gap: 6px; +} + +.youtube-live-warning { + border-left: 3px solid var(--md-tertiary, var(--md-primary)); + padding: 8px 10px; + color: var(--md-on-surface); + background: color-mix(in srgb, var(--md-tertiary, var(--md-primary)) 9%, transparent); + font-size: var(--fs-body-sm); + overflow-wrap: anywhere; +} + +.youtube-live-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.youtube-live-action { + min-height: 36px; +} + +.youtube-live-action--danger { + color: var(--md-error); + border-color: color-mix(in srgb, var(--md-error) 35%, var(--md-outline-var)); +} + +@media (max-width: 430px) { + .youtube-live-card__head { + align-items: stretch; + flex-direction: column; + } + + .youtube-live-pill { + max-width: none; + align-self: flex-start; + } + + .youtube-live-metrics { + grid-template-columns: 1fr; + } +} diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index ed811bb590c..5a0cbc18af2 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -100,6 +100,7 @@ + @@ -705,9 +706,9 @@

Home

- - - + + + @@ -723,6 +724,7 @@

Home

+ diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js new file mode 100644 index 00000000000..8aa1d176cd7 --- /dev/null +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -0,0 +1,362 @@ +"use strict"; + +(function () { + const YOUTUBE_GROUP = "SYS_YOUTUBE"; + const CARD_ID = "youtubeLiveStatusCard"; + const POLL_MS = 2500; + const state = { + timer: null, + loading: false, + status: null, + observer: null, + }; + + function text(key, fallback, vars) { + if (typeof getUIText === "function") return getUIText(key, fallback, vars); + let value = fallback || key; + if (vars) { + Object.keys(vars).forEach((name) => { + value = value.replace(new RegExp(`\\{${name}\\}`, "g"), String(vars[name])); + }); + } + return value; + } + + function toast(message, options) { + if (typeof showAppToast === "function") showAppToast(message, options || undefined); + } + + function itemsBox() { + return document.getElementById("items"); + } + + function isVisible() { + const box = itemsBox(); + return Boolean(box && box.dataset.renderedGroup === YOUTUBE_GROUP && !box.dataset.renderedDetail); + } + + function stateLabel(status) { + const key = String(status?.state || "disabled"); + return text(`youtube_live_state_${key}`, key); + } + + function configuredLabel(status) { + if (status?.configured) { + return text("youtube_live_key_configured", "Key saved: {key}", { key: status.masked_key || "****" }); + } + return text("youtube_live_key_missing", "Stream key is not saved"); + } + + function metric(label, value) { + const item = document.createElement("div"); + item.className = "youtube-live-metric"; + const key = document.createElement("div"); + key.className = "youtube-live-metric__label"; + key.textContent = label; + const val = document.createElement("div"); + val.className = "youtube-live-metric__value"; + val.textContent = value; + item.appendChild(key); + item.appendChild(val); + return item; + } + + function makeButton(label, action, tone) { + const button = document.createElement("button"); + button.type = "button"; + button.className = `smallBtn youtube-live-action${tone ? ` youtube-live-action--${tone}` : ""}`; + button.textContent = label; + button.addEventListener("click", action); + return button; + } + + function ensureCard() { + const box = itemsBox(); + if (!box) return null; + const existing = document.getElementById(CARD_ID); + if (existing) return existing; + + const section = document.createElement("section"); + section.id = CARD_ID; + section.className = "setting-section-block youtube-live-section"; + + const title = document.createElement("div"); + title.className = "setting-group-card__title youtube-live-section__title"; + title.textContent = text("youtube_live_title", "YouTube Live"); + section.appendChild(title); + + const card = document.createElement("div"); + card.className = "setting-group-card youtube-live-card"; + section.appendChild(card); + + const head = document.createElement("div"); + head.className = "youtube-live-card__head"; + const summary = document.createElement("div"); + summary.className = "youtube-live-card__summary"; + const summaryTitle = document.createElement("div"); + summaryTitle.className = "youtube-live-card__title"; + summaryTitle.dataset.role = "title"; + summaryTitle.textContent = text("youtube_live_phase1_source", "Phase 1 - qRoadEncodeData"); + const summaryDesc = document.createElement("div"); + summaryDesc.className = "youtube-live-card__desc"; + summaryDesc.dataset.role = "configured"; + summaryDesc.textContent = text("loading", "Loading..."); + summary.appendChild(summaryTitle); + summary.appendChild(summaryDesc); + const pill = document.createElement("span"); + pill.className = "youtube-live-pill"; + pill.dataset.role = "state"; + pill.textContent = "-"; + head.appendChild(summary); + head.appendChild(pill); + card.appendChild(head); + + const metrics = document.createElement("div"); + metrics.className = "youtube-live-metrics"; + metrics.dataset.role = "metrics"; + card.appendChild(metrics); + + const error = document.createElement("div"); + error.className = "youtube-live-error"; + error.dataset.role = "error"; + error.hidden = true; + card.appendChild(error); + + const warnings = document.createElement("div"); + warnings.className = "youtube-live-warnings"; + warnings.dataset.role = "warnings"; + warnings.hidden = true; + card.appendChild(warnings); + + const actions = document.createElement("div"); + actions.className = "youtube-live-actions"; + actions.appendChild(makeButton(text("youtube_live_set_key", "Set key"), promptSetKey)); + actions.appendChild(makeButton(text("youtube_live_test", "Test"), testConfig)); + actions.appendChild(makeButton(text("youtube_live_diagnostics", "Diagnostics"), showDiagnostics)); + actions.appendChild(makeButton(text("youtube_live_download_logs", "Download logs"), downloadDiagnostics)); + actions.appendChild(makeButton(text("youtube_live_stop", "Stop"), stopStream)); + actions.appendChild(makeButton(text("youtube_live_clear_key", "Clear key"), clearKey, "danger")); + card.appendChild(actions); + + box.appendChild(section); + return section; + } + + function renderStatus(status) { + const card = ensureCard(); + if (!card) return; + const stateEl = card.querySelector('[data-role="state"]'); + const configuredEl = card.querySelector('[data-role="configured"]'); + const metricsEl = card.querySelector('[data-role="metrics"]'); + const errorEl = card.querySelector('[data-role="error"]'); + const warningsEl = card.querySelector('[data-role="warnings"]'); + card.dataset.liveState = String(status?.state || "disabled"); + if (stateEl) stateEl.textContent = stateLabel(status); + if (configuredEl) configuredEl.textContent = configuredLabel(status); + if (metricsEl) { + metricsEl.innerHTML = ""; + metricsEl.appendChild(metric(text("youtube_live_metric_source", "Source"), status?.source || "qRoadEncodeData")); + metricsEl.appendChild(metric(text("youtube_live_metric_runtime", "Runtime"), formatSeconds(status?.uptime_sec || 0))); + metricsEl.appendChild(metric(text("youtube_live_metric_bitrate", "Bitrate"), `${status?.estimated_kbps || 0} kbps`)); + metricsEl.appendChild(metric(text("youtube_live_metric_session", "Session"), `${status?.session_mb || 0} MB`)); + metricsEl.appendChild(metric(text("youtube_live_metric_total", "Total"), `${status?.total_mb || 0} MB`)); + metricsEl.appendChild(metric(text("youtube_live_metric_retry", "Retry"), retryLabel(status))); + } + const error = String(status?.last_error || "").trim(); + if (errorEl) { + errorEl.hidden = !error; + errorEl.textContent = error; + } + renderWarnings(warningsEl, status); + } + + function formatSeconds(seconds) { + const total = Math.max(0, Math.floor(Number(seconds) || 0)); + const h = Math.floor(total / 3600); + const m = Math.floor((total % 3600) / 60); + const s = total % 60; + return [h, m, s].map((v) => String(v).padStart(2, "0")).join(":"); + } + + function retryLabel(status) { + const retry = Number(status?.retry_in_sec || 0); + if (String(status?.state || "") === "backoff" && retry > 0) { + return text("youtube_live_retry_in", "{seconds}s", { seconds: retry }); + } + return "-"; + } + + function renderWarnings(container, status) { + if (!container) return; + const warnings = Array.isArray(status?.warnings) ? status.warnings.filter(Boolean) : []; + if (!warnings.length) { + container.hidden = true; + container.innerHTML = ""; + return; + } + container.hidden = false; + container.innerHTML = ""; + warnings.slice(0, 3).forEach((warning) => { + const item = document.createElement("div"); + item.className = "youtube-live-warning"; + item.textContent = warning; + container.appendChild(item); + }); + } + + async function loadStatus() { + if (state.loading || !isVisible()) return; + state.loading = true; + try { + const status = await getJson("/api/youtube_live/status"); + state.status = status; + renderStatus(status); + } catch (err) { + renderStatus({ state: "error", last_error: err?.message || String(err) }); + } finally { + state.loading = false; + } + } + + async function promptSetKey() { + const current = state.status?.configured ? state.status?.masked_key : ""; + const prompt = text("youtube_live_key_prompt", "Enter YouTube stream key."); + const value = await appPrompt(prompt, { + title: text("youtube_live_set_key", "Set key"), + defaultValue: "", + placeholder: current || "xxxx-xxxx-xxxx-xxxx", + confirmLabel: text("save", "Save"), + }); + if (value === null || value === false || String(value).trim() === "") return; + try { + const status = await postJson("/api/youtube_live/stream_key", { stream_key: String(value).trim() }); + state.status = status; + renderStatus(status); + toast(text("youtube_live_key_saved", "Stream key saved")); + } catch (err) { + toast(err?.message || String(err), { tone: "error" }); + } + } + + async function clearKey() { + const ok = await appConfirm(text("youtube_live_clear_confirm", "Clear saved stream key?"), { + title: text("youtube_live_clear_key", "Clear key"), + }); + if (!ok) return; + try { + const status = await requestJson("/api/youtube_live/stream_key", { method: "DELETE" }); + state.status = status; + renderStatus(status); + toast(text("youtube_live_key_cleared", "Stream key cleared")); + } catch (err) { + toast(err?.message || String(err), { tone: "error" }); + } + } + + async function stopStream() { + try { + const status = await postJson("/api/youtube_live/stop", {}); + state.status = status; + renderStatus(status); + toast(text("youtube_live_stopped", "Stream stopped")); + } catch (err) { + toast(err?.message || String(err), { tone: "error" }); + } + } + + async function testConfig() { + try { + const result = await postJson("/api/youtube_live/test", {}); + toast(result.message || text("youtube_live_test_ready", "Ready")); + } catch (err) { + toast(err?.message || text("youtube_live_test_failed", "Test failed"), { tone: "error" }); + } + } + + function diagnosticText(payload) { + const diag = payload?.diagnostics || payload || {}; + const status = diag.status || {}; + const resources = status.resource_status || {}; + const cluster = resources.cluster || {}; + const vision = resources.carrot_vision || {}; + const lines = [ + `${text("youtube_live_state_label", "State")}: ${stateLabel(status)}`, + `${text("youtube_live_metric_source", "Source")}: ${status.source || "-"}`, + `${text("youtube_live_metric_bitrate", "Bitrate")}: ${status.estimated_kbps || 0} kbps`, + `${text("youtube_live_metric_session", "Session")}: ${status.session_mb || 0} MB`, + `${text("youtube_live_metric_total", "Total")}: ${status.total_mb || 0} MB`, + `ClusterHud: ${cluster.enabled ? "on" : "off"} (${cluster.param ?? "-"})`, + `CarrotVision: ${vision.enabled ? "on" : "off"} (DisableDM=${vision.disable_dm ?? "-"})`, + ]; + const warnings = Array.isArray(status.warnings) ? status.warnings : []; + if (warnings.length) { + lines.push(""); + lines.push(text("youtube_live_warnings", "Warnings")); + warnings.forEach((warning) => lines.push(`- ${warning}`)); + } + const stderr = diag.ffmpeg?.stderr_tail || status.stderr_tail || []; + if (stderr.length) { + lines.push(""); + lines.push("ffmpeg stderr"); + stderr.slice(-8).forEach((line) => lines.push(`- ${line}`)); + } + return lines.join("\n"); + } + + async function showDiagnostics() { + try { + const payload = await getJson("/api/youtube_live/diagnostics"); + const raw = JSON.stringify(payload.diagnostics || payload, null, 2); + await appAlert(diagnosticText(payload), { + title: text("youtube_live_diagnostics", "Diagnostics"), + copyText: raw, + }); + } catch (err) { + toast(err?.message || String(err), { tone: "error" }); + } + } + + function downloadDiagnostics() { + window.open("/api/youtube_live/diagnostics/download", "_blank", "noopener"); + } + + function startPolling() { + if (!state.timer) state.timer = window.setInterval(loadStatus, POLL_MS); + loadStatus(); + } + + function stopPolling() { + if (state.timer) window.clearInterval(state.timer); + state.timer = null; + } + + function sync() { + if (isVisible()) { + ensureCard(); + startPolling(); + return; + } + stopPolling(); + const card = document.getElementById(CARD_ID); + if (card) card.remove(); + } + + function init() { + const box = itemsBox(); + if (!box || state.observer) return; + state.observer = new MutationObserver(sync); + state.observer.observe(box, { + childList: true, + attributes: true, + attributeFilter: ["data-rendered-group", "data-rendered-detail"], + }); + window.addEventListener("carrot:paramchange", (event) => { + const name = event?.detail?.name; + if (name === "CarrotYouTubeLive" || name === "CarrotYouTubeQuality") loadStatus(); + }); + sync(); + } + + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init, { once: true }); + else init(); +})(); diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index 64c7487c20a..b0a38f21bc4 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -5,6 +5,40 @@ window.CarrotTranslations.register("en", { nativeName: "English", shortName: "EN", strings: { + youtube_live_title: "YouTube Live", + youtube_live_phase1_source: "Phase 1 - qRoadEncodeData", + youtube_live_key_configured: "Key saved: {key}", + youtube_live_key_missing: "Stream key is not saved", + youtube_live_key_prompt: "Enter YouTube stream key.", + youtube_live_set_key: "Set key", + youtube_live_clear_key: "Clear key", + youtube_live_clear_confirm: "Clear saved stream key?", + youtube_live_key_saved: "Stream key saved", + youtube_live_key_cleared: "Stream key cleared", + youtube_live_test: "Test", + youtube_live_test_ready: "Ready", + youtube_live_test_failed: "Test failed", + youtube_live_stop: "Stop", + youtube_live_stopped: "Stream stopped", + youtube_live_metric_source: "Source", + youtube_live_metric_runtime: "Runtime", + youtube_live_metric_bitrate: "Bitrate", + youtube_live_metric_session: "Session", + youtube_live_metric_total: "Total", + youtube_live_metric_retry: "Retry", + youtube_live_retry_in: "{seconds}s", + youtube_live_diagnostics: "Diagnostics", + youtube_live_download_logs: "Download logs", + youtube_live_state_label: "State", + youtube_live_warnings: "Warnings", + youtube_live_state_backoff: "Backoff", + youtube_live_state_disabled: "Disabled", + youtube_live_state_needs_setup: "Needs setup", + youtube_live_state_waiting_frame: "Waiting frame", + youtube_live_state_starting: "Starting", + youtube_live_state_live: "Live", + youtube_live_state_stopping: "Stopping", + youtube_live_state_error: "Error", home: "Drive", setting: "Setting", setting_tab_device: "Device", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index 024cbce7fae..0e253e99ec2 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -5,6 +5,40 @@ window.CarrotTranslations.register("ko", { nativeName: "한국어", shortName: "KO", strings: { + youtube_live_title: "YouTube 라이브", + youtube_live_phase1_source: "페이즈 1 - qRoadEncodeData", + youtube_live_key_configured: "키 저장됨: {key}", + youtube_live_key_missing: "스트림 키가 저장되지 않음", + youtube_live_key_prompt: "YouTube 스트림 키를 입력하세요.", + youtube_live_set_key: "키 설정", + youtube_live_clear_key: "키 삭제", + youtube_live_clear_confirm: "저장된 스트림 키를 삭제할까요?", + youtube_live_key_saved: "스트림 키 저장됨", + youtube_live_key_cleared: "스트림 키 삭제됨", + youtube_live_test: "테스트", + youtube_live_test_ready: "준비됨", + youtube_live_test_failed: "테스트 실패", + youtube_live_stop: "중지", + youtube_live_stopped: "송출 중지됨", + youtube_live_metric_source: "소스", + youtube_live_metric_runtime: "시간", + youtube_live_metric_bitrate: "비트레이트", + youtube_live_metric_session: "세션", + youtube_live_metric_total: "누적", + youtube_live_metric_retry: "재시도", + youtube_live_retry_in: "{seconds}초", + youtube_live_diagnostics: "진단", + youtube_live_download_logs: "로그 다운로드", + youtube_live_state_label: "상태", + youtube_live_warnings: "경고", + youtube_live_state_backoff: "재시도 대기", + youtube_live_state_disabled: "꺼짐", + youtube_live_state_needs_setup: "설정 필요", + youtube_live_state_waiting_frame: "프레임 대기", + youtube_live_state_starting: "시작 중", + youtube_live_state_live: "송출 중", + youtube_live_state_stopping: "중지 중", + youtube_live_state_error: "오류", home: "주행", setting: "설정", setting_tab_device: "디바이스", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 1ffd015343c..d5a9b0e1f53 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -5,6 +5,40 @@ window.CarrotTranslations.register("zh", { nativeName: "中文", shortName: "ZH", strings: { + youtube_live_title: "YouTube Live", + youtube_live_phase1_source: "Phase 1 - qRoadEncodeData", + youtube_live_key_configured: "Key saved: {key}", + youtube_live_key_missing: "Stream key is not saved", + youtube_live_key_prompt: "Enter YouTube stream key.", + youtube_live_set_key: "Set key", + youtube_live_clear_key: "Clear key", + youtube_live_clear_confirm: "Clear saved stream key?", + youtube_live_key_saved: "Stream key saved", + youtube_live_key_cleared: "Stream key cleared", + youtube_live_test: "Test", + youtube_live_test_ready: "Ready", + youtube_live_test_failed: "Test failed", + youtube_live_stop: "Stop", + youtube_live_stopped: "Stream stopped", + youtube_live_metric_source: "Source", + youtube_live_metric_runtime: "Runtime", + youtube_live_metric_bitrate: "Bitrate", + youtube_live_metric_session: "Session", + youtube_live_metric_total: "Total", + youtube_live_metric_retry: "Retry", + youtube_live_retry_in: "{seconds}s", + youtube_live_diagnostics: "Diagnostics", + youtube_live_download_logs: "Download logs", + youtube_live_state_label: "State", + youtube_live_warnings: "Warnings", + youtube_live_state_backoff: "Backoff", + youtube_live_state_disabled: "Disabled", + youtube_live_state_needs_setup: "Needs setup", + youtube_live_state_waiting_frame: "Waiting frame", + youtube_live_state_starting: "Starting", + youtube_live_state_live: "Live", + youtube_live_state_stopping: "Stopping", + youtube_live_state_error: "Error", home: "驾驶", setting: "设置", setting_tab_device: "设备", diff --git a/selfdrive/carrot_settings.json b/selfdrive/carrot_settings.json index dd43f399604..733391e161e 100644 --- a/selfdrive/carrot_settings.json +++ b/selfdrive/carrot_settings.json @@ -544,6 +544,16 @@ "MaxTimeOffroadMin" ] }, + { + "id": "SYS_YOUTUBE", + "ko": "YouTube 라이브", + "en": "YouTube Live", + "zh": "YouTube Live", + "params": [ + "CarrotYouTubeLive", + "CarrotYouTubeQuality" + ] + }, { "id": "SYS_NET", "ko": "네트워크·지도", @@ -1729,6 +1739,38 @@ "default": 0, "unit": 1 }, + { + "group": "SYS_YOUTUBE", + "name": "CarrotYouTubeLive", + "title": "YouTube 송출", + "descr": "YouTube Live로 qRoadEncodeData 영상을 송출합니다.\n0: 끄기, 1: 켜기", + "egroup": "System", + "etitle": "YouTube Live", + "edescr": "Stream qRoadEncodeData video to YouTube Live.\n0: OFF, 1: ON", + "cgroup": "System", + "ctitle": "YouTube Live", + "cdescr": "Stream qRoadEncodeData video to YouTube Live.\n0: OFF, 1: ON", + "min": 0, + "max": 1, + "default": 0, + "unit": 1 + }, + { + "group": "SYS_YOUTUBE", + "name": "CarrotYouTubeQuality", + "title": "YouTube 화질", + "descr": "0: 표준(qRoadEncodeData), 1: 고화질(준비중)", + "egroup": "System", + "etitle": "YouTube Quality", + "edescr": "0: Standard(qRoadEncodeData), 1: High quality(preparing)", + "cgroup": "System", + "ctitle": "YouTube Quality", + "cdescr": "0: Standard(qRoadEncodeData), 1: High quality(preparing)", + "min": 0, + "max": 1, + "default": 0, + "unit": 1 + }, { "group": "시작", "name": "HDPuse", From bfab1ffbf5c6a9759ffcc0d9c45fb4adc8932c02 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 12:46:31 +0900 Subject: [PATCH 31/61] f --- .../carrot/server/features/youtube_live.py | 14 ++ .../carrot/server/services/youtube_live.py | 49 ++++++ .../web/css/pages/settings/youtube_live.css | 87 ++++++++++- selfdrive/carrot/web/index.html | 10 +- .../web/js/pages/setting_youtube_live.js | 143 +++++++++++++++--- selfdrive/carrot/web/js/translations/en.js | 14 ++ selfdrive/carrot/web/js/translations/ko.js | 14 ++ selfdrive/carrot/web/js/translations/zh.js | 14 ++ 8 files changed, 316 insertions(+), 29 deletions(-) diff --git a/selfdrive/carrot/server/features/youtube_live.py b/selfdrive/carrot/server/features/youtube_live.py index 4108bcba42a..f5fb6c352ba 100644 --- a/selfdrive/carrot/server/features/youtube_live.py +++ b/selfdrive/carrot/server/features/youtube_live.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import asyncio from aiohttp import web @@ -59,6 +60,18 @@ async def api_test(request: web.Request) -> web.Response: return web.json_response(result, status=200 if result.get("ok") else 409) +async def api_validate_stream_key(request: web.Request) -> web.Response: + body = {} + if request.can_read_body: + try: + body = await request.json() + except Exception: + return web.json_response({"ok": False, "error": "invalid json"}, status=400) + key = body.get("stream_key", body.get("key")) if isinstance(body, dict) else None + result = await asyncio.to_thread(_service(request).validate_stream_key, key) + return web.json_response(result, status=200 if result.get("ok") else 409) + + async def api_diagnostics(request: web.Request) -> web.Response: return web.json_response({"ok": True, "diagnostics": _service(request).diagnostics()}) @@ -81,6 +94,7 @@ def register(app: web.Application) -> None: app.router.add_get("/api/youtube_live/diagnostics", api_diagnostics) app.router.add_get("/api/youtube_live/diagnostics/download", api_download_diagnostics) app.router.add_post("/api/youtube_live/stream_key", api_set_stream_key) + app.router.add_post("/api/youtube_live/stream_key/validate", api_validate_stream_key) app.router.add_delete("/api/youtube_live/stream_key", api_clear_stream_key) app.router.add_post("/api/youtube_live/stop", api_stop) app.router.add_post("/api/youtube_live/test", api_test) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index a1d13928d2b..d655361bc14 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -3,7 +3,10 @@ import asyncio import json import os +import re import shutil +import socket +import ssl import time from collections import deque from pathlib import Path @@ -17,6 +20,8 @@ PHASE1_SOURCE_SERVICE = "qRoadEncodeData" PHASE1_QUALITY = "standard" YOUTUBE_RTMPS_BASE = "rtmps://a.rtmps.youtube.com/live2" +YOUTUBE_RTMPS_HOST = "a.rtmps.youtube.com" +YOUTUBE_RTMPS_PORT = 443 NO_FRAME_STOP_SECONDS = 8.0 START_BACKOFF_SECONDS = 5.0 STATUS_WRITE_MIN_INTERVAL = 1.0 @@ -77,6 +82,31 @@ def _extract_stream_key(value: str) -> str: return raw +def _validate_stream_key_format(stream_key: str) -> tuple[bool, str]: + key = stream_key.strip() + if not key: + return False, "stream key is required" + if len(key) < 8: + return False, "stream key is too short" + if len(key) > 256: + return False, "stream key is too long" + if re.search(r"\s", key): + return False, "stream key must not contain spaces" + if not re.fullmatch(r"[A-Za-z0-9._/-]+", key): + return False, "stream key contains unsupported characters" + return True, "format looks valid" + + +def _check_rtmps_reachable(timeout: float = 2.5) -> tuple[bool, str]: + try: + context = ssl.create_default_context() + with socket.create_connection((YOUTUBE_RTMPS_HOST, YOUTUBE_RTMPS_PORT), timeout=timeout) as sock: + with context.wrap_socket(sock, server_hostname=YOUTUBE_RTMPS_HOST): + return True, "YouTube RTMPS ingest is reachable" + except Exception as exc: + return False, f"YouTube RTMPS ingest unreachable: {exc}" + + class YouTubeLiveService: def __init__( self, @@ -195,6 +225,25 @@ def test_config(self) -> dict[str, Any]: "message": "ready" if ok else "missing stream key or ffmpeg", } + def validate_stream_key(self, value: str | None = None) -> dict[str, Any]: + stream_key = _extract_stream_key(value) if value is not None else self.get_stream_key() + format_ok, format_message = _validate_stream_key_format(stream_key) + rtmps_ok, rtmps_message = _check_rtmps_reachable() + ffmpeg_path = shutil.which("ffmpeg") + ok = bool(format_ok and rtmps_ok and ffmpeg_path) + return { + "ok": ok, + "configured": bool(self.get_stream_key()), + "format_ok": format_ok, + "format_message": format_message, + "rtmps_reachable": rtmps_ok, + "rtmps_message": rtmps_message, + "ffmpeg_available": bool(ffmpeg_path), + "ffmpeg_path": ffmpeg_path or "", + "masked_key": _mask_stream_key(stream_key), + "note": "YouTube only confirms whether the key is accepted when an encoder starts streaming.", + } + def diagnostics(self) -> dict[str, Any]: status = self.status() stream_key = self.get_stream_key() diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css index a2321684a37..7abfd8615fa 100644 --- a/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -1,14 +1,25 @@ .youtube-live-section { + display: grid; + gap: var(--sp-md); margin-top: var(--sp-lg); } -.youtube-live-card { +.youtube-live-key-card, +.youtube-live-card, +.youtube-live-ops-card { display: grid; gap: var(--sp-md); padding-top: var(--sp-lg); padding-bottom: var(--sp-lg); } +.youtube-live-key-card__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--sp-md); +} + .youtube-live-card__head { display: flex; align-items: flex-start; @@ -16,7 +27,8 @@ gap: var(--sp-md); } -.youtube-live-card__summary { +.youtube-live-card__summary, +.youtube-live-key-card__head > div { min-width: 0; } @@ -34,6 +46,55 @@ letter-spacing: 0; } +.youtube-live-guide { + display: grid; + gap: 4px; + color: var(--md-on-surface-var); + font-size: var(--fs-body-sm); + letter-spacing: 0; +} + +.youtube-live-guide span { + min-width: 0; + overflow-wrap: anywhere; +} + +.youtube-live-key-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; +} + +.youtube-live-key-input { + min-width: 0; + min-height: 40px; + border: 1px solid color-mix(in srgb, var(--md-outline-var) 42%, transparent); + border-radius: var(--r-md); + padding: 0 12px; + color: var(--md-on-surface); + background: color-mix(in srgb, var(--md-surface) 72%, transparent); + font: inherit; + font-size: var(--fs-body); + letter-spacing: 0; +} + +.youtube-live-key-input:focus { + outline: none; + border-color: color-mix(in srgb, var(--md-primary) 62%, var(--md-outline-var)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-primary) 16%, transparent); +} + +.youtube-live-key-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.youtube-live-link-btn { + flex: 0 0 auto; +} + .youtube-live-pill { flex: 0 0 auto; max-width: 44%; @@ -63,7 +124,7 @@ .youtube-live-metrics { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } @@ -113,8 +174,19 @@ gap: 8px; } +.youtube-live-ops-card__title { + color: var(--md-on-surface); + font-size: var(--fs-title-sm); + font-weight: 800; + letter-spacing: 0; +} + .youtube-live-action { + width: auto; + min-width: 0; min-height: 36px; + flex: 0 1 auto; + white-space: nowrap; } .youtube-live-action--danger { @@ -128,6 +200,15 @@ flex-direction: column; } + .youtube-live-key-card__head, + .youtube-live-key-row { + grid-template-columns: 1fr; + } + + .youtube-live-key-card__head { + flex-direction: column; + } + .youtube-live-pill { max-width: none; align-self: flex-start; diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 5a0cbc18af2..9aba9ee74cf 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -100,7 +100,7 @@ - + @@ -706,9 +706,9 @@

Home

- - - + + + @@ -724,7 +724,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 8aa1d176cd7..78faad742b9 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -3,6 +3,8 @@ (function () { const YOUTUBE_GROUP = "SYS_YOUTUBE"; const CARD_ID = "youtubeLiveStatusCard"; + const LIVE_CONTROL_ROOM_URL = "https://www.youtube.com/livestreaming"; + const YOUTUBE_ENCODER_HELP_URL = "https://support.google.com/youtube/answer/2907883"; const POLL_MS = 2500; const state = { timer: null, @@ -85,9 +87,34 @@ title.textContent = text("youtube_live_title", "YouTube Live"); section.appendChild(title); - const card = document.createElement("div"); - card.className = "setting-group-card youtube-live-card"; - section.appendChild(card); + const keyCard = document.createElement("div"); + keyCard.className = "setting-group-card youtube-live-key-card"; + keyCard.innerHTML = ` +
+
+
${escapeHtmlLocal(text("youtube_live_key_panel_title", "Stream key"))}
+
${escapeHtmlLocal(text("loading", "Loading..."))}
+
+ +
+
+ ${escapeHtmlLocal(text("youtube_live_key_guide_1", "YouTube Studio > Create > Go live"))} + ${escapeHtmlLocal(text("youtube_live_key_guide_2", "Live Control Room > Stream > copy Stream key"))} +
+
+ + +
+
+ + +
+ `; + section.appendChild(keyCard); + + const statusCard = document.createElement("div"); + statusCard.className = "setting-group-card youtube-live-card youtube-live-status-card"; + section.appendChild(statusCard); const head = document.createElement("div"); head.className = "youtube-live-card__head"; @@ -109,49 +136,79 @@ pill.textContent = "-"; head.appendChild(summary); head.appendChild(pill); - card.appendChild(head); + statusCard.appendChild(head); const metrics = document.createElement("div"); metrics.className = "youtube-live-metrics"; metrics.dataset.role = "metrics"; - card.appendChild(metrics); + statusCard.appendChild(metrics); const error = document.createElement("div"); error.className = "youtube-live-error"; error.dataset.role = "error"; error.hidden = true; - card.appendChild(error); + statusCard.appendChild(error); const warnings = document.createElement("div"); warnings.className = "youtube-live-warnings"; warnings.dataset.role = "warnings"; warnings.hidden = true; - card.appendChild(warnings); + statusCard.appendChild(warnings); + + const opsCard = document.createElement("div"); + opsCard.className = "setting-group-card youtube-live-ops-card"; + const opsTitle = document.createElement("div"); + opsTitle.className = "youtube-live-ops-card__title"; + opsTitle.textContent = text("youtube_live_operations", "Operations"); + opsCard.appendChild(opsTitle); const actions = document.createElement("div"); actions.className = "youtube-live-actions"; - actions.appendChild(makeButton(text("youtube_live_set_key", "Set key"), promptSetKey)); actions.appendChild(makeButton(text("youtube_live_test", "Test"), testConfig)); actions.appendChild(makeButton(text("youtube_live_diagnostics", "Diagnostics"), showDiagnostics)); actions.appendChild(makeButton(text("youtube_live_download_logs", "Download logs"), downloadDiagnostics)); actions.appendChild(makeButton(text("youtube_live_stop", "Stop"), stopStream)); actions.appendChild(makeButton(text("youtube_live_clear_key", "Clear key"), clearKey, "danger")); - card.appendChild(actions); + opsCard.appendChild(actions); + section.appendChild(opsCard); + + keyCard.querySelector('[data-action="open-studio"]')?.addEventListener("click", openLiveControlRoom); + keyCard.querySelector('[data-action="open-help"]')?.addEventListener("click", openYouTubeHelp); + keyCard.querySelector('[data-action="save-key"]')?.addEventListener("click", saveKeyFromInput); + keyCard.querySelector('[data-action="validate-key"]')?.addEventListener("click", validateStreamKey); + keyCard.querySelector('[data-role="key-input"]')?.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + saveKeyFromInput(); + } + }); - box.appendChild(section); + box.insertBefore(section, box.firstChild); return section; } + function escapeHtmlLocal(value) { + return String(value ?? "").replace(/[&<>"']/g, (ch) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[ch])); + } + function renderStatus(status) { const card = ensureCard(); if (!card) return; const stateEl = card.querySelector('[data-role="state"]'); + const keyStatusEl = card.querySelector('[data-role="key-status"]'); const configuredEl = card.querySelector('[data-role="configured"]'); const metricsEl = card.querySelector('[data-role="metrics"]'); const errorEl = card.querySelector('[data-role="error"]'); const warningsEl = card.querySelector('[data-role="warnings"]'); card.dataset.liveState = String(status?.state || "disabled"); if (stateEl) stateEl.textContent = stateLabel(status); + if (keyStatusEl) keyStatusEl.textContent = configuredLabel(status); if (configuredEl) configuredEl.textContent = configuredLabel(status); if (metricsEl) { metricsEl.innerHTML = ""; @@ -218,26 +275,70 @@ } } - async function promptSetKey() { - const current = state.status?.configured ? state.status?.masked_key : ""; - const prompt = text("youtube_live_key_prompt", "Enter YouTube stream key."); - const value = await appPrompt(prompt, { - title: text("youtube_live_set_key", "Set key"), - defaultValue: "", - placeholder: current || "xxxx-xxxx-xxxx-xxxx", - confirmLabel: text("save", "Save"), - }); - if (value === null || value === false || String(value).trim() === "") return; + function keyInputValue() { + return String(document.querySelector(`#${CARD_ID} [data-role="key-input"]`)?.value || "").trim(); + } + + async function saveKeyFromInput() { + const value = keyInputValue(); + if (!value) { + toast(text("youtube_live_key_empty", "Enter a stream key first."), { tone: "error" }); + return; + } try { - const status = await postJson("/api/youtube_live/stream_key", { stream_key: String(value).trim() }); + const status = await postJson("/api/youtube_live/stream_key", { stream_key: value }); state.status = status; renderStatus(status); + const input = document.querySelector(`#${CARD_ID} [data-role="key-input"]`); + if (input) input.value = ""; toast(text("youtube_live_key_saved", "Stream key saved")); } catch (err) { toast(err?.message || String(err), { tone: "error" }); } } + async function validateStreamKey() { + const value = keyInputValue(); + try { + const payload = value + ? { stream_key: value } + : {}; + const result = await postJson("/api/youtube_live/stream_key/validate", payload); + await appAlert(validationText(result), { + title: text("youtube_live_validate_key", "Validate key"), + }); + } catch (err) { + const payload = err?.payload || null; + if (payload) { + await appAlert(validationText(payload), { + title: text("youtube_live_validate_key", "Validate key"), + }); + return; + } + toast(err?.message || String(err), { tone: "error" }); + } + } + + function validationText(result) { + const lines = [ + `${text("youtube_live_validation_format", "Key format")}: ${result?.format_ok ? "OK" : "FAIL"} - ${result?.format_message || "-"}`, + `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${result?.ffmpeg_available ? "OK" : "FAIL"}`, + `${text("youtube_live_validation_rtmps", "YouTube RTMPS")}: ${result?.rtmps_reachable ? "OK" : "FAIL"} - ${result?.rtmps_message || "-"}`, + ]; + if (result?.masked_key) lines.push(`${text("youtube_live_key", "Stream key")}: ${result.masked_key}`); + lines.push(""); + lines.push(result?.note || text("youtube_live_validation_note", "YouTube confirms the key only when streaming starts.")); + return lines.join("\n"); + } + + function openLiveControlRoom() { + window.open(LIVE_CONTROL_ROOM_URL, "_blank", "noopener"); + } + + function openYouTubeHelp() { + window.open(YOUTUBE_ENCODER_HELP_URL, "_blank", "noopener"); + } + async function clearKey() { const ok = await appConfirm(text("youtube_live_clear_confirm", "Clear saved stream key?"), { title: text("youtube_live_clear_key", "Clear key"), diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index b0a38f21bc4..62f33287308 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -9,6 +9,20 @@ window.CarrotTranslations.register("en", { youtube_live_phase1_source: "Phase 1 - qRoadEncodeData", youtube_live_key_configured: "Key saved: {key}", youtube_live_key_missing: "Stream key is not saved", + youtube_live_key_panel_title: "Stream key", + youtube_live_key_guide_1: "YouTube Studio > Create > Go live", + youtube_live_key_guide_2: "Live Control Room > Stream > copy Stream key", + youtube_live_key_placeholder: "Paste stream key", + youtube_live_open_studio: "Open Studio", + youtube_live_help: "Help", + youtube_live_key_empty: "Enter a stream key first.", + youtube_live_validate_key: "Validate key", + youtube_live_validation_format: "Key format", + youtube_live_validation_ffmpeg: "FFmpeg", + youtube_live_validation_rtmps: "YouTube RTMPS", + youtube_live_validation_note: "YouTube confirms the key only when streaming starts.", + youtube_live_key: "Stream key", + youtube_live_operations: "Operations", youtube_live_key_prompt: "Enter YouTube stream key.", youtube_live_set_key: "Set key", youtube_live_clear_key: "Clear key", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index 0e253e99ec2..0d26a4b0cc9 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -9,6 +9,20 @@ window.CarrotTranslations.register("ko", { youtube_live_phase1_source: "페이즈 1 - qRoadEncodeData", youtube_live_key_configured: "키 저장됨: {key}", youtube_live_key_missing: "스트림 키가 저장되지 않음", + youtube_live_key_panel_title: "스트림 키", + youtube_live_key_guide_1: "YouTube Studio > 만들기 > Go live", + youtube_live_key_guide_2: "Live Control Room > Stream > Stream key 복사", + youtube_live_key_placeholder: "스트림 키 붙여넣기", + youtube_live_open_studio: "Studio 열기", + youtube_live_help: "도움말", + youtube_live_key_empty: "스트림 키를 먼저 입력하세요.", + youtube_live_validate_key: "키 검증", + youtube_live_validation_format: "키 형식", + youtube_live_validation_ffmpeg: "FFmpeg", + youtube_live_validation_rtmps: "YouTube RTMPS", + youtube_live_validation_note: "YouTube는 송출 시작 시점에만 키 수락 여부를 확정합니다.", + youtube_live_key: "스트림 키", + youtube_live_operations: "운영", youtube_live_key_prompt: "YouTube 스트림 키를 입력하세요.", youtube_live_set_key: "키 설정", youtube_live_clear_key: "키 삭제", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index d5a9b0e1f53..2f97efec914 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -9,6 +9,20 @@ window.CarrotTranslations.register("zh", { youtube_live_phase1_source: "Phase 1 - qRoadEncodeData", youtube_live_key_configured: "Key saved: {key}", youtube_live_key_missing: "Stream key is not saved", + youtube_live_key_panel_title: "Stream key", + youtube_live_key_guide_1: "YouTube Studio > Create > Go live", + youtube_live_key_guide_2: "Live Control Room > Stream > copy Stream key", + youtube_live_key_placeholder: "Paste stream key", + youtube_live_open_studio: "Open Studio", + youtube_live_help: "Help", + youtube_live_key_empty: "Enter a stream key first.", + youtube_live_validate_key: "Validate key", + youtube_live_validation_format: "Key format", + youtube_live_validation_ffmpeg: "FFmpeg", + youtube_live_validation_rtmps: "YouTube RTMPS", + youtube_live_validation_note: "YouTube confirms the key only when streaming starts.", + youtube_live_key: "Stream key", + youtube_live_operations: "Operations", youtube_live_key_prompt: "Enter YouTube stream key.", youtube_live_set_key: "Set key", youtube_live_clear_key: "Clear key", From 81a8ff3c5dbac13d9a4e8babce607acac5e43f99 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 12:53:52 +0900 Subject: [PATCH 32/61] f --- .../web/css/pages/settings/youtube_live.css | 52 ++--- selfdrive/carrot/web/index.html | 10 +- .../web/js/pages/setting_youtube_live.js | 218 +++++++++--------- selfdrive/carrot/web/js/translations/en.js | 7 + selfdrive/carrot/web/js/translations/ko.js | 7 + selfdrive/carrot/web/js/translations/zh.js | 7 + 6 files changed, 156 insertions(+), 145 deletions(-) diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css index 7abfd8615fa..5802f3f330d 100644 --- a/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -6,20 +6,14 @@ .youtube-live-key-card, .youtube-live-card, -.youtube-live-ops-card { +.youtube-live-help-card, +.youtube-live-studio-card { display: grid; gap: var(--sp-md); padding-top: var(--sp-lg); padding-bottom: var(--sp-lg); } -.youtube-live-key-card__head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--sp-md); -} - .youtube-live-card__head { display: flex; align-items: flex-start; @@ -27,8 +21,7 @@ gap: var(--sp-md); } -.youtube-live-card__summary, -.youtube-live-key-card__head > div { +.youtube-live-card__summary { min-width: 0; } @@ -46,22 +39,22 @@ letter-spacing: 0; } -.youtube-live-guide { +.youtube-live-help-list { display: grid; - gap: 4px; + gap: 6px; color: var(--md-on-surface-var); font-size: var(--fs-body-sm); letter-spacing: 0; } -.youtube-live-guide span { +.youtube-live-help-list > div { min-width: 0; overflow-wrap: anywhere; } .youtube-live-key-row { display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; align-items: center; } @@ -85,14 +78,9 @@ box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-primary) 16%, transparent); } -.youtube-live-key-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.youtube-live-link-btn { - flex: 0 0 auto; +.youtube-live-studio-card { + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; } .youtube-live-pill { @@ -174,19 +162,17 @@ gap: 8px; } -.youtube-live-ops-card__title { - color: var(--md-on-surface); - font-size: var(--fs-title-sm); - font-weight: 800; - letter-spacing: 0; -} - +.youtube-live-section .smallBtn, .youtube-live-action { + display: inline-flex; + align-items: center; + justify-content: center; width: auto; min-width: 0; min-height: 36px; flex: 0 1 auto; white-space: nowrap; + padding: 0 12px; } .youtube-live-action--danger { @@ -200,15 +186,11 @@ flex-direction: column; } - .youtube-live-key-card__head, - .youtube-live-key-row { + .youtube-live-key-row, + .youtube-live-studio-card { grid-template-columns: 1fr; } - .youtube-live-key-card__head { - flex-direction: column; - } - .youtube-live-pill { max-width: none; align-self: flex-start; diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 9aba9ee74cf..dabd37b5468 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -100,7 +100,7 @@ - + @@ -706,9 +706,9 @@

Home

- - - + + + @@ -724,7 +724,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 78faad742b9..475aea40d3d 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -2,9 +2,9 @@ (function () { const YOUTUBE_GROUP = "SYS_YOUTUBE"; + const KEY_SECTION_ID = "youtubeLiveKeySection"; const CARD_ID = "youtubeLiveStatusCard"; const LIVE_CONTROL_ROOM_URL = "https://www.youtube.com/livestreaming"; - const YOUTUBE_ENCODER_HELP_URL = "https://support.google.com/youtube/answer/2907883"; const POLL_MS = 2500; const state = { timer: null, @@ -76,114 +76,124 @@ const box = itemsBox(); if (!box) return null; const existing = document.getElementById(CARD_ID); - if (existing) return existing; - - const section = document.createElement("section"); - section.id = CARD_ID; - section.className = "setting-section-block youtube-live-section"; - - const title = document.createElement("div"); - title.className = "setting-group-card__title youtube-live-section__title"; - title.textContent = text("youtube_live_title", "YouTube Live"); - section.appendChild(title); + const existingKey = document.getElementById(KEY_SECTION_ID); + if (existing && existingKey) { + if (existing.nextSibling) box.appendChild(existing); + return existing; + } + if (!existingKey) { + const keySection = document.createElement("section"); + keySection.id = KEY_SECTION_ID; + keySection.className = "setting-section-block youtube-live-section youtube-live-section--key"; const keyCard = document.createElement("div"); keyCard.className = "setting-group-card youtube-live-key-card"; keyCard.innerHTML = ` -
-
-
${escapeHtmlLocal(text("youtube_live_key_panel_title", "Stream key"))}
-
${escapeHtmlLocal(text("loading", "Loading..."))}
-
- -
-
- ${escapeHtmlLocal(text("youtube_live_key_guide_1", "YouTube Studio > Create > Go live"))} - ${escapeHtmlLocal(text("youtube_live_key_guide_2", "Live Control Room > Stream > copy Stream key"))} -
+
${escapeHtmlLocal(text("youtube_live_key_panel_title", "Stream key"))}
+
${escapeHtmlLocal(text("loading", "Loading..."))}
-
-
-
`; - section.appendChild(keyCard); - - const statusCard = document.createElement("div"); - statusCard.className = "setting-group-card youtube-live-card youtube-live-status-card"; - section.appendChild(statusCard); - - const head = document.createElement("div"); - head.className = "youtube-live-card__head"; - const summary = document.createElement("div"); - summary.className = "youtube-live-card__summary"; - const summaryTitle = document.createElement("div"); - summaryTitle.className = "youtube-live-card__title"; - summaryTitle.dataset.role = "title"; - summaryTitle.textContent = text("youtube_live_phase1_source", "Phase 1 - qRoadEncodeData"); - const summaryDesc = document.createElement("div"); - summaryDesc.className = "youtube-live-card__desc"; - summaryDesc.dataset.role = "configured"; - summaryDesc.textContent = text("loading", "Loading..."); - summary.appendChild(summaryTitle); - summary.appendChild(summaryDesc); - const pill = document.createElement("span"); - pill.className = "youtube-live-pill"; - pill.dataset.role = "state"; - pill.textContent = "-"; - head.appendChild(summary); - head.appendChild(pill); - statusCard.appendChild(head); - - const metrics = document.createElement("div"); - metrics.className = "youtube-live-metrics"; - metrics.dataset.role = "metrics"; - statusCard.appendChild(metrics); - - const error = document.createElement("div"); - error.className = "youtube-live-error"; - error.dataset.role = "error"; - error.hidden = true; - statusCard.appendChild(error); - - const warnings = document.createElement("div"); - warnings.className = "youtube-live-warnings"; - warnings.dataset.role = "warnings"; - warnings.hidden = true; - statusCard.appendChild(warnings); - - const opsCard = document.createElement("div"); - opsCard.className = "setting-group-card youtube-live-ops-card"; - const opsTitle = document.createElement("div"); - opsTitle.className = "youtube-live-ops-card__title"; - opsTitle.textContent = text("youtube_live_operations", "Operations"); - opsCard.appendChild(opsTitle); - - const actions = document.createElement("div"); - actions.className = "youtube-live-actions"; - actions.appendChild(makeButton(text("youtube_live_test", "Test"), testConfig)); - actions.appendChild(makeButton(text("youtube_live_diagnostics", "Diagnostics"), showDiagnostics)); - actions.appendChild(makeButton(text("youtube_live_download_logs", "Download logs"), downloadDiagnostics)); - actions.appendChild(makeButton(text("youtube_live_stop", "Stop"), stopStream)); - actions.appendChild(makeButton(text("youtube_live_clear_key", "Clear key"), clearKey, "danger")); - opsCard.appendChild(actions); - section.appendChild(opsCard); - - keyCard.querySelector('[data-action="open-studio"]')?.addEventListener("click", openLiveControlRoom); - keyCard.querySelector('[data-action="open-help"]')?.addEventListener("click", openYouTubeHelp); - keyCard.querySelector('[data-action="save-key"]')?.addEventListener("click", saveKeyFromInput); - keyCard.querySelector('[data-action="validate-key"]')?.addEventListener("click", validateStreamKey); - keyCard.querySelector('[data-role="key-input"]')?.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - saveKeyFromInput(); - } - }); + keySection.appendChild(keyCard); + keyCard.querySelector('[data-action="save-key"]')?.addEventListener("click", saveKeyFromInput); + keyCard.querySelector('[data-action="validate-key"]')?.addEventListener("click", validateStreamKey); + keyCard.querySelector('[data-role="key-input"]')?.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + saveKeyFromInput(); + } + }); + box.insertBefore(keySection, box.firstChild); + } + + const section = existing || document.createElement("section"); + if (!existing) { + section.id = CARD_ID; + section.className = "setting-section-block youtube-live-section youtube-live-section--info"; + + const helpCard = document.createElement("div"); + helpCard.className = "setting-group-card youtube-live-help-card"; + helpCard.innerHTML = ` +
${escapeHtmlLocal(text("youtube_live_help", "Help"))}
+
+
${escapeHtmlLocal(text("youtube_live_help_step_1", "1. Open YouTube Studio and click Create > Go live."))}
+
${escapeHtmlLocal(text("youtube_live_help_step_2", "2. In Live Control Room, open Stream and copy Stream key."))}
+
${escapeHtmlLocal(text("youtube_live_help_step_3", "3. Paste the key here, save it, then run Validate key."))}
+
${escapeHtmlLocal(text("youtube_live_help_note", "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts."))}
+
+ `; + section.appendChild(helpCard); + + const studioCard = document.createElement("div"); + studioCard.className = "setting-group-card youtube-live-studio-card"; + studioCard.innerHTML = ` +
+
${escapeHtmlLocal(text("youtube_live_studio_title", "Studio"))}
+
${escapeHtmlLocal(text("youtube_live_studio_desc", "Open Live Control Room to copy the stream key and monitor stream health."))}
+
+ + `; + section.appendChild(studioCard); + + const statusCard = document.createElement("div"); + statusCard.className = "setting-group-card youtube-live-card youtube-live-status-card"; + section.appendChild(statusCard); + + const head = document.createElement("div"); + head.className = "youtube-live-card__head"; + const summary = document.createElement("div"); + summary.className = "youtube-live-card__summary"; + const summaryTitle = document.createElement("div"); + summaryTitle.className = "youtube-live-card__title"; + summaryTitle.dataset.role = "title"; + summaryTitle.textContent = text("youtube_live_phase_status_title", "Phase 1 / status"); + const summaryDesc = document.createElement("div"); + summaryDesc.className = "youtube-live-card__desc"; + summaryDesc.dataset.role = "configured"; + summaryDesc.textContent = text("loading", "Loading..."); + summary.appendChild(summaryTitle); + summary.appendChild(summaryDesc); + const pill = document.createElement("span"); + pill.className = "youtube-live-pill"; + pill.dataset.role = "state"; + pill.textContent = "-"; + head.appendChild(summary); + head.appendChild(pill); + statusCard.appendChild(head); + + const metrics = document.createElement("div"); + metrics.className = "youtube-live-metrics"; + metrics.dataset.role = "metrics"; + statusCard.appendChild(metrics); + + const error = document.createElement("div"); + error.className = "youtube-live-error"; + error.dataset.role = "error"; + error.hidden = true; + statusCard.appendChild(error); + + const warnings = document.createElement("div"); + warnings.className = "youtube-live-warnings"; + warnings.dataset.role = "warnings"; + warnings.hidden = true; + statusCard.appendChild(warnings); + + const actions = document.createElement("div"); + actions.className = "youtube-live-actions"; + actions.appendChild(makeButton(text("youtube_live_test", "Test"), testConfig)); + actions.appendChild(makeButton(text("youtube_live_diagnostics", "Diagnostics"), showDiagnostics)); + actions.appendChild(makeButton(text("youtube_live_download_logs", "Download logs"), downloadDiagnostics)); + actions.appendChild(makeButton(text("youtube_live_stop", "Stop"), stopStream)); + actions.appendChild(makeButton(text("youtube_live_clear_key", "Clear key"), clearKey, "danger")); + statusCard.appendChild(actions); + + studioCard.querySelector('[data-action="open-studio"]')?.addEventListener("click", openLiveControlRoom); + } - box.insertBefore(section, box.firstChild); + box.appendChild(section); return section; } @@ -201,7 +211,7 @@ const card = ensureCard(); if (!card) return; const stateEl = card.querySelector('[data-role="state"]'); - const keyStatusEl = card.querySelector('[data-role="key-status"]'); + const keyStatusEl = document.querySelector(`#${KEY_SECTION_ID} [data-role="key-status"]`); const configuredEl = card.querySelector('[data-role="configured"]'); const metricsEl = card.querySelector('[data-role="metrics"]'); const errorEl = card.querySelector('[data-role="error"]'); @@ -276,7 +286,7 @@ } function keyInputValue() { - return String(document.querySelector(`#${CARD_ID} [data-role="key-input"]`)?.value || "").trim(); + return String(document.querySelector(`#${KEY_SECTION_ID} [data-role="key-input"]`)?.value || "").trim(); } async function saveKeyFromInput() { @@ -289,7 +299,7 @@ const status = await postJson("/api/youtube_live/stream_key", { stream_key: value }); state.status = status; renderStatus(status); - const input = document.querySelector(`#${CARD_ID} [data-role="key-input"]`); + const input = document.querySelector(`#${KEY_SECTION_ID} [data-role="key-input"]`); if (input) input.value = ""; toast(text("youtube_live_key_saved", "Stream key saved")); } catch (err) { @@ -335,10 +345,6 @@ window.open(LIVE_CONTROL_ROOM_URL, "_blank", "noopener"); } - function openYouTubeHelp() { - window.open(YOUTUBE_ENCODER_HELP_URL, "_blank", "noopener"); - } - async function clearKey() { const ok = await appConfirm(text("youtube_live_clear_confirm", "Clear saved stream key?"), { title: text("youtube_live_clear_key", "Clear key"), @@ -440,6 +446,8 @@ stopPolling(); const card = document.getElementById(CARD_ID); if (card) card.remove(); + const keySection = document.getElementById(KEY_SECTION_ID); + if (keySection) keySection.remove(); } function init() { diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index 62f33287308..ab7f7ba5400 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -15,6 +15,13 @@ window.CarrotTranslations.register("en", { youtube_live_key_placeholder: "Paste stream key", youtube_live_open_studio: "Open Studio", youtube_live_help: "Help", + youtube_live_help_step_1: "1. Open YouTube Studio and click Create > Go live.", + youtube_live_help_step_2: "2. In Live Control Room, open Stream and copy Stream key.", + youtube_live_help_step_3: "3. Paste the key here, save it, then run Validate key.", + youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", + youtube_live_studio_title: "Studio", + youtube_live_studio_desc: "Open Live Control Room to copy the stream key and monitor stream health.", + youtube_live_phase_status_title: "Phase 1 / status", youtube_live_key_empty: "Enter a stream key first.", youtube_live_validate_key: "Validate key", youtube_live_validation_format: "Key format", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index 0d26a4b0cc9..441962f6546 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -15,6 +15,13 @@ window.CarrotTranslations.register("ko", { youtube_live_key_placeholder: "스트림 키 붙여넣기", youtube_live_open_studio: "Studio 열기", youtube_live_help: "도움말", + youtube_live_help_step_1: "1. YouTube Studio에서 만들기 > Go live를 엽니다.", + youtube_live_help_step_2: "2. Live Control Room의 Stream 탭에서 Stream key를 복사합니다.", + youtube_live_help_step_3: "3. 위 스트림 키 칸에 붙여넣고 저장 후 키 검증을 실행합니다.", + youtube_live_help_note: "키 검증은 로컬 형식, FFmpeg, RTMPS 접속만 확인합니다. YouTube의 키 수락 여부는 실제 송출 시작 후 확정됩니다.", + youtube_live_studio_title: "스튜디오", + youtube_live_studio_desc: "Live Control Room을 열어 스트림 키를 복사하고 스트림 상태를 확인합니다.", + youtube_live_phase_status_title: "페이즈 1 / 상태", youtube_live_key_empty: "스트림 키를 먼저 입력하세요.", youtube_live_validate_key: "키 검증", youtube_live_validation_format: "키 형식", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 2f97efec914..406e438ff9c 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -15,6 +15,13 @@ window.CarrotTranslations.register("zh", { youtube_live_key_placeholder: "Paste stream key", youtube_live_open_studio: "Open Studio", youtube_live_help: "Help", + youtube_live_help_step_1: "1. Open YouTube Studio and click Create > Go live.", + youtube_live_help_step_2: "2. In Live Control Room, open Stream and copy Stream key.", + youtube_live_help_step_3: "3. Paste the key here, save it, then run Validate key.", + youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", + youtube_live_studio_title: "Studio", + youtube_live_studio_desc: "Open Live Control Room to copy the stream key and monitor stream health.", + youtube_live_phase_status_title: "Phase 1 / status", youtube_live_key_empty: "Enter a stream key first.", youtube_live_validate_key: "Validate key", youtube_live_validation_format: "Key format", From 7ff3320b8b345136b73c8372459776a6bc98fd32 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:01:21 +0900 Subject: [PATCH 33/61] f --- common/params_keys.h | 2 + .../web/css/pages/settings/youtube_live.css | 38 ++++++++--- selfdrive/carrot/web/index.html | 10 +-- .../web/js/pages/setting_youtube_live.js | 65 ++++++++++++------- selfdrive/carrot/web/js/translations/en.js | 14 +++- selfdrive/carrot/web/js/translations/ko.js | 16 ++++- selfdrive/carrot/web/js/translations/zh.js | 14 +++- 7 files changed, 116 insertions(+), 43 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index bc4bd8ffe17..0f651a838da 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -183,6 +183,8 @@ inline static std::unordered_map keys = { {"ClusterHudRadarDisplay", {PERSISTENT, INT, "0"}}, {"ClusterHudRadarSourceColor", {PERSISTENT, INT, "0"}}, {"RecordRoadCam", {PERSISTENT, INT, "0"}}, + {"CarrotYouTubeLive", {PERSISTENT, INT, "0"}}, + {"CarrotYouTubeQuality", {PERSISTENT, INT, "0"}}, {"HDPuse", {PERSISTENT, INT, "0"}}, {"AutoCruiseControl", {PERSISTENT, INT, "0"}}, diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css index 5802f3f330d..c9f0d216f1f 100644 --- a/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -6,8 +6,7 @@ .youtube-live-key-card, .youtube-live-card, -.youtube-live-help-card, -.youtube-live-studio-card { +.youtube-live-help-card { display: grid; gap: var(--sp-md); padding-top: var(--sp-lg); @@ -54,7 +53,7 @@ .youtube-live-key-row { display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; + grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 8px; align-items: center; } @@ -78,11 +77,6 @@ box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-primary) 16%, transparent); } -.youtube-live-studio-card { - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; -} - .youtube-live-pill { flex: 0 0 auto; max-width: 44%; @@ -162,6 +156,23 @@ gap: 8px; } +.youtube-live-menu { + border-top: 1px solid color-mix(in srgb, var(--md-outline-var) 20%, transparent); + padding-top: var(--sp-sm); +} + +.youtube-live-menu > summary { + cursor: pointer; + color: var(--md-on-surface); + font-size: var(--fs-body); + font-weight: 800; + list-style-position: inside; +} + +.youtube-live-actions--menu { + margin-top: var(--sp-sm); +} + .youtube-live-section .smallBtn, .youtube-live-action { display: inline-flex; @@ -187,10 +198,19 @@ } .youtube-live-key-row, - .youtube-live-studio-card { + .youtube-live-actions--menu { grid-template-columns: 1fr; } + .youtube-live-key-row { + display: grid; + } + + .youtube-live-section .smallBtn, + .youtube-live-action { + width: 100%; + } + .youtube-live-pill { max-width: none; align-self: flex-start; diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index dabd37b5468..e1e9dfbe9f9 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -100,7 +100,7 @@ - + @@ -706,9 +706,9 @@

Home

- - - + + + @@ -724,7 +724,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 475aea40d3d..50334273d3b 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -94,11 +94,13 @@
+
`; keySection.appendChild(keyCard); keyCard.querySelector('[data-action="save-key"]')?.addEventListener("click", saveKeyFromInput); + keyCard.querySelector('[data-action="clear-key"]')?.addEventListener("click", clearKey); keyCard.querySelector('[data-action="validate-key"]')?.addEventListener("click", validateStreamKey); keyCard.querySelector('[data-role="key-input"]')?.addEventListener("keydown", (event) => { if (event.key === "Enter") { @@ -122,21 +124,24 @@
${escapeHtmlLocal(text("youtube_live_help_step_1", "1. Open YouTube Studio and click Create > Go live."))}
${escapeHtmlLocal(text("youtube_live_help_step_2", "2. In Live Control Room, open Stream and copy Stream key."))}
${escapeHtmlLocal(text("youtube_live_help_step_3", "3. Paste the key here, save it, then run Validate key."))}
+
${escapeHtmlLocal(text("youtube_live_help_step_4", "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps."))}
${escapeHtmlLocal(text("youtube_live_help_note", "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts."))}
+
+ ${escapeHtmlLocal(text("youtube_live_menu_title", "Actions"))} +
+
`; section.appendChild(helpCard); - const studioCard = document.createElement("div"); - studioCard.className = "setting-group-card youtube-live-studio-card"; - studioCard.innerHTML = ` -
-
${escapeHtmlLocal(text("youtube_live_studio_title", "Studio"))}
-
${escapeHtmlLocal(text("youtube_live_studio_desc", "Open Live Control Room to copy the stream key and monitor stream health."))}
-
- - `; - section.appendChild(studioCard); + const menuActions = helpCard.querySelector('[data-role="action-menu"]'); + if (menuActions) { + menuActions.appendChild(makeButton(text("youtube_live_open_studio", "Open Studio"), openLiveControlRoom)); + menuActions.appendChild(makeButton(text("youtube_live_test", "Test"), testConfig)); + menuActions.appendChild(makeButton(text("youtube_live_diagnostics", "Diagnostics"), showDiagnostics)); + menuActions.appendChild(makeButton(text("youtube_live_download_logs", "Download logs"), downloadDiagnostics)); + menuActions.appendChild(makeButton(text("youtube_live_stop", "Stop"), stopStream)); + } const statusCard = document.createElement("div"); statusCard.className = "setting-group-card youtube-live-card youtube-live-status-card"; @@ -181,16 +186,6 @@ warnings.hidden = true; statusCard.appendChild(warnings); - const actions = document.createElement("div"); - actions.className = "youtube-live-actions"; - actions.appendChild(makeButton(text("youtube_live_test", "Test"), testConfig)); - actions.appendChild(makeButton(text("youtube_live_diagnostics", "Diagnostics"), showDiagnostics)); - actions.appendChild(makeButton(text("youtube_live_download_logs", "Download logs"), downloadDiagnostics)); - actions.appendChild(makeButton(text("youtube_live_stop", "Stop"), stopStream)); - actions.appendChild(makeButton(text("youtube_live_clear_key", "Clear key"), clearKey, "danger")); - statusCard.appendChild(actions); - - studioCard.querySelector('[data-action="open-studio"]')?.addEventListener("click", openLiveControlRoom); } box.appendChild(section); @@ -330,10 +325,12 @@ } function validationText(result) { + const pass = text("youtube_live_validation_pass", "OK"); + const fail = text("youtube_live_validation_fail", "FAIL"); const lines = [ - `${text("youtube_live_validation_format", "Key format")}: ${result?.format_ok ? "OK" : "FAIL"} - ${result?.format_message || "-"}`, - `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${result?.ffmpeg_available ? "OK" : "FAIL"}`, - `${text("youtube_live_validation_rtmps", "YouTube RTMPS")}: ${result?.rtmps_reachable ? "OK" : "FAIL"} - ${result?.rtmps_message || "-"}`, + `${text("youtube_live_validation_format", "Key format")}: ${result?.format_ok ? pass : fail} - ${localizedValidationMessage(result?.format_message)}`, + `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${result?.ffmpeg_available ? pass : fail}`, + `${text("youtube_live_validation_rtmps", "YouTube RTMPS")}: ${result?.rtmps_reachable ? pass : fail} - ${localizedValidationMessage(result?.rtmps_message)}`, ]; if (result?.masked_key) lines.push(`${text("youtube_live_key", "Stream key")}: ${result.masked_key}`); lines.push(""); @@ -341,6 +338,22 @@ return lines.join("\n"); } + function localizedValidationMessage(message) { + const raw = String(message || "").trim(); + if (!raw) return "-"; + const map = { + "stream key is required": text("youtube_live_validation_required", "Stream key is required."), + "stream key is too short": text("youtube_live_validation_too_short", "Stream key is too short."), + "stream key is too long": text("youtube_live_validation_too_long", "Stream key is too long."), + "stream key must not contain spaces": text("youtube_live_validation_no_spaces", "Stream key must not contain spaces."), + "stream key contains unsupported characters": text("youtube_live_validation_bad_chars", "Stream key contains unsupported characters."), + "format looks valid": text("youtube_live_validation_format_ok", "Format looks valid."), + "YouTube RTMPS ingest is reachable": text("youtube_live_validation_rtmps_ok", "YouTube RTMPS ingest is reachable."), + "missing stream key or ffmpeg": text("youtube_live_test_missing", "Missing stream key or FFmpeg."), + }; + return map[raw] || raw; + } + function openLiveControlRoom() { window.open(LIVE_CONTROL_ROOM_URL, "_blank", "noopener"); } @@ -374,9 +387,11 @@ async function testConfig() { try { const result = await postJson("/api/youtube_live/test", {}); - toast(result.message || text("youtube_live_test_ready", "Ready")); + toast(result.ok ? text("youtube_live_test_ready", "Ready") : localizedValidationMessage(result.message)); } catch (err) { - toast(err?.message || text("youtube_live_test_failed", "Test failed"), { tone: "error" }); + const payload = err?.payload || null; + const reason = payload?.message ? localizedValidationMessage(payload.message) : (err?.message || text("youtube_live_test_failed", "Test failed")); + toast(reason, { tone: "error" }); } } diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index ab7f7ba5400..e405e41ef58 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -16,8 +16,9 @@ window.CarrotTranslations.register("en", { youtube_live_open_studio: "Open Studio", youtube_live_help: "Help", youtube_live_help_step_1: "1. Open YouTube Studio and click Create > Go live.", - youtube_live_help_step_2: "2. In Live Control Room, open Stream and copy Stream key.", + youtube_live_help_step_2: "2. In Live Control Room, open the Stream tab, create a Stream key, then copy it.", youtube_live_help_step_3: "3. Paste the key here, save it, then run Validate key.", + youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", youtube_live_studio_title: "Studio", youtube_live_studio_desc: "Open Live Control Room to copy the stream key and monitor stream health.", @@ -28,8 +29,18 @@ window.CarrotTranslations.register("en", { youtube_live_validation_ffmpeg: "FFmpeg", youtube_live_validation_rtmps: "YouTube RTMPS", youtube_live_validation_note: "YouTube confirms the key only when streaming starts.", + youtube_live_validation_pass: "OK", + youtube_live_validation_fail: "FAIL", + youtube_live_validation_required: "Stream key is required.", + youtube_live_validation_too_short: "Stream key is too short.", + youtube_live_validation_too_long: "Stream key is too long.", + youtube_live_validation_no_spaces: "Stream key must not contain spaces.", + youtube_live_validation_bad_chars: "Stream key contains unsupported characters.", + youtube_live_validation_format_ok: "Format looks valid.", + youtube_live_validation_rtmps_ok: "YouTube RTMPS ingest is reachable.", youtube_live_key: "Stream key", youtube_live_operations: "Operations", + youtube_live_menu_title: "Actions", youtube_live_key_prompt: "Enter YouTube stream key.", youtube_live_set_key: "Set key", youtube_live_clear_key: "Clear key", @@ -38,6 +49,7 @@ window.CarrotTranslations.register("en", { youtube_live_key_cleared: "Stream key cleared", youtube_live_test: "Test", youtube_live_test_ready: "Ready", + youtube_live_test_missing: "Missing stream key or FFmpeg.", youtube_live_test_failed: "Test failed", youtube_live_stop: "Stop", youtube_live_stopped: "Stream stopped", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index 441962f6546..778eb1fb38e 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -15,9 +15,10 @@ window.CarrotTranslations.register("ko", { youtube_live_key_placeholder: "스트림 키 붙여넣기", youtube_live_open_studio: "Studio 열기", youtube_live_help: "도움말", - youtube_live_help_step_1: "1. YouTube Studio에서 만들기 > Go live를 엽니다.", - youtube_live_help_step_2: "2. Live Control Room의 Stream 탭에서 Stream key를 복사합니다.", + youtube_live_help_step_1: "1. YouTube Studio에서 만들기 > 라이브 스트리밍을 엽니다.", + youtube_live_help_step_2: "2. Live Control Room의 스트림 탭에서 Stream key를 생성한 뒤 복사합니다.", youtube_live_help_step_3: "3. 위 스트림 키 칸에 붙여넣고 저장 후 키 검증을 실행합니다.", + youtube_live_help_step_4: "표준 모드는 qRoadEncodeData를 사용합니다. 영상은 약 526x330/20fps이며 YouTube의 240p-720p 인코더 범위에 맞고, 오디오는 AAC 128Kbps입니다.", youtube_live_help_note: "키 검증은 로컬 형식, FFmpeg, RTMPS 접속만 확인합니다. YouTube의 키 수락 여부는 실제 송출 시작 후 확정됩니다.", youtube_live_studio_title: "스튜디오", youtube_live_studio_desc: "Live Control Room을 열어 스트림 키를 복사하고 스트림 상태를 확인합니다.", @@ -28,8 +29,18 @@ window.CarrotTranslations.register("ko", { youtube_live_validation_ffmpeg: "FFmpeg", youtube_live_validation_rtmps: "YouTube RTMPS", youtube_live_validation_note: "YouTube는 송출 시작 시점에만 키 수락 여부를 확정합니다.", + youtube_live_validation_pass: "정상", + youtube_live_validation_fail: "실패", + youtube_live_validation_required: "스트림 키가 필요합니다.", + youtube_live_validation_too_short: "스트림 키가 너무 짧습니다.", + youtube_live_validation_too_long: "스트림 키가 너무 깁니다.", + youtube_live_validation_no_spaces: "스트림 키에는 공백이 들어가면 안 됩니다.", + youtube_live_validation_bad_chars: "스트림 키에 지원하지 않는 문자가 있습니다.", + youtube_live_validation_format_ok: "형식은 정상입니다.", + youtube_live_validation_rtmps_ok: "YouTube RTMPS 서버에 접속 가능합니다.", youtube_live_key: "스트림 키", youtube_live_operations: "운영", + youtube_live_menu_title: "작업 메뉴", youtube_live_key_prompt: "YouTube 스트림 키를 입력하세요.", youtube_live_set_key: "키 설정", youtube_live_clear_key: "키 삭제", @@ -38,6 +49,7 @@ window.CarrotTranslations.register("ko", { youtube_live_key_cleared: "스트림 키 삭제됨", youtube_live_test: "테스트", youtube_live_test_ready: "준비됨", + youtube_live_test_missing: "스트림 키 또는 FFmpeg가 없습니다.", youtube_live_test_failed: "테스트 실패", youtube_live_stop: "중지", youtube_live_stopped: "송출 중지됨", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 406e438ff9c..6516991331b 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -16,8 +16,9 @@ window.CarrotTranslations.register("zh", { youtube_live_open_studio: "Open Studio", youtube_live_help: "Help", youtube_live_help_step_1: "1. Open YouTube Studio and click Create > Go live.", - youtube_live_help_step_2: "2. In Live Control Room, open Stream and copy Stream key.", + youtube_live_help_step_2: "2. In Live Control Room, open the Stream tab, create a Stream key, then copy it.", youtube_live_help_step_3: "3. Paste the key here, save it, then run Validate key.", + youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", youtube_live_studio_title: "Studio", youtube_live_studio_desc: "Open Live Control Room to copy the stream key and monitor stream health.", @@ -28,8 +29,18 @@ window.CarrotTranslations.register("zh", { youtube_live_validation_ffmpeg: "FFmpeg", youtube_live_validation_rtmps: "YouTube RTMPS", youtube_live_validation_note: "YouTube confirms the key only when streaming starts.", + youtube_live_validation_pass: "OK", + youtube_live_validation_fail: "FAIL", + youtube_live_validation_required: "Stream key is required.", + youtube_live_validation_too_short: "Stream key is too short.", + youtube_live_validation_too_long: "Stream key is too long.", + youtube_live_validation_no_spaces: "Stream key must not contain spaces.", + youtube_live_validation_bad_chars: "Stream key contains unsupported characters.", + youtube_live_validation_format_ok: "Format looks valid.", + youtube_live_validation_rtmps_ok: "YouTube RTMPS ingest is reachable.", youtube_live_key: "Stream key", youtube_live_operations: "Operations", + youtube_live_menu_title: "Actions", youtube_live_key_prompt: "Enter YouTube stream key.", youtube_live_set_key: "Set key", youtube_live_clear_key: "Clear key", @@ -38,6 +49,7 @@ window.CarrotTranslations.register("zh", { youtube_live_key_cleared: "Stream key cleared", youtube_live_test: "Test", youtube_live_test_ready: "Ready", + youtube_live_test_missing: "Missing stream key or FFmpeg.", youtube_live_test_failed: "Test failed", youtube_live_stop: "Stop", youtube_live_stopped: "Stream stopped", From 30535b36c319fb87d98b7c4121da0b5b015a120e Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:06:22 +0900 Subject: [PATCH 34/61] f --- .../carrot/server/features/youtube_live.py | 20 ----- .../carrot/server/services/youtube_live.py | 5 -- selfdrive/carrot/web/index.html | 8 +- .../web/js/pages/setting_youtube_live.js | 85 ++++++++++++------- selfdrive/carrot/web/js/translations/en.js | 7 +- selfdrive/carrot/web/js/translations/ko.js | 7 +- selfdrive/carrot/web/js/translations/zh.js | 7 +- 7 files changed, 68 insertions(+), 71 deletions(-) diff --git a/selfdrive/carrot/server/features/youtube_live.py b/selfdrive/carrot/server/features/youtube_live.py index f5fb6c352ba..f2e4c7d121b 100644 --- a/selfdrive/carrot/server/features/youtube_live.py +++ b/selfdrive/carrot/server/features/youtube_live.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import asyncio from aiohttp import web @@ -50,11 +49,6 @@ async def api_clear_stream_key(request: web.Request) -> web.Response: return web.json_response({"ok": True, **_service(request).clear_stream_key()}) -async def api_stop(request: web.Request) -> web.Response: - status = await _service(request).stop_stream() - return web.json_response({"ok": True, **status}) - - async def api_test(request: web.Request) -> web.Response: result = _service(request).test_config() return web.json_response(result, status=200 if result.get("ok") else 409) @@ -76,25 +70,11 @@ async def api_diagnostics(request: web.Request) -> web.Response: return web.json_response({"ok": True, "diagnostics": _service(request).diagnostics()}) -async def api_download_diagnostics(request: web.Request) -> web.Response: - body = json.dumps(_service(request).diagnostics(), ensure_ascii=False, indent=2, sort_keys=True) - return web.Response( - text=body, - content_type="application/json", - headers={ - "Cache-Control": "no-store", - "Content-Disposition": "attachment; filename=carrot-youtube-live-diagnostics.json", - }, - ) - - def register(app: web.Application) -> None: app.cleanup_ctx.append(youtube_live_context) app.router.add_get("/api/youtube_live/status", api_status) app.router.add_get("/api/youtube_live/diagnostics", api_diagnostics) - app.router.add_get("/api/youtube_live/diagnostics/download", api_download_diagnostics) app.router.add_post("/api/youtube_live/stream_key", api_set_stream_key) app.router.add_post("/api/youtube_live/stream_key/validate", api_validate_stream_key) app.router.add_delete("/api/youtube_live/stream_key", api_clear_stream_key) - app.router.add_post("/api/youtube_live/stop", api_stop) app.router.add_post("/api/youtube_live/test", api_test) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index d655361bc14..b46f294c7db 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -161,11 +161,6 @@ async def stop(self) -> None: self._task = None self._write_status(force=True) - async def stop_stream(self) -> dict[str, Any]: - await self._stop_process() - self._set_state("disabled") - return self.status() - def status(self) -> dict[str, Any]: stream_key = self.get_stream_key() pid = self._process.pid if self._process and self._process.returncode is None else None diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index e1e9dfbe9f9..d6a43451a15 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -706,9 +706,9 @@

Home

- - - + + + @@ -724,7 +724,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 50334273d3b..6399ab23bfd 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -137,10 +137,7 @@ const menuActions = helpCard.querySelector('[data-role="action-menu"]'); if (menuActions) { menuActions.appendChild(makeButton(text("youtube_live_open_studio", "Open Studio"), openLiveControlRoom)); - menuActions.appendChild(makeButton(text("youtube_live_test", "Test"), testConfig)); - menuActions.appendChild(makeButton(text("youtube_live_diagnostics", "Diagnostics"), showDiagnostics)); - menuActions.appendChild(makeButton(text("youtube_live_download_logs", "Download logs"), downloadDiagnostics)); - menuActions.appendChild(makeButton(text("youtube_live_stop", "Stop"), stopStream)); + menuActions.appendChild(makeButton(text("youtube_live_check", "Check"), runCheck)); } const statusCard = document.createElement("div"); @@ -350,6 +347,7 @@ "format looks valid": text("youtube_live_validation_format_ok", "Format looks valid."), "YouTube RTMPS ingest is reachable": text("youtube_live_validation_rtmps_ok", "YouTube RTMPS ingest is reachable."), "missing stream key or ffmpeg": text("youtube_live_test_missing", "Missing stream key or FFmpeg."), + "ready": text("youtube_live_check_ready", "Ready"), }; return map[raw] || raw; } @@ -373,26 +371,51 @@ } } - async function stopStream() { + async function fetchTestResult() { try { - const status = await postJson("/api/youtube_live/stop", {}); - state.status = status; - renderStatus(status); - toast(text("youtube_live_stopped", "Stream stopped")); + const result = await postJson("/api/youtube_live/test", {}); + return { ok: Boolean(result?.ok), payload: result }; } catch (err) { - toast(err?.message || String(err), { tone: "error" }); + const payload = err?.payload || null; + return { + ok: false, + payload, + error: payload?.message ? localizedValidationMessage(payload.message) : (err?.message || text("youtube_live_test_failed", "Test failed")), + }; } } - async function testConfig() { + async function fetchDiagnosticsResult() { try { - const result = await postJson("/api/youtube_live/test", {}); - toast(result.ok ? text("youtube_live_test_ready", "Ready") : localizedValidationMessage(result.message)); + const payload = await getJson("/api/youtube_live/diagnostics"); + return { ok: true, payload }; } catch (err) { - const payload = err?.payload || null; - const reason = payload?.message ? localizedValidationMessage(payload.message) : (err?.message || text("youtube_live_test_failed", "Test failed")); - toast(reason, { tone: "error" }); + return { ok: false, error: err?.message || String(err) }; + } + } + + function checkText(testResult, diagnosticsResult) { + const pass = text("youtube_live_validation_pass", "OK"); + const fail = text("youtube_live_validation_fail", "FAIL"); + const test = testResult?.payload || {}; + const lines = [ + `${text("youtube_live_test", "Test")}: ${testResult?.ok ? pass : fail} - ${localizedValidationMessage(test.message || testResult?.error)}`, + `${text("youtube_live_key", "Stream key")}: ${test.configured ? pass : fail}`, + `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${test.ffmpeg_available ? pass : fail}`, + `${text("youtube_live_metric_source", "Source")}: ${test.source || "-"}`, + ]; + if (Array.isArray(test.warnings) && test.warnings.length) { + lines.push(""); + lines.push(text("youtube_live_warnings", "Warnings")); + test.warnings.forEach((warning) => lines.push(`- ${warning}`)); + } + lines.push(""); + if (diagnosticsResult?.ok) { + lines.push(diagnosticText(diagnosticsResult.payload)); + } else { + lines.push(`${text("youtube_live_diagnostics", "Diagnostics")}: ${fail} - ${diagnosticsResult?.error || "-"}`); } + return lines.join("\n"); } function diagnosticText(payload) { @@ -425,21 +448,23 @@ return lines.join("\n"); } - async function showDiagnostics() { - try { - const payload = await getJson("/api/youtube_live/diagnostics"); - const raw = JSON.stringify(payload.diagnostics || payload, null, 2); - await appAlert(diagnosticText(payload), { - title: text("youtube_live_diagnostics", "Diagnostics"), - copyText: raw, - }); - } catch (err) { - toast(err?.message || String(err), { tone: "error" }); + async function runCheck() { + const [testResult, diagnosticsResult] = await Promise.all([ + fetchTestResult(), + fetchDiagnosticsResult(), + ]); + const status = diagnosticsResult?.payload?.diagnostics?.status; + if (status) { + state.status = status; + renderStatus(status); } - } - - function downloadDiagnostics() { - window.open("/api/youtube_live/diagnostics/download", "_blank", "noopener"); + await appAlert(checkText(testResult, diagnosticsResult), { + title: text("youtube_live_check_result", "Check result"), + copyText: JSON.stringify({ + test: testResult.payload || { ok: false, error: testResult.error }, + diagnostics: diagnosticsResult.payload?.diagnostics || { ok: false, error: diagnosticsResult.error }, + }, null, 2), + }); } function startPolling() { diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index e405e41ef58..f62793fa2ce 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -47,12 +47,12 @@ window.CarrotTranslations.register("en", { youtube_live_clear_confirm: "Clear saved stream key?", youtube_live_key_saved: "Stream key saved", youtube_live_key_cleared: "Stream key cleared", + youtube_live_check: "Check", + youtube_live_check_result: "Check result", + youtube_live_check_ready: "Ready", youtube_live_test: "Test", - youtube_live_test_ready: "Ready", youtube_live_test_missing: "Missing stream key or FFmpeg.", youtube_live_test_failed: "Test failed", - youtube_live_stop: "Stop", - youtube_live_stopped: "Stream stopped", youtube_live_metric_source: "Source", youtube_live_metric_runtime: "Runtime", youtube_live_metric_bitrate: "Bitrate", @@ -61,7 +61,6 @@ window.CarrotTranslations.register("en", { youtube_live_metric_retry: "Retry", youtube_live_retry_in: "{seconds}s", youtube_live_diagnostics: "Diagnostics", - youtube_live_download_logs: "Download logs", youtube_live_state_label: "State", youtube_live_warnings: "Warnings", youtube_live_state_backoff: "Backoff", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index 778eb1fb38e..f143953d639 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -47,12 +47,12 @@ window.CarrotTranslations.register("ko", { youtube_live_clear_confirm: "저장된 스트림 키를 삭제할까요?", youtube_live_key_saved: "스트림 키 저장됨", youtube_live_key_cleared: "스트림 키 삭제됨", + youtube_live_check: "점검", + youtube_live_check_result: "점검 결과", + youtube_live_check_ready: "준비됨", youtube_live_test: "테스트", - youtube_live_test_ready: "준비됨", youtube_live_test_missing: "스트림 키 또는 FFmpeg가 없습니다.", youtube_live_test_failed: "테스트 실패", - youtube_live_stop: "중지", - youtube_live_stopped: "송출 중지됨", youtube_live_metric_source: "소스", youtube_live_metric_runtime: "시간", youtube_live_metric_bitrate: "비트레이트", @@ -61,7 +61,6 @@ window.CarrotTranslations.register("ko", { youtube_live_metric_retry: "재시도", youtube_live_retry_in: "{seconds}초", youtube_live_diagnostics: "진단", - youtube_live_download_logs: "로그 다운로드", youtube_live_state_label: "상태", youtube_live_warnings: "경고", youtube_live_state_backoff: "재시도 대기", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 6516991331b..0f02697acaa 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -47,12 +47,12 @@ window.CarrotTranslations.register("zh", { youtube_live_clear_confirm: "Clear saved stream key?", youtube_live_key_saved: "Stream key saved", youtube_live_key_cleared: "Stream key cleared", + youtube_live_check: "Check", + youtube_live_check_result: "Check result", + youtube_live_check_ready: "Ready", youtube_live_test: "Test", - youtube_live_test_ready: "Ready", youtube_live_test_missing: "Missing stream key or FFmpeg.", youtube_live_test_failed: "Test failed", - youtube_live_stop: "Stop", - youtube_live_stopped: "Stream stopped", youtube_live_metric_source: "Source", youtube_live_metric_runtime: "Runtime", youtube_live_metric_bitrate: "Bitrate", @@ -61,7 +61,6 @@ window.CarrotTranslations.register("zh", { youtube_live_metric_retry: "Retry", youtube_live_retry_in: "{seconds}s", youtube_live_diagnostics: "Diagnostics", - youtube_live_download_logs: "Download logs", youtube_live_state_label: "State", youtube_live_warnings: "Warnings", youtube_live_state_backoff: "Backoff", From 8351a3636a65e40588cbf50d915107be5fcdfdc2 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:10:07 +0900 Subject: [PATCH 35/61] f --- .../web/css/pages/settings/youtube_live.css | 51 ++++++------------- selfdrive/carrot/web/index.html | 10 ++-- .../web/js/pages/setting_youtube_live.js | 23 ++++----- selfdrive/carrot/web/js/translations/en.js | 14 ++--- selfdrive/carrot/web/js/translations/ko.js | 14 ++--- selfdrive/carrot/web/js/translations/zh.js | 14 ++--- 6 files changed, 39 insertions(+), 87 deletions(-) diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css index c9f0d216f1f..6e511562993 100644 --- a/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -40,17 +40,26 @@ .youtube-live-help-list { display: grid; - gap: 6px; + gap: var(--sp-sm); + margin: 0; + padding-left: 1.35em; color: var(--md-on-surface-var); font-size: var(--fs-body-sm); letter-spacing: 0; } -.youtube-live-help-list > div { +.youtube-live-help-list > li, +.youtube-live-help-note { min-width: 0; overflow-wrap: anywhere; } +.youtube-live-help-note { + color: var(--md-on-surface-var); + font-size: var(--fs-body-sm); + letter-spacing: 0; +} + .youtube-live-key-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; @@ -151,30 +160,10 @@ } .youtube-live-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.youtube-live-menu { - border-top: 1px solid color-mix(in srgb, var(--md-outline-var) 20%, transparent); - padding-top: var(--sp-sm); -} - -.youtube-live-menu > summary { - cursor: pointer; - color: var(--md-on-surface); - font-size: var(--fs-body); - font-weight: 800; - list-style-position: inside; + --ui-action-min: 136px; } -.youtube-live-actions--menu { - margin-top: var(--sp-sm); -} - -.youtube-live-section .smallBtn, -.youtube-live-action { +.youtube-live-key-row .smallBtn { display: inline-flex; align-items: center; justify-content: center; @@ -186,28 +175,18 @@ padding: 0 12px; } -.youtube-live-action--danger { - color: var(--md-error); - border-color: color-mix(in srgb, var(--md-error) 35%, var(--md-outline-var)); -} - @media (max-width: 430px) { .youtube-live-card__head { align-items: stretch; flex-direction: column; } - .youtube-live-key-row, - .youtube-live-actions--menu { - grid-template-columns: 1fr; - } - .youtube-live-key-row { display: grid; + grid-template-columns: 1fr; } - .youtube-live-section .smallBtn, - .youtube-live-action { + .youtube-live-key-row .smallBtn { width: 100%; } diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index d6a43451a15..fba532e614a 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -100,7 +100,7 @@ - + @@ -706,9 +706,9 @@

Home

- - - + + + @@ -724,7 +724,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 6399ab23bfd..60c4c7f699f 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -66,7 +66,7 @@ function makeButton(label, action, tone) { const button = document.createElement("button"); button.type = "button"; - button.className = `smallBtn youtube-live-action${tone ? ` youtube-live-action--${tone}` : ""}`; + button.className = `smallBtn${tone ? ` btn--${tone}` : ""}`; button.textContent = label; button.addEventListener("click", action); return button; @@ -94,7 +94,7 @@
- +
`; @@ -120,17 +120,14 @@ helpCard.className = "setting-group-card youtube-live-help-card"; helpCard.innerHTML = `
${escapeHtmlLocal(text("youtube_live_help", "Help"))}
-
-
${escapeHtmlLocal(text("youtube_live_help_step_1", "1. Open YouTube Studio and click Create > Go live."))}
-
${escapeHtmlLocal(text("youtube_live_help_step_2", "2. In Live Control Room, open Stream and copy Stream key."))}
-
${escapeHtmlLocal(text("youtube_live_help_step_3", "3. Paste the key here, save it, then run Validate key."))}
-
${escapeHtmlLocal(text("youtube_live_help_step_4", "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps."))}
-
${escapeHtmlLocal(text("youtube_live_help_note", "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts."))}
-
-
- ${escapeHtmlLocal(text("youtube_live_menu_title", "Actions"))} -
-
+
    +
  1. ${escapeHtmlLocal(text("youtube_live_help_step_1", "Open YouTube Studio and click Create > Go live."))}
  2. +
  3. ${escapeHtmlLocal(text("youtube_live_help_step_2", "In Live Control Room, open Stream and copy Stream key."))}
  4. +
  5. ${escapeHtmlLocal(text("youtube_live_help_step_3", "Paste the key here, save it, then run Validate key."))}
  6. +
+
${escapeHtmlLocal(text("youtube_live_help_step_4", "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps."))}
+
${escapeHtmlLocal(text("youtube_live_help_note", "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts."))}
+
`; section.appendChild(helpCard); diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index f62793fa2ce..5fe8df6f163 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -10,18 +10,14 @@ window.CarrotTranslations.register("en", { youtube_live_key_configured: "Key saved: {key}", youtube_live_key_missing: "Stream key is not saved", youtube_live_key_panel_title: "Stream key", - youtube_live_key_guide_1: "YouTube Studio > Create > Go live", - youtube_live_key_guide_2: "Live Control Room > Stream > copy Stream key", youtube_live_key_placeholder: "Paste stream key", youtube_live_open_studio: "Open Studio", youtube_live_help: "Help", - youtube_live_help_step_1: "1. Open YouTube Studio and click Create > Go live.", - youtube_live_help_step_2: "2. In Live Control Room, open the Stream tab, create a Stream key, then copy it.", - youtube_live_help_step_3: "3. Paste the key here, save it, then run Validate key.", + youtube_live_help_step_1: "Open YouTube Studio and click Create > Go live.", + youtube_live_help_step_2: "In Live Control Room, open the Stream tab, create a Stream key, then copy it.", + youtube_live_help_step_3: "Paste the key here, save it, then run Validate key.", youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", - youtube_live_studio_title: "Studio", - youtube_live_studio_desc: "Open Live Control Room to copy the stream key and monitor stream health.", youtube_live_phase_status_title: "Phase 1 / status", youtube_live_key_empty: "Enter a stream key first.", youtube_live_validate_key: "Validate key", @@ -39,10 +35,6 @@ window.CarrotTranslations.register("en", { youtube_live_validation_format_ok: "Format looks valid.", youtube_live_validation_rtmps_ok: "YouTube RTMPS ingest is reachable.", youtube_live_key: "Stream key", - youtube_live_operations: "Operations", - youtube_live_menu_title: "Actions", - youtube_live_key_prompt: "Enter YouTube stream key.", - youtube_live_set_key: "Set key", youtube_live_clear_key: "Clear key", youtube_live_clear_confirm: "Clear saved stream key?", youtube_live_key_saved: "Stream key saved", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index f143953d639..d5e59a6bbbb 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -10,18 +10,14 @@ window.CarrotTranslations.register("ko", { youtube_live_key_configured: "키 저장됨: {key}", youtube_live_key_missing: "스트림 키가 저장되지 않음", youtube_live_key_panel_title: "스트림 키", - youtube_live_key_guide_1: "YouTube Studio > 만들기 > Go live", - youtube_live_key_guide_2: "Live Control Room > Stream > Stream key 복사", youtube_live_key_placeholder: "스트림 키 붙여넣기", youtube_live_open_studio: "Studio 열기", youtube_live_help: "도움말", - youtube_live_help_step_1: "1. YouTube Studio에서 만들기 > 라이브 스트리밍을 엽니다.", - youtube_live_help_step_2: "2. Live Control Room의 스트림 탭에서 Stream key를 생성한 뒤 복사합니다.", - youtube_live_help_step_3: "3. 위 스트림 키 칸에 붙여넣고 저장 후 키 검증을 실행합니다.", + youtube_live_help_step_1: "YouTube Studio에서 만들기 > 라이브 스트리밍을 엽니다.", + youtube_live_help_step_2: "Live Control Room의 스트림 탭에서 Stream key를 생성한 뒤 복사합니다.", + youtube_live_help_step_3: "위 스트림 키 칸에 붙여넣고 저장 후 키 검증을 실행합니다.", youtube_live_help_step_4: "표준 모드는 qRoadEncodeData를 사용합니다. 영상은 약 526x330/20fps이며 YouTube의 240p-720p 인코더 범위에 맞고, 오디오는 AAC 128Kbps입니다.", youtube_live_help_note: "키 검증은 로컬 형식, FFmpeg, RTMPS 접속만 확인합니다. YouTube의 키 수락 여부는 실제 송출 시작 후 확정됩니다.", - youtube_live_studio_title: "스튜디오", - youtube_live_studio_desc: "Live Control Room을 열어 스트림 키를 복사하고 스트림 상태를 확인합니다.", youtube_live_phase_status_title: "페이즈 1 / 상태", youtube_live_key_empty: "스트림 키를 먼저 입력하세요.", youtube_live_validate_key: "키 검증", @@ -39,10 +35,6 @@ window.CarrotTranslations.register("ko", { youtube_live_validation_format_ok: "형식은 정상입니다.", youtube_live_validation_rtmps_ok: "YouTube RTMPS 서버에 접속 가능합니다.", youtube_live_key: "스트림 키", - youtube_live_operations: "운영", - youtube_live_menu_title: "작업 메뉴", - youtube_live_key_prompt: "YouTube 스트림 키를 입력하세요.", - youtube_live_set_key: "키 설정", youtube_live_clear_key: "키 삭제", youtube_live_clear_confirm: "저장된 스트림 키를 삭제할까요?", youtube_live_key_saved: "스트림 키 저장됨", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 0f02697acaa..b793c3bee97 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -10,18 +10,14 @@ window.CarrotTranslations.register("zh", { youtube_live_key_configured: "Key saved: {key}", youtube_live_key_missing: "Stream key is not saved", youtube_live_key_panel_title: "Stream key", - youtube_live_key_guide_1: "YouTube Studio > Create > Go live", - youtube_live_key_guide_2: "Live Control Room > Stream > copy Stream key", youtube_live_key_placeholder: "Paste stream key", youtube_live_open_studio: "Open Studio", youtube_live_help: "Help", - youtube_live_help_step_1: "1. Open YouTube Studio and click Create > Go live.", - youtube_live_help_step_2: "2. In Live Control Room, open the Stream tab, create a Stream key, then copy it.", - youtube_live_help_step_3: "3. Paste the key here, save it, then run Validate key.", + youtube_live_help_step_1: "Open YouTube Studio and click Create > Go live.", + youtube_live_help_step_2: "In Live Control Room, open the Stream tab, create a Stream key, then copy it.", + youtube_live_help_step_3: "Paste the key here, save it, then run Validate key.", youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", - youtube_live_studio_title: "Studio", - youtube_live_studio_desc: "Open Live Control Room to copy the stream key and monitor stream health.", youtube_live_phase_status_title: "Phase 1 / status", youtube_live_key_empty: "Enter a stream key first.", youtube_live_validate_key: "Validate key", @@ -39,10 +35,6 @@ window.CarrotTranslations.register("zh", { youtube_live_validation_format_ok: "Format looks valid.", youtube_live_validation_rtmps_ok: "YouTube RTMPS ingest is reachable.", youtube_live_key: "Stream key", - youtube_live_operations: "Operations", - youtube_live_menu_title: "Actions", - youtube_live_key_prompt: "Enter YouTube stream key.", - youtube_live_set_key: "Set key", youtube_live_clear_key: "Clear key", youtube_live_clear_confirm: "Clear saved stream key?", youtube_live_key_saved: "Stream key saved", From d7c6dbf118c0009f1fdb008188969ec72c2ccc53 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:18:12 +0900 Subject: [PATCH 36/61] f --- selfdrive/carrot/web/index.html | 10 +-- selfdrive/carrot/web/js/pages/setting.js | 1 + .../web/js/pages/setting_youtube_live.js | 87 ++++++++++++++++--- selfdrive/carrot/web/js/translations/en.js | 8 +- selfdrive/carrot/web/js/translations/ko.js | 8 +- selfdrive/carrot/web/js/translations/zh.js | 8 +- selfdrive/carrot_settings.json | 34 +++++--- 7 files changed, 118 insertions(+), 38 deletions(-) diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index fba532e614a..d02c5f6cfc4 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -706,9 +706,9 @@

Home

- - - + + + @@ -723,8 +723,8 @@

Home

- - + + diff --git a/selfdrive/carrot/web/js/pages/setting.js b/selfdrive/carrot/web/js/pages/setting.js index 0c38db1ecd6..ec81157a5d2 100644 --- a/selfdrive/carrot/web/js/pages/setting.js +++ b/selfdrive/carrot/web/js/pages/setting.js @@ -2666,6 +2666,7 @@ async function renderItems(group, options = {}) { lastCategorySectionKey = secKey; const sectionBlock = document.createElement("section"); sectionBlock.className = animateItems ? "setting-section-block ui-stagger-item" : "setting-section-block"; + sectionBlock.dataset.settingSectionId = secKey; if (animateItems) sectionBlock.style.setProperty("--i", String(Math.min(index + 1, 14))); const cardLabel = settingNodeLabel(p.__section); if (cardLabel) { diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 60c4c7f699f..f8fe13cf9a9 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -1,7 +1,8 @@ "use strict"; (function () { - const YOUTUBE_GROUP = "SYS_YOUTUBE"; + const YOUTUBE_GROUP = "SYS_RECORD"; + const YOUTUBE_SECTION = "SYS_YOUTUBE"; const KEY_SECTION_ID = "youtubeLiveKeySection"; const CARD_ID = "youtubeLiveStatusCard"; const LIVE_CONTROL_ROOM_URL = "https://www.youtube.com/livestreaming"; @@ -49,6 +50,19 @@ return text("youtube_live_key_missing", "Stream key is not saved"); } + function streamTitle(status) { + return status?.enabled + ? text("youtube_live_stream_active", "Streaming") + : text("youtube_live_stream_inactive", "Stream stopped"); + } + + function streamSummary(status) { + const quality = text("youtube_live_quality_standard", "Standard"); + const source = status?.source || "qRoadEncodeData"; + const visibility = text("youtube_live_visibility_studio", "Visibility: set in YouTube Studio"); + return `${quality} · ${source} · ${visibility}`; + } + function metric(label, value) { const item = document.createElement("div"); item.className = "youtube-live-metric"; @@ -72,13 +86,24 @@ return button; } + function placeYoutubeSections(box, keySection, infoSection) { + const target = box.querySelector(`[data-setting-section-id="${YOUTUBE_SECTION}"]`); + if (!target) { + box.insertBefore(keySection, box.firstChild); + box.appendChild(infoSection); + return; + } + box.insertBefore(keySection, target); + box.insertBefore(infoSection, target.nextSibling); + } + function ensureCard() { const box = itemsBox(); if (!box) return null; const existing = document.getElementById(CARD_ID); const existingKey = document.getElementById(KEY_SECTION_ID); if (existing && existingKey) { - if (existing.nextSibling) box.appendChild(existing); + placeYoutubeSections(box, existingKey, existing); return existing; } @@ -92,7 +117,7 @@
${escapeHtmlLocal(text("youtube_live_key_panel_title", "Stream key"))}
${escapeHtmlLocal(text("loading", "Loading..."))}
- + @@ -108,7 +133,6 @@ saveKeyFromInput(); } }); - box.insertBefore(keySection, box.firstChild); } const section = existing || document.createElement("section"); @@ -148,10 +172,10 @@ const summaryTitle = document.createElement("div"); summaryTitle.className = "youtube-live-card__title"; summaryTitle.dataset.role = "title"; - summaryTitle.textContent = text("youtube_live_phase_status_title", "Phase 1 / status"); + summaryTitle.textContent = text("youtube_live_stream_inactive", "Stream stopped"); const summaryDesc = document.createElement("div"); summaryDesc.className = "youtube-live-card__desc"; - summaryDesc.dataset.role = "configured"; + summaryDesc.dataset.role = "summary"; summaryDesc.textContent = text("loading", "Loading..."); summary.appendChild(summaryTitle); summary.appendChild(summaryDesc); @@ -182,7 +206,8 @@ } - box.appendChild(section); + const keySection = document.getElementById(KEY_SECTION_ID); + if (keySection) placeYoutubeSections(box, keySection, section); return section; } @@ -199,16 +224,18 @@ function renderStatus(status) { const card = ensureCard(); if (!card) return; + const titleEl = card.querySelector('[data-role="title"]'); const stateEl = card.querySelector('[data-role="state"]'); const keyStatusEl = document.querySelector(`#${KEY_SECTION_ID} [data-role="key-status"]`); - const configuredEl = card.querySelector('[data-role="configured"]'); + const summaryEl = card.querySelector('[data-role="summary"]'); const metricsEl = card.querySelector('[data-role="metrics"]'); const errorEl = card.querySelector('[data-role="error"]'); const warningsEl = card.querySelector('[data-role="warnings"]'); card.dataset.liveState = String(status?.state || "disabled"); + if (titleEl) titleEl.textContent = streamTitle(status); if (stateEl) stateEl.textContent = stateLabel(status); if (keyStatusEl) keyStatusEl.textContent = configuredLabel(status); - if (configuredEl) configuredEl.textContent = configuredLabel(status); + if (summaryEl) summaryEl.textContent = streamSummary(status); if (metricsEl) { metricsEl.innerHTML = ""; metricsEl.appendChild(metric(text("youtube_live_metric_source", "Source"), status?.source || "qRoadEncodeData")); @@ -290,19 +317,40 @@ renderStatus(status); const input = document.querySelector(`#${KEY_SECTION_ID} [data-role="key-input"]`); if (input) input.value = ""; - toast(text("youtube_live_key_saved", "Stream key saved")); + const validation = await fetchStreamKeyValidation(); + await appAlert(saveResultText(status, validation), { + title: text("youtube_live_save_result", "Stream key saved"), + copyText: JSON.stringify({ + status, + validation, + }, null, 2), + }); } catch (err) { toast(err?.message || String(err), { tone: "error" }); } } + async function fetchStreamKeyValidation(value) { + const payload = value ? { stream_key: value } : {}; + try { + return await postJson("/api/youtube_live/stream_key/validate", payload); + } catch (err) { + if (err?.payload) return err.payload; + return { + ok: false, + format_ok: false, + format_message: err?.message || String(err), + ffmpeg_available: false, + rtmps_reachable: false, + rtmps_message: "-", + }; + } + } + async function validateStreamKey() { const value = keyInputValue(); try { - const payload = value - ? { stream_key: value } - : {}; - const result = await postJson("/api/youtube_live/stream_key/validate", payload); + const result = await fetchStreamKeyValidation(value); await appAlert(validationText(result), { title: text("youtube_live_validate_key", "Validate key"), }); @@ -318,6 +366,17 @@ } } + function saveResultText(status, validation) { + const pass = text("youtube_live_validation_pass", "OK"); + const fail = text("youtube_live_validation_fail", "FAIL"); + const lines = [ + `${text("youtube_live_save_status", "Save")}: ${status?.configured ? pass : fail} - ${configuredLabel(status)}`, + "", + validationText(validation), + ]; + return lines.join("\n"); + } + function validationText(result) { const pass = text("youtube_live_validation_pass", "OK"); const fail = text("youtube_live_validation_fail", "FAIL"); diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index 5fe8df6f163..5b81125694c 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -18,7 +18,10 @@ window.CarrotTranslations.register("en", { youtube_live_help_step_3: "Paste the key here, save it, then run Validate key.", youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", - youtube_live_phase_status_title: "Phase 1 / status", + youtube_live_stream_active: "Streaming", + youtube_live_stream_inactive: "Stream stopped", + youtube_live_quality_standard: "Standard", + youtube_live_visibility_studio: "Visibility: set in YouTube Studio", youtube_live_key_empty: "Enter a stream key first.", youtube_live_validate_key: "Validate key", youtube_live_validation_format: "Key format", @@ -37,8 +40,9 @@ window.CarrotTranslations.register("en", { youtube_live_key: "Stream key", youtube_live_clear_key: "Clear key", youtube_live_clear_confirm: "Clear saved stream key?", - youtube_live_key_saved: "Stream key saved", youtube_live_key_cleared: "Stream key cleared", + youtube_live_save_result: "Stream key saved", + youtube_live_save_status: "Save", youtube_live_check: "Check", youtube_live_check_result: "Check result", youtube_live_check_ready: "Ready", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index d5e59a6bbbb..c6bdde385dc 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -18,7 +18,10 @@ window.CarrotTranslations.register("ko", { youtube_live_help_step_3: "위 스트림 키 칸에 붙여넣고 저장 후 키 검증을 실행합니다.", youtube_live_help_step_4: "표준 모드는 qRoadEncodeData를 사용합니다. 영상은 약 526x330/20fps이며 YouTube의 240p-720p 인코더 범위에 맞고, 오디오는 AAC 128Kbps입니다.", youtube_live_help_note: "키 검증은 로컬 형식, FFmpeg, RTMPS 접속만 확인합니다. YouTube의 키 수락 여부는 실제 송출 시작 후 확정됩니다.", - youtube_live_phase_status_title: "페이즈 1 / 상태", + youtube_live_stream_active: "송출 중", + youtube_live_stream_inactive: "송출 중지", + youtube_live_quality_standard: "표준", + youtube_live_visibility_studio: "공개 범위: YouTube Studio 설정", youtube_live_key_empty: "스트림 키를 먼저 입력하세요.", youtube_live_validate_key: "키 검증", youtube_live_validation_format: "키 형식", @@ -37,8 +40,9 @@ window.CarrotTranslations.register("ko", { youtube_live_key: "스트림 키", youtube_live_clear_key: "키 삭제", youtube_live_clear_confirm: "저장된 스트림 키를 삭제할까요?", - youtube_live_key_saved: "스트림 키 저장됨", youtube_live_key_cleared: "스트림 키 삭제됨", + youtube_live_save_result: "스트림 키 저장", + youtube_live_save_status: "저장", youtube_live_check: "점검", youtube_live_check_result: "점검 결과", youtube_live_check_ready: "준비됨", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index b793c3bee97..09eed1b20e3 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -18,7 +18,10 @@ window.CarrotTranslations.register("zh", { youtube_live_help_step_3: "Paste the key here, save it, then run Validate key.", youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", - youtube_live_phase_status_title: "Phase 1 / status", + youtube_live_stream_active: "Streaming", + youtube_live_stream_inactive: "Stream stopped", + youtube_live_quality_standard: "Standard", + youtube_live_visibility_studio: "Visibility: set in YouTube Studio", youtube_live_key_empty: "Enter a stream key first.", youtube_live_validate_key: "Validate key", youtube_live_validation_format: "Key format", @@ -37,8 +40,9 @@ window.CarrotTranslations.register("zh", { youtube_live_key: "Stream key", youtube_live_clear_key: "Clear key", youtube_live_clear_confirm: "Clear saved stream key?", - youtube_live_key_saved: "Stream key saved", youtube_live_key_cleared: "Stream key cleared", + youtube_live_save_result: "Stream key saved", + youtube_live_save_status: "Save", youtube_live_check: "Check", youtube_live_check_result: "Check result", youtube_live_check_ready: "Ready", diff --git a/selfdrive/carrot_settings.json b/selfdrive/carrot_settings.json index 733391e161e..70c8d417e43 100644 --- a/selfdrive/carrot_settings.json +++ b/selfdrive/carrot_settings.json @@ -539,19 +539,27 @@ "ko": "녹화·전원", "en": "Record & Power", "zh": "录制与电源", - "params": [ - "RecordRoadCam", - "MaxTimeOffroadMin" - ] - }, - { - "id": "SYS_YOUTUBE", - "ko": "YouTube 라이브", - "en": "YouTube Live", - "zh": "YouTube Live", - "params": [ - "CarrotYouTubeLive", - "CarrotYouTubeQuality" + "groups": [ + { + "id": "SYS_RECORD_BASIC", + "ko": "녹화·전원", + "en": "Record & Power", + "zh": "录制与电源", + "params": [ + "RecordRoadCam", + "MaxTimeOffroadMin" + ] + }, + { + "id": "SYS_YOUTUBE", + "ko": "YouTube 라이브", + "en": "YouTube Live", + "zh": "YouTube Live", + "params": [ + "CarrotYouTubeLive", + "CarrotYouTubeQuality" + ] + } ] }, { From 59a80306ab0f70dab3cfb517eb7cad3cb46fc539 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:23:29 +0900 Subject: [PATCH 37/61] f --- .../carrot/server/features/youtube_live.py | 6 ++ .../web/css/pages/settings/youtube_live.css | 1 + selfdrive/carrot/web/index.html | 10 +-- .../web/js/pages/setting_youtube_live.js | 86 +++++++++++++------ selfdrive/carrot/web/js/translations/en.js | 3 +- selfdrive/carrot/web/js/translations/ko.js | 3 +- selfdrive/carrot/web/js/translations/zh.js | 3 +- 7 files changed, 74 insertions(+), 38 deletions(-) diff --git a/selfdrive/carrot/server/features/youtube_live.py b/selfdrive/carrot/server/features/youtube_live.py index f2e4c7d121b..6389832e9cb 100644 --- a/selfdrive/carrot/server/features/youtube_live.py +++ b/selfdrive/carrot/server/features/youtube_live.py @@ -45,6 +45,11 @@ async def api_set_stream_key(request: web.Request) -> web.Response: return web.json_response({"ok": True, **status}) +async def api_get_stream_key(request: web.Request) -> web.Response: + stream_key = _service(request).get_stream_key() + return web.json_response({"ok": True, "configured": bool(stream_key), "stream_key": stream_key}) + + async def api_clear_stream_key(request: web.Request) -> web.Response: return web.json_response({"ok": True, **_service(request).clear_stream_key()}) @@ -74,6 +79,7 @@ def register(app: web.Application) -> None: app.cleanup_ctx.append(youtube_live_context) app.router.add_get("/api/youtube_live/status", api_status) app.router.add_get("/api/youtube_live/diagnostics", api_diagnostics) + app.router.add_get("/api/youtube_live/stream_key", api_get_stream_key) app.router.add_post("/api/youtube_live/stream_key", api_set_stream_key) app.router.add_post("/api/youtube_live/stream_key/validate", api_validate_stream_key) app.router.add_delete("/api/youtube_live/stream_key", api_clear_stream_key) diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css index 6e511562993..a6305c6132d 100644 --- a/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -36,6 +36,7 @@ color: var(--md-on-surface-var); font-size: var(--fs-body-sm); letter-spacing: 0; + overflow-wrap: anywhere; } .youtube-live-help-list { diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index d02c5f6cfc4..8093d79dd7c 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -100,7 +100,7 @@ - + @@ -706,9 +706,9 @@

Home

- - - + + + @@ -724,7 +724,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index f8fe13cf9a9..0e3ccfcaf5e 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -6,10 +6,13 @@ const KEY_SECTION_ID = "youtubeLiveKeySection"; const CARD_ID = "youtubeLiveStatusCard"; const LIVE_CONTROL_ROOM_URL = "https://www.youtube.com/livestreaming"; - const POLL_MS = 2500; + const POLL_MS = 1000; const state = { timer: null, loading: false, + keyLoading: false, + keyLoaded: false, + streamKey: "", status: null, observer: null, }; @@ -45,7 +48,8 @@ function configuredLabel(status) { if (status?.configured) { - return text("youtube_live_key_configured", "Key saved: {key}", { key: status.masked_key || "****" }); + if (!state.keyLoaded) return text("loading", "Loading..."); + return text("youtube_live_key_configured", "Key saved: {key}", { key: state.streamKey || "-" }); } return text("youtube_live_key_missing", "Stream key is not saved"); } @@ -153,7 +157,6 @@
${escapeHtmlLocal(text("youtube_live_help_note", "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts."))}
`; - section.appendChild(helpCard); const menuActions = helpCard.querySelector('[data-role="action-menu"]'); if (menuActions) { @@ -204,6 +207,8 @@ warnings.hidden = true; statusCard.appendChild(warnings); + section.appendChild(helpCard); + } const keySection = document.getElementById(KEY_SECTION_ID); @@ -289,6 +294,7 @@ async function loadStatus() { if (state.loading || !isVisible()) return; + loadStreamKey(); state.loading = true; try { const status = await getJson("/api/youtube_live/status"); @@ -301,37 +307,54 @@ } } + async function loadStreamKey() { + if (state.keyLoading || state.keyLoaded || !isVisible()) return; + state.keyLoading = true; + try { + const result = await getJson("/api/youtube_live/stream_key"); + state.streamKey = String(result?.stream_key || ""); + state.keyLoaded = true; + if (state.status) renderStatus(state.status); + } catch (err) { + state.keyLoaded = false; + } finally { + state.keyLoading = false; + } + } + function keyInputValue() { return String(document.querySelector(`#${KEY_SECTION_ID} [data-role="key-input"]`)?.value || "").trim(); } async function saveKeyFromInput() { const value = keyInputValue(); - if (!value) { - toast(text("youtube_live_key_empty", "Enter a stream key first."), { tone: "error" }); - return; - } + let saveResult; try { const status = await postJson("/api/youtube_live/stream_key", { stream_key: value }); + saveResult = { ok: true, status }; + state.streamKey = value; + state.keyLoaded = true; state.status = status; renderStatus(status); - const input = document.querySelector(`#${KEY_SECTION_ID} [data-role="key-input"]`); - if (input) input.value = ""; - const validation = await fetchStreamKeyValidation(); - await appAlert(saveResultText(status, validation), { - title: text("youtube_live_save_result", "Stream key saved"), - copyText: JSON.stringify({ - status, - validation, - }, null, 2), - }); } catch (err) { - toast(err?.message || String(err), { tone: "error" }); + saveResult = { + ok: false, + error: err?.message || String(err), + payload: err?.payload || null, + }; } + const validation = await fetchStreamKeyValidation(value, true); + await appAlert(saveResultText(saveResult, validation, value), { + title: text("youtube_live_save_result", "Stream key save result"), + copyText: JSON.stringify({ + save: saveResult.ok ? { ok: true, status: saveResult.status } : saveResult, + validation, + }, null, 2), + }); } - async function fetchStreamKeyValidation(value) { - const payload = value ? { stream_key: value } : {}; + async function fetchStreamKeyValidation(value, explicitValue) { + const payload = explicitValue || value ? { stream_key: value } : {}; try { return await postJson("/api/youtube_live/stream_key/validate", payload); } catch (err) { @@ -351,13 +374,13 @@ const value = keyInputValue(); try { const result = await fetchStreamKeyValidation(value); - await appAlert(validationText(result), { + await appAlert(validationText(result, value || state.streamKey), { title: text("youtube_live_validate_key", "Validate key"), }); } catch (err) { const payload = err?.payload || null; if (payload) { - await appAlert(validationText(payload), { + await appAlert(validationText(payload, value || state.streamKey), { title: text("youtube_live_validate_key", "Validate key"), }); return; @@ -366,18 +389,21 @@ } } - function saveResultText(status, validation) { + function saveResultText(saveResult, validation, streamKey) { const pass = text("youtube_live_validation_pass", "OK"); const fail = text("youtube_live_validation_fail", "FAIL"); + const saveDetail = saveResult?.ok + ? configuredLabel(saveResult.status) + : (saveResult?.error || "-"); const lines = [ - `${text("youtube_live_save_status", "Save")}: ${status?.configured ? pass : fail} - ${configuredLabel(status)}`, + `${text("youtube_live_save_status", "Save")}: ${saveResult?.ok ? pass : fail} - ${saveDetail}`, "", - validationText(validation), + validationText(validation, streamKey), ]; return lines.join("\n"); } - function validationText(result) { + function validationText(result, streamKey) { const pass = text("youtube_live_validation_pass", "OK"); const fail = text("youtube_live_validation_fail", "FAIL"); const lines = [ @@ -385,7 +411,8 @@ `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${result?.ffmpeg_available ? pass : fail}`, `${text("youtube_live_validation_rtmps", "YouTube RTMPS")}: ${result?.rtmps_reachable ? pass : fail} - ${localizedValidationMessage(result?.rtmps_message)}`, ]; - if (result?.masked_key) lines.push(`${text("youtube_live_key", "Stream key")}: ${result.masked_key}`); + const visibleKey = String(streamKey || ""); + if (visibleKey) lines.push(`${text("youtube_live_key", "Stream key")}: ${visibleKey}`); lines.push(""); lines.push(result?.note || text("youtube_live_validation_note", "YouTube confirms the key only when streaming starts.")); return lines.join("\n"); @@ -419,6 +446,8 @@ if (!ok) return; try { const status = await requestJson("/api/youtube_live/stream_key", { method: "DELETE" }); + state.streamKey = ""; + state.keyLoaded = true; state.status = status; renderStatus(status); toast(text("youtube_live_key_cleared", "Stream key cleared")); @@ -525,6 +554,7 @@ function startPolling() { if (!state.timer) state.timer = window.setInterval(loadStatus, POLL_MS); + loadStreamKey(); loadStatus(); } @@ -540,6 +570,8 @@ return; } stopPolling(); + state.keyLoaded = false; + state.streamKey = ""; const card = document.getElementById(CARD_ID); if (card) card.remove(); const keySection = document.getElementById(KEY_SECTION_ID); diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index 5b81125694c..209c08ac6f1 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -22,7 +22,6 @@ window.CarrotTranslations.register("en", { youtube_live_stream_inactive: "Stream stopped", youtube_live_quality_standard: "Standard", youtube_live_visibility_studio: "Visibility: set in YouTube Studio", - youtube_live_key_empty: "Enter a stream key first.", youtube_live_validate_key: "Validate key", youtube_live_validation_format: "Key format", youtube_live_validation_ffmpeg: "FFmpeg", @@ -41,7 +40,7 @@ window.CarrotTranslations.register("en", { youtube_live_clear_key: "Clear key", youtube_live_clear_confirm: "Clear saved stream key?", youtube_live_key_cleared: "Stream key cleared", - youtube_live_save_result: "Stream key saved", + youtube_live_save_result: "Stream key save result", youtube_live_save_status: "Save", youtube_live_check: "Check", youtube_live_check_result: "Check result", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index c6bdde385dc..a9721b7c034 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -22,7 +22,6 @@ window.CarrotTranslations.register("ko", { youtube_live_stream_inactive: "송출 중지", youtube_live_quality_standard: "표준", youtube_live_visibility_studio: "공개 범위: YouTube Studio 설정", - youtube_live_key_empty: "스트림 키를 먼저 입력하세요.", youtube_live_validate_key: "키 검증", youtube_live_validation_format: "키 형식", youtube_live_validation_ffmpeg: "FFmpeg", @@ -41,7 +40,7 @@ window.CarrotTranslations.register("ko", { youtube_live_clear_key: "키 삭제", youtube_live_clear_confirm: "저장된 스트림 키를 삭제할까요?", youtube_live_key_cleared: "스트림 키 삭제됨", - youtube_live_save_result: "스트림 키 저장", + youtube_live_save_result: "스트림 키 저장 결과", youtube_live_save_status: "저장", youtube_live_check: "점검", youtube_live_check_result: "점검 결과", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 09eed1b20e3..9875dc2d7c2 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -22,7 +22,6 @@ window.CarrotTranslations.register("zh", { youtube_live_stream_inactive: "Stream stopped", youtube_live_quality_standard: "Standard", youtube_live_visibility_studio: "Visibility: set in YouTube Studio", - youtube_live_key_empty: "Enter a stream key first.", youtube_live_validate_key: "Validate key", youtube_live_validation_format: "Key format", youtube_live_validation_ffmpeg: "FFmpeg", @@ -41,7 +40,7 @@ window.CarrotTranslations.register("zh", { youtube_live_clear_key: "Clear key", youtube_live_clear_confirm: "Clear saved stream key?", youtube_live_key_cleared: "Stream key cleared", - youtube_live_save_result: "Stream key saved", + youtube_live_save_result: "Stream key save result", youtube_live_save_status: "Save", youtube_live_check: "Check", youtube_live_check_result: "Check result", From 0c8c0003e2bad21f103c055b377a2e9c99131770 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:26:04 +0900 Subject: [PATCH 38/61] f --- selfdrive/carrot/web/index.html | 4 ++-- selfdrive/carrot/web/js/pages/setting.js | 3 +++ selfdrive/carrot/web/js/pages/setting_youtube_live.js | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 8093d79dd7c..8a3eb0a3d85 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -723,8 +723,8 @@

Home

- - + + diff --git a/selfdrive/carrot/web/js/pages/setting.js b/selfdrive/carrot/web/js/pages/setting.js index ec81157a5d2..acd5eb00e2b 100644 --- a/selfdrive/carrot/web/js/pages/setting.js +++ b/selfdrive/carrot/web/js/pages/setting.js @@ -3247,6 +3247,9 @@ async function renderItems(group, options = {}) { itemsBox.dataset.renderedGroup = group; if (detailMode) itemsBox.dataset.renderedDetail = detailName; scheduleSettingOverflowSync(itemsBox); + window.dispatchEvent(new CustomEvent("carrot:settingrendered", { + detail: { group, detailName: detailMode ? detailName : "" }, + })); if (pendingSettingFocus?.group === group) { requestAnimationFrame(() => focusSettingItem(pendingSettingFocus.name)); diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 0e3ccfcaf5e..c436ebb8cfe 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -591,6 +591,7 @@ const name = event?.detail?.name; if (name === "CarrotYouTubeLive" || name === "CarrotYouTubeQuality") loadStatus(); }); + window.addEventListener("carrot:settingrendered", sync); sync(); } From ab8ac4767ebed476ac6c9f908522de3cce302080 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:28:51 +0900 Subject: [PATCH 39/61] f --- selfdrive/carrot/web/index.html | 2 +- selfdrive/carrot/web/js/pages/setting_youtube_live.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 8a3eb0a3d85..cb683f5773b 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -724,7 +724,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index c436ebb8cfe..3329d570417 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -1,7 +1,6 @@ "use strict"; (function () { - const YOUTUBE_GROUP = "SYS_RECORD"; const YOUTUBE_SECTION = "SYS_YOUTUBE"; const KEY_SECTION_ID = "youtubeLiveKeySection"; const CARD_ID = "youtubeLiveStatusCard"; @@ -38,7 +37,7 @@ function isVisible() { const box = itemsBox(); - return Boolean(box && box.dataset.renderedGroup === YOUTUBE_GROUP && !box.dataset.renderedDetail); + return Boolean(box?.querySelector(`[data-setting-section-id="${YOUTUBE_SECTION}"]`)); } function stateLabel(status) { From c5b5cf8ca87d23c4d47cda3f53997ad2c97e4e5f Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:32:31 +0900 Subject: [PATCH 40/61] f --- selfdrive/carrot/web/index.html | 4 ++-- selfdrive/carrot/web/js/pages/setting.js | 4 +--- selfdrive/carrot/web/js/pages/setting_youtube_live.js | 11 ++++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index cb683f5773b..72a3f41b6de 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -723,8 +723,8 @@

Home

- - + + diff --git a/selfdrive/carrot/web/js/pages/setting.js b/selfdrive/carrot/web/js/pages/setting.js index acd5eb00e2b..e14c8219283 100644 --- a/selfdrive/carrot/web/js/pages/setting.js +++ b/selfdrive/carrot/web/js/pages/setting.js @@ -3247,9 +3247,7 @@ async function renderItems(group, options = {}) { itemsBox.dataset.renderedGroup = group; if (detailMode) itemsBox.dataset.renderedDetail = detailName; scheduleSettingOverflowSync(itemsBox); - window.dispatchEvent(new CustomEvent("carrot:settingrendered", { - detail: { group, detailName: detailMode ? detailName : "" }, - })); + window.CarrotYouTubeLiveSettings?.sync?.(); if (pendingSettingFocus?.group === group) { requestAnimationFrame(() => focusSettingItem(pendingSettingFocus.name)); diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 3329d570417..d8cac4ab05d 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -92,12 +92,12 @@ function placeYoutubeSections(box, keySection, infoSection) { const target = box.querySelector(`[data-setting-section-id="${YOUTUBE_SECTION}"]`); if (!target) { - box.insertBefore(keySection, box.firstChild); - box.appendChild(infoSection); + if (box.firstChild !== keySection) box.insertBefore(keySection, box.firstChild); + if (box.lastChild !== infoSection) box.appendChild(infoSection); return; } - box.insertBefore(keySection, target); - box.insertBefore(infoSection, target.nextSibling); + if (keySection.nextSibling !== target) box.insertBefore(keySection, target); + if (target.nextSibling !== infoSection) box.insertBefore(infoSection, target.nextSibling); } function ensureCard() { @@ -590,10 +590,11 @@ const name = event?.detail?.name; if (name === "CarrotYouTubeLive" || name === "CarrotYouTubeQuality") loadStatus(); }); - window.addEventListener("carrot:settingrendered", sync); sync(); } + window.CarrotYouTubeLiveSettings = Object.freeze({ sync }); + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init, { once: true }); else init(); })(); From 075d4c0da1573a03844d8bbf35456cd5978e0501 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:36:19 +0900 Subject: [PATCH 41/61] f --- selfdrive/carrot/web/index.html | 2 +- selfdrive/carrot/web/js/pages/setting_youtube_live.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 72a3f41b6de..7759d0bd497 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -723,7 +723,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index d8cac4ab05d..0aa46033e07 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -110,8 +110,9 @@ return existing; } - if (!existingKey) { - const keySection = document.createElement("section"); + let keySection = existingKey; + if (!keySection) { + keySection = document.createElement("section"); keySection.id = KEY_SECTION_ID; keySection.className = "setting-section-block youtube-live-section youtube-live-section--key"; const keyCard = document.createElement("div"); @@ -210,8 +211,7 @@ } - const keySection = document.getElementById(KEY_SECTION_ID); - if (keySection) placeYoutubeSections(box, keySection, section); + placeYoutubeSections(box, keySection, section); return section; } From a7a3c154e262a2e8ab907844dff8537ddc280a7d Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:38:54 +0900 Subject: [PATCH 42/61] f --- .../web/css/pages/settings/youtube_live.css | 2 +- selfdrive/carrot/web/index.html | 4 ++-- .../web/js/pages/setting_youtube_live.js | 19 +++++++++++-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css index a6305c6132d..643c33189e5 100644 --- a/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -1,7 +1,7 @@ .youtube-live-section { display: grid; gap: var(--sp-md); - margin-top: var(--sp-lg); + margin: 0; } .youtube-live-key-card, diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 7759d0bd497..2e3542aaa0b 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -100,7 +100,7 @@ - + @@ -723,7 +723,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 0aa46033e07..d154a008a31 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -91,18 +91,21 @@ function placeYoutubeSections(box, keySection, infoSection) { const target = box.querySelector(`[data-setting-section-id="${YOUTUBE_SECTION}"]`); - if (!target) { - if (box.firstChild !== keySection) box.insertBefore(keySection, box.firstChild); - if (box.lastChild !== infoSection) box.appendChild(infoSection); - return; + if (!target) return false; + const settingsCard = Array.from(target.children).find((child) => child.classList.contains("setting-group-card")); + if (keySection.parentElement !== target || keySection.nextSibling !== settingsCard) { + target.insertBefore(keySection, settingsCard || null); + } + if (infoSection.parentElement !== target || target.lastElementChild !== infoSection) { + target.appendChild(infoSection); } - if (keySection.nextSibling !== target) box.insertBefore(keySection, target); - if (target.nextSibling !== infoSection) box.insertBefore(infoSection, target.nextSibling); + return true; } function ensureCard() { const box = itemsBox(); if (!box) return null; + if (!box.querySelector(`[data-setting-section-id="${YOUTUBE_SECTION}"]`)) return null; const existing = document.getElementById(CARD_ID); const existingKey = document.getElementById(KEY_SECTION_ID); if (existing && existingKey) { @@ -114,7 +117,7 @@ if (!keySection) { keySection = document.createElement("section"); keySection.id = KEY_SECTION_ID; - keySection.className = "setting-section-block youtube-live-section youtube-live-section--key"; + keySection.className = "youtube-live-section youtube-live-section--key"; const keyCard = document.createElement("div"); keyCard.className = "setting-group-card youtube-live-key-card"; keyCard.innerHTML = ` @@ -142,7 +145,7 @@ const section = existing || document.createElement("section"); if (!existing) { section.id = CARD_ID; - section.className = "setting-section-block youtube-live-section youtube-live-section--info"; + section.className = "youtube-live-section youtube-live-section--info"; const helpCard = document.createElement("div"); helpCard.className = "setting-group-card youtube-live-help-card"; From 7c28bce9053004ba6ebfd3b09f70fcebd9c02828 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 13:43:34 +0900 Subject: [PATCH 43/61] f --- .../carrot/server/services/youtube_live.py | 24 ++++- selfdrive/carrot/web/index.html | 8 +- .../web/js/pages/setting_youtube_live.js | 87 ++++++++++++------- selfdrive/carrot/web/js/translations/en.js | 16 ++++ selfdrive/carrot/web/js/translations/ko.js | 18 +++- selfdrive/carrot/web/js/translations/zh.js | 16 ++++ 6 files changed, 129 insertions(+), 40 deletions(-) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index b46f294c7db..84e525f29ef 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -27,6 +27,7 @@ STATUS_WRITE_MIN_INTERVAL = 1.0 BACKOFF_BASE_SECONDS = 3.0 BACKOFF_MAX_SECONDS = 60.0 +PROCESS_STABLE_SECONDS = 10.0 _PROCESS_MATCHES = { "carrot_cluster": "selfdrive.carrot.cluster_autorun", @@ -306,6 +307,10 @@ async def _tick(self) -> None: if not self._param_enabled(): if self._process is not None: await self._stop_process() + self._last_error = "" + self._stderr_tail.clear() + self._consecutive_failures = 0 + self._next_retry_at = 0.0 self._set_state("disabled") return @@ -331,6 +336,15 @@ async def _tick(self) -> None: self._set_state("backoff") return + if self._process is not None and self._process.returncode is not None: + exit_code = self._process.returncode + if not self._last_error: + self._last_error = f"ffmpeg exited code={exit_code}" + await self._stop_process() + self._schedule_backoff("ffmpeg exited") + self._set_state("backoff") + return + payload, frame_id = self._recv_payload() if not payload: if self._process is not None and self._last_frame_at and _now() - self._last_frame_at > NO_FRAME_STOP_SECONDS: @@ -480,7 +494,7 @@ async def _start_process(self, stream_key: str) -> None: "nobuffer", "-f", "h264", - "-framerate", + "-r", "20", "-i", "pipe:0", @@ -488,12 +502,17 @@ async def _start_process(self, stream_key: str) -> None: "lavfi", "-i", "anullsrc=r=44100:cl=stereo", + "-map", + "0:v:0", + "-map", + "1:a:0", "-c:v", "copy", "-c:a", "aac", "-b:a", "128k", + "-shortest", "-f", "flv", rtmp_url, @@ -521,7 +540,8 @@ async def _write_frame(self, payload: bytes) -> None: proc.stdin.write(payload) await proc.stdin.drain() self._bytes_sent += len(payload) - self._consecutive_failures = 0 + if self._started_at and _now() - self._started_at >= PROCESS_STABLE_SECONDS: + self._consecutive_failures = 0 except (BrokenPipeError, ConnectionResetError) as exc: self._last_error = f"ffmpeg pipe closed: {exc}" self._schedule_backoff("ffmpeg pipe closed") diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 2e3542aaa0b..3744215e6a0 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -706,9 +706,9 @@

Home

- - - + + + @@ -723,7 +723,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index d154a008a31..45560869be5 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -255,7 +255,7 @@ const error = String(status?.last_error || "").trim(); if (errorEl) { errorEl.hidden = !error; - errorEl.textContent = error; + errorEl.textContent = localizedRuntimeMessage(error); } renderWarnings(warningsEl, status); } @@ -289,7 +289,7 @@ warnings.slice(0, 3).forEach((warning) => { const item = document.createElement("div"); item.className = "youtube-live-warning"; - item.textContent = warning; + item.textContent = localizedRuntimeMessage(warning); container.appendChild(item); }); } @@ -437,6 +437,30 @@ return map[raw] || raw; } + function localizedRuntimeMessage(message) { + const raw = String(message || "").trim(); + if (!raw) return "-"; + const exact = { + "Cluster HUD is enabled; monitor encoder load and thermal headroom while streaming.": text("youtube_live_warning_cluster", "Cluster HUD is enabled. Monitor encoder load and temperature while streaming."), + "Carrot Vision is enabled; YouTube Live shares camera/encoder/network resources.": text("youtube_live_warning_vision", "Carrot Vision is enabled. Camera, encoder, and network resources are shared."), + "High quality mode is planned for Phase 3; Phase 2 keeps qRoadEncodeData only.": text("youtube_live_warning_quality", "High quality mode is not available yet."), + "Error opening input files: Invalid argument": text("youtube_live_error_input_invalid", "Could not open the video input because an FFmpeg argument was invalid."), + "ffmpeg not found": text("youtube_live_error_ffmpeg_missing", "FFmpeg is not installed."), + "no qRoadEncodeData frames": text("youtube_live_error_no_frames", "No qRoadEncodeData frames were received."), + }; + if (exact[raw]) return exact[raw]; + const exitMatch = raw.match(/^ffmpeg exited code=(-?\d+)$/i); + if (exitMatch) return text("youtube_live_error_ffmpeg_exit", "FFmpeg exited (code {code}).", { code: exitMatch[1] }); + if (/^ffmpeg pipe closed:/i.test(raw)) { + return text("youtube_live_error_ffmpeg_pipe", "The FFmpeg input pipe was closed."); + } + return raw; + } + + function uniqueLocalizedMessages(values) { + return [...new Set((values || []).filter(Boolean).map(localizedRuntimeMessage))]; + } + function openLiveControlRoom() { window.open(LIVE_CONTROL_ROOM_URL, "_blank", "noopener"); } @@ -485,52 +509,49 @@ const pass = text("youtube_live_validation_pass", "OK"); const fail = text("youtube_live_validation_fail", "FAIL"); const test = testResult?.payload || {}; - const lines = [ - `${text("youtube_live_test", "Test")}: ${testResult?.ok ? pass : fail} - ${localizedValidationMessage(test.message || testResult?.error)}`, - `${text("youtube_live_key", "Stream key")}: ${test.configured ? pass : fail}`, - `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${test.ffmpeg_available ? pass : fail}`, - `${text("youtube_live_metric_source", "Source")}: ${test.source || "-"}`, - ]; - if (Array.isArray(test.warnings) && test.warnings.length) { - lines.push(""); - lines.push(text("youtube_live_warnings", "Warnings")); - test.warnings.forEach((warning) => lines.push(`- ${warning}`)); - } - lines.push(""); - if (diagnosticsResult?.ok) { - lines.push(diagnosticText(diagnosticsResult.payload)); - } else { - lines.push(`${text("youtube_live_diagnostics", "Diagnostics")}: ${fail} - ${diagnosticsResult?.error || "-"}`); - } - return lines.join("\n"); - } - - function diagnosticText(payload) { - const diag = payload?.diagnostics || payload || {}; + const diag = diagnosticsResult?.payload?.diagnostics || diagnosticsResult?.payload || {}; const status = diag.status || {}; - const resources = status.resource_status || {}; + const resources = status.resource_status || test.resource_status || {}; const cluster = resources.cluster || {}; const vision = resources.carrot_vision || {}; + const enabled = text("youtube_live_resource_enabled", "Enabled"); + const disabled = text("youtube_live_resource_disabled", "Disabled"); const lines = [ + `[${text("youtube_live_check_ready_section", "Readiness")}]`, + `${text("youtube_live_key", "Stream key")}: ${test.configured ? pass : fail}`, + `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${test.ffmpeg_available ? pass : fail}`, + `${text("youtube_live_metric_source", "Source")}: ${test.source || "-"}`, + "", + `[${text("youtube_live_check_stream_section", "Stream status")}]`, `${text("youtube_live_state_label", "State")}: ${stateLabel(status)}`, - `${text("youtube_live_metric_source", "Source")}: ${status.source || "-"}`, `${text("youtube_live_metric_bitrate", "Bitrate")}: ${status.estimated_kbps || 0} kbps`, `${text("youtube_live_metric_session", "Session")}: ${status.session_mb || 0} MB`, `${text("youtube_live_metric_total", "Total")}: ${status.total_mb || 0} MB`, - `ClusterHud: ${cluster.enabled ? "on" : "off"} (${cluster.param ?? "-"})`, - `CarrotVision: ${vision.enabled ? "on" : "off"} (DisableDM=${vision.disable_dm ?? "-"})`, + "", + `[${text("youtube_live_check_resource_section", "Resources")}]`, + `${text("youtube_live_resource_cluster", "Cluster HUD")}: ${cluster.enabled ? enabled : disabled}`, + `${text("youtube_live_resource_vision", "Carrot Vision")}: ${vision.enabled ? enabled : disabled}`, ]; - const warnings = Array.isArray(status.warnings) ? status.warnings : []; + const warnings = uniqueLocalizedMessages([ + ...(Array.isArray(test.warnings) ? test.warnings : []), + ...(Array.isArray(status.warnings) ? status.warnings : []), + ]); if (warnings.length) { lines.push(""); - lines.push(text("youtube_live_warnings", "Warnings")); + lines.push(`[${text("youtube_live_warnings", "Warnings")}]`); warnings.forEach((warning) => lines.push(`- ${warning}`)); } const stderr = diag.ffmpeg?.stderr_tail || status.stderr_tail || []; - if (stderr.length) { + const errors = uniqueLocalizedMessages([ + status.last_error, + ...(Array.isArray(stderr) ? stderr.slice(-8) : []), + diagnosticsResult?.ok ? "" : diagnosticsResult?.error, + testResult?.ok || test.message === "ready" ? "" : (testResult?.error || test.message), + ]); + if (errors.length) { lines.push(""); - lines.push("ffmpeg stderr"); - stderr.slice(-8).forEach((line) => lines.push(`- ${line}`)); + lines.push(`[${text("youtube_live_check_error_section", "Errors and FFmpeg logs")}]`); + errors.forEach((error) => lines.push(`- ${localizedValidationMessage(error)}`)); } return lines.join("\n"); } diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index 209c08ac6f1..d97af7890d4 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -45,6 +45,22 @@ window.CarrotTranslations.register("en", { youtube_live_check: "Check", youtube_live_check_result: "Check result", youtube_live_check_ready: "Ready", + youtube_live_check_ready_section: "Readiness", + youtube_live_check_stream_section: "Stream status", + youtube_live_check_resource_section: "Resources", + youtube_live_check_error_section: "Errors and FFmpeg logs", + youtube_live_resource_cluster: "Cluster HUD", + youtube_live_resource_vision: "Carrot Vision", + youtube_live_resource_enabled: "Enabled", + youtube_live_resource_disabled: "Disabled", + youtube_live_warning_cluster: "Cluster HUD is enabled. Monitor encoder load and device temperature while streaming.", + youtube_live_warning_vision: "Carrot Vision is enabled. Camera, encoder, and network resources are shared.", + youtube_live_warning_quality: "High quality mode is not available yet.", + youtube_live_error_input_invalid: "Could not open the video input because an FFmpeg argument was invalid.", + youtube_live_error_ffmpeg_missing: "FFmpeg is not installed.", + youtube_live_error_no_frames: "No qRoadEncodeData frames were received.", + youtube_live_error_ffmpeg_exit: "FFmpeg exited (code {code}).", + youtube_live_error_ffmpeg_pipe: "The FFmpeg input pipe was closed.", youtube_live_test: "Test", youtube_live_test_missing: "Missing stream key or FFmpeg.", youtube_live_test_failed: "Test failed", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index a9721b7c034..21c987b4b4e 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -45,6 +45,22 @@ window.CarrotTranslations.register("ko", { youtube_live_check: "점검", youtube_live_check_result: "점검 결과", youtube_live_check_ready: "준비됨", + youtube_live_check_ready_section: "준비 상태", + youtube_live_check_stream_section: "송출 상태", + youtube_live_check_resource_section: "리소스", + youtube_live_check_error_section: "오류 및 FFmpeg 로그", + youtube_live_resource_cluster: "Cluster HUD", + youtube_live_resource_vision: "Carrot Vision", + youtube_live_resource_enabled: "사용 중", + youtube_live_resource_disabled: "꺼짐", + youtube_live_warning_cluster: "Cluster HUD가 켜져 있습니다. 송출 중 인코더 부하와 기기 온도를 확인하세요.", + youtube_live_warning_vision: "Carrot Vision이 켜져 있습니다. 카메라, 인코더, 네트워크 자원을 함께 사용합니다.", + youtube_live_warning_quality: "고화질 모드는 아직 사용할 수 없습니다.", + youtube_live_error_input_invalid: "FFmpeg 인자가 잘못되어 영상 입력을 열지 못했습니다.", + youtube_live_error_ffmpeg_missing: "FFmpeg가 설치되어 있지 않습니다.", + youtube_live_error_no_frames: "qRoadEncodeData 프레임이 수신되지 않습니다.", + youtube_live_error_ffmpeg_exit: "FFmpeg가 종료되었습니다(코드 {code}).", + youtube_live_error_ffmpeg_pipe: "FFmpeg 입력 파이프가 닫혔습니다.", youtube_live_test: "테스트", youtube_live_test_missing: "스트림 키 또는 FFmpeg가 없습니다.", youtube_live_test_failed: "테스트 실패", @@ -94,7 +110,7 @@ window.CarrotTranslations.register("ko", { reboot: "재부팅", backup: "Backup", restore: "Restore", - copy: "Copy", + copy: "복사", view: "View", device_info: "기기 정보", device_tab_error: "기기 정보를 불러오지 못했습니다.", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 9875dc2d7c2..382858ac8ed 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -45,6 +45,22 @@ window.CarrotTranslations.register("zh", { youtube_live_check: "Check", youtube_live_check_result: "Check result", youtube_live_check_ready: "Ready", + youtube_live_check_ready_section: "准备状态", + youtube_live_check_stream_section: "推流状态", + youtube_live_check_resource_section: "资源", + youtube_live_check_error_section: "错误和 FFmpeg 日志", + youtube_live_resource_cluster: "Cluster HUD", + youtube_live_resource_vision: "Carrot Vision", + youtube_live_resource_enabled: "已启用", + youtube_live_resource_disabled: "已关闭", + youtube_live_warning_cluster: "Cluster HUD 已启用。推流时请注意编码负载和设备温度。", + youtube_live_warning_vision: "Carrot Vision 已启用。摄像头、编码器和网络资源将被共享。", + youtube_live_warning_quality: "高画质模式暂不可用。", + youtube_live_error_input_invalid: "FFmpeg 参数无效,无法打开视频输入。", + youtube_live_error_ffmpeg_missing: "未安装 FFmpeg。", + youtube_live_error_no_frames: "未收到 qRoadEncodeData 帧。", + youtube_live_error_ffmpeg_exit: "FFmpeg 已退出(代码 {code})。", + youtube_live_error_ffmpeg_pipe: "FFmpeg 输入管道已关闭。", youtube_live_test: "Test", youtube_live_test_missing: "Missing stream key or FFmpeg.", youtube_live_test_failed: "Test failed", From d992678d2cdf1bf661919914c2bdb745187cfa16 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 14:08:08 +0900 Subject: [PATCH 44/61] f --- .../carrot/server/services/youtube_live.py | 133 +++++++++++++----- .../server/services/youtube_live_muxer.py | 112 +++++++++++++++ .../web/css/pages/settings/youtube_live.css | 67 --------- selfdrive/carrot/web/index.html | 10 +- .../web/js/pages/setting_youtube_live.js | 76 ++++------ selfdrive/carrot/web/js/translations/en.js | 29 ++-- selfdrive/carrot/web/js/translations/ko.js | 29 ++-- selfdrive/carrot/web/js/translations/zh.js | 29 ++-- 8 files changed, 294 insertions(+), 191 deletions(-) create mode 100644 selfdrive/carrot/server/services/youtube_live_muxer.py diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index 84e525f29ef..f5efc8969a3 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -13,6 +13,7 @@ from typing import Any from ..config import CARROT_YOUTUBE_LIVE_SECRET_PATH, CARROT_YOUTUBE_LIVE_STATE_PATH +from .youtube_live_muxer import H264MpegTsMuxer, pyav_capabilities YOUTUBE_LIVE_PARAM = "CarrotYouTubeLive" @@ -120,6 +121,7 @@ def __init__( self._task: asyncio.Task | None = None self._stop_event = asyncio.Event() self._process: asyncio.subprocess.Process | None = None + self._muxer: H264MpegTsMuxer | None = None self._stderr_task: asyncio.Task | None = None self._stderr_tail: deque[str] = deque(maxlen=20) self._messaging: Any | None = None @@ -136,6 +138,7 @@ def __init__( self._next_retry_at = 0.0 self._last_error = "" self._state = "disabled" + self._muxer_capabilities = pyav_capabilities() persisted = _read_json(self.state_path) try: self._bytes_sent = int(persisted.get("bytes_sent") or 0) @@ -177,6 +180,12 @@ def status(self) -> dict[str, Any]: "configured": bool(stream_key), "masked_key": _mask_stream_key(stream_key), "source": PHASE1_SOURCE_SERVICE, + "muxer": "pyav-mpegts", + "muxer_available": bool( + self._muxer_capabilities.get("available") + and self._muxer_capabilities.get("mpegts") + and self._muxer_capabilities.get("h264") + ), "quality": PHASE1_QUALITY, "requested_quality": self._param_int(YOUTUBE_QUALITY_PARAM, 0), "phase": 1, @@ -206,19 +215,26 @@ def status(self) -> dict[str, Any]: def test_config(self) -> dict[str, Any]: stream_key = self.get_stream_key() ffmpeg_path = shutil.which("ffmpeg") - ok = bool(stream_key and ffmpeg_path) + muxer_ok = bool( + self._muxer_capabilities.get("available") + and self._muxer_capabilities.get("mpegts") + and self._muxer_capabilities.get("h264") + ) + ok = bool(stream_key and ffmpeg_path and muxer_ok) status = self.status() return { "ok": ok, "configured": bool(stream_key), "ffmpeg_available": bool(ffmpeg_path), "ffmpeg_path": ffmpeg_path or "", + "muxer_available": muxer_ok, + "muxer": dict(self._muxer_capabilities), "source": PHASE1_SOURCE_SERVICE, "quality": PHASE1_QUALITY, "resource_status": status["resource_status"], "warnings": status["warnings"], "stderr_tail": list(self._stderr_tail), - "message": "ready" if ok else "missing stream key or ffmpeg", + "message": "ready" if ok else "missing stream key, FFmpeg, or PyAV MPEG-TS support", } def validate_stream_key(self, value: str | None = None) -> dict[str, Any]: @@ -265,6 +281,10 @@ def diagnostics(self) -> dict[str, Any]: "path": shutil.which("ffmpeg") or "", "stderr_tail": list(self._stderr_tail), }, + "muxer": { + "name": "pyav-mpegts", + **self._muxer_capabilities, + }, "processes": self._process_status(), "state_path": str(self.state_path), } @@ -326,6 +346,16 @@ async def _tick(self) -> None: self._set_state("error") return + if not ( + self._muxer_capabilities.get("available") + and self._muxer_capabilities.get("mpegts") + and self._muxer_capabilities.get("h264") + ): + await self._stop_process() + self._last_error = "PyAV MPEG-TS muxer is unavailable" + self._set_state("error") + return + if self._param_int(YOUTUBE_QUALITY_PARAM, 0) > 0: await self._stop_process() self._last_error = "high quality YouTube Live is planned for Phase 3" @@ -345,7 +375,7 @@ async def _tick(self) -> None: self._set_state("backoff") return - payload, frame_id = self._recv_payload() + payload, frame_id, keyframe, width, height = self._recv_payload() if not payload: if self._process is not None and self._last_frame_at and _now() - self._last_frame_at > NO_FRAME_STOP_SECONDS: self._last_error = "no qRoadEncodeData frames" @@ -357,11 +387,14 @@ async def _tick(self) -> None: self._last_frame_at = _now() self._last_frame_id = frame_id - if self._process is None or self._process.returncode is not None: - await self._start_process(stream_key) + if self._process is None: + if not keyframe: + self._set_state("waiting_keyframe") + return + await self._start_process(stream_key, width=width, height=height) - await self._write_frame(payload) - self._set_state("live") + if await self._write_frame(payload, keyframe=keyframe): + self._set_state("live") def _set_state(self, state: str) -> None: self._state = state @@ -446,45 +479,55 @@ def _get_socket(self) -> Any | None: self._socket = None return self._socket - def _recv_payload(self) -> tuple[bytes, int | None]: + def _recv_payload(self) -> tuple[bytes, int | None, bool, int, int]: messaging = self._get_messaging() sock = self._get_socket() if messaging is None or sock is None: - return b"", None + return b"", None, False, 526, 330 try: msg = messaging.recv_one_or_none(sock) if msg is None: - return b"", None + return b"", None, False, 526, 330 which = msg.which() frame = getattr(msg, which, None) if frame is None: - return b"", None + return b"", None, False, 526, 330 header = bytes(getattr(frame, "header", b"") or b"") data = bytes(getattr(frame, "data", b"") or b"") frame_id = None + flags = 0 idx = getattr(frame, "idx", None) if idx is not None: try: frame_id = int(getattr(idx, "frameId", 0) or 0) or None except Exception: frame_id = None + try: + flags = int(getattr(idx, "flags", 0) or 0) + except Exception: + flags = 0 if frame_id is None: try: frame_id = int(getattr(frame, "frameId", 0) or 0) or None except Exception: frame_id = None - return header + data, frame_id + width = int(getattr(frame, "width", 0) or 526) + height = int(getattr(frame, "height", 0) or 330) + keyframe = bool(header) or bool(flags & 0x8) + return header + data, frame_id, keyframe, width, height except Exception as exc: self._last_error = f"{PHASE1_SOURCE_SERVICE} recv failed: {exc}" - return b"", None + return b"", None, False, 526, 330 - async def _start_process(self, stream_key: str) -> None: + async def _start_process(self, stream_key: str, *, width: int, height: int) -> None: await self._stop_process() - self._set_state("starting") self._stderr_tail.clear() + self._last_error = "" + self._set_state("starting") self._started_at = _now() self._session_started_bytes = self._bytes_sent rtmp_url = f"{YOUTUBE_RTMPS_BASE}/{stream_key}" + muxer = H264MpegTsMuxer(fps=20, width=width, height=height) cmd = [ "ffmpeg", "-hide_banner", @@ -493,9 +536,7 @@ async def _start_process(self, stream_key: str) -> None: "-fflags", "nobuffer", "-f", - "h264", - "-r", - "20", + "mpegts", "-i", "pipe:0", "-f", @@ -517,35 +558,50 @@ async def _start_process(self, stream_key: str) -> None: "flv", rtmp_url, ] - self._process = await asyncio.create_subprocess_exec( - *cmd, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.PIPE, - ) + try: + self._process = await asyncio.create_subprocess_exec( + *cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.PIPE, + ) + except Exception: + muxer.close() + raise + self._muxer = muxer self._restart_count += 1 self._next_retry_at = 0.0 self._stderr_task = asyncio.create_task(self._read_stderr(self._process), name="carrot-youtube-live-stderr") - async def _write_frame(self, payload: bytes) -> None: + async def _write_frame(self, payload: bytes, *, keyframe: bool) -> bool: proc = self._process - if proc is None or proc.stdin is None: - return + muxer = self._muxer + if proc is None or proc.stdin is None or muxer is None: + return False if proc.returncode is not None: self._last_error = f"ffmpeg exited code={proc.returncode}" self._schedule_backoff("ffmpeg exited") await self._stop_process() - return + return False try: - proc.stdin.write(payload) - await proc.stdin.drain() - self._bytes_sent += len(payload) + chunk = muxer.mux(payload, keyframe=keyframe) + if chunk: + proc.stdin.write(chunk) + await proc.stdin.drain() + self._bytes_sent += len(chunk) if self._started_at and _now() - self._started_at >= PROCESS_STABLE_SECONDS: self._consecutive_failures = 0 + return True except (BrokenPipeError, ConnectionResetError) as exc: self._last_error = f"ffmpeg pipe closed: {exc}" self._schedule_backoff("ffmpeg pipe closed") await self._stop_process() + return False + except Exception as exc: + self._last_error = f"MPEG-TS mux failed: {exc}" + self._schedule_backoff("MPEG-TS mux failed") + await self._stop_process() + return False async def _read_stderr(self, proc: asyncio.subprocess.Process) -> None: if proc.stderr is None: @@ -562,8 +618,16 @@ async def _read_stderr(self, proc: asyncio.subprocess.Process) -> None: async def _stop_process(self) -> None: proc = self._process self._process = None + muxer = self._muxer + self._muxer = None stderr_task = self._stderr_task self._stderr_task = None + trailer = b"" + if muxer is not None: + try: + trailer = muxer.close() + except Exception: + trailer = b"" if proc is None: if stderr_task is not None and not stderr_task.done(): stderr_task.cancel() @@ -571,6 +635,9 @@ async def _stop_process(self) -> None: self._set_state("stopping") if proc.stdin is not None: try: + if trailer and proc.returncode is None: + proc.stdin.write(trailer) + await proc.stdin.drain() proc.stdin.close() await proc.stdin.wait_closed() except Exception: @@ -642,9 +709,9 @@ def _warnings(self, resource_status: dict[str, Any]) -> list[str]: cluster = resource_status.get("cluster") if isinstance(resource_status, dict) else {} vision = resource_status.get("carrot_vision") if isinstance(resource_status, dict) else {} if cluster and cluster.get("enabled"): - warnings.append("Cluster HUD is enabled; monitor encoder load and thermal headroom while streaming.") + warnings.append("Cluster HUD is enabled; monitor overall load and temperature during simultaneous use.") if vision and vision.get("enabled"): - warnings.append("Carrot Vision is enabled; YouTube Live shares camera/encoder/network resources.") + warnings.append("Carrot Vision is enabled; simultaneous streaming increases network and memory bandwidth use.") if self._param_int(YOUTUBE_QUALITY_PARAM, 0) > 0: warnings.append("High quality mode is planned for Phase 3; Phase 2 keeps qRoadEncodeData only.") return warnings diff --git a/selfdrive/carrot/server/services/youtube_live_muxer.py b/selfdrive/carrot/server/services/youtube_live_muxer.py new file mode 100644 index 00000000000..41e9150f57f --- /dev/null +++ b/selfdrive/carrot/server/services/youtube_live_muxer.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import io +from fractions import Fraction +from typing import Any + + +MPEGTS_FORMAT = "mpegts" +H264_CODEC = "h264" + + +class _StreamingBuffer(io.RawIOBase): + def __init__(self) -> None: + super().__init__() + self._chunks = bytearray() + self._position = 0 + + def writable(self) -> bool: + return True + + def seekable(self) -> bool: + return False + + def write(self, data: bytes | bytearray | memoryview) -> int: + chunk = bytes(data) + self._chunks.extend(chunk) + self._position += len(chunk) + return len(chunk) + + def tell(self) -> int: + return self._position + + def drain(self) -> bytes: + if not self._chunks: + return b"" + chunk = bytes(self._chunks) + self._chunks.clear() + return chunk + + +def pyav_capabilities() -> dict[str, Any]: + try: + import av + except Exception as exc: + return { + "available": False, + "version": "", + "mpegts": False, + "h264": False, + "error": str(exc), + } + formats = getattr(av, "formats_available", set()) + codecs = getattr(av, "codecs_available", set()) + return { + "available": True, + "version": str(getattr(av, "__version__", "")), + "mpegts": MPEGTS_FORMAT in formats, + "h264": H264_CODEC in codecs, + "error": "", + } + + +class H264MpegTsMuxer: + def __init__(self, *, fps: int = 20, width: int = 526, height: int = 330) -> None: + import av + + self._av = av + self._fps = max(1, int(fps)) + self._time_base = Fraction(1, self._fps) + self._buffer = _StreamingBuffer() + self._container = av.open( + self._buffer, + mode="w", + format=MPEGTS_FORMAT, + options={"flush_packets": "1", "mpegts_flags": "+resend_headers"}, + ) + self._stream = self._container.add_stream(H264_CODEC, rate=self._fps) + self._stream.width = max(1, int(width)) + self._stream.height = max(1, int(height)) + self._stream.time_base = self._time_base + self._packet_index = 0 + self._closed = False + + def mux(self, payload: bytes, *, keyframe: bool = False) -> bytes: + if self._closed: + raise RuntimeError("MPEG-TS muxer is closed") + if not payload: + return b"" + packet = self._av.Packet(payload) + packet.stream = self._stream + packet.pts = self._packet_index + packet.dts = self._packet_index + packet.duration = 1 + packet.time_base = self._time_base + if keyframe: + try: + packet.is_keyframe = True + except Exception: + pass + self._container.mux(packet) + self._packet_index += 1 + return self._buffer.drain() + + def close(self) -> bytes: + if self._closed: + return self._buffer.drain() + self._closed = True + try: + self._container.close() + except Exception: + pass + return self._buffer.drain() diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css index 643c33189e5..fd51b15c317 100644 --- a/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -13,13 +13,6 @@ padding-bottom: var(--sp-lg); } -.youtube-live-card__head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--sp-md); -} - .youtube-live-card__summary { min-width: 0; } @@ -87,33 +80,6 @@ box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-primary) 16%, transparent); } -.youtube-live-pill { - flex: 0 0 auto; - max-width: 44%; - border: 1px solid color-mix(in srgb, var(--md-outline-var) 38%, transparent); - border-radius: 999px; - padding: 5px 10px; - color: var(--md-on-surface); - background: color-mix(in srgb, var(--md-surface) 72%, transparent); - font-size: var(--fs-label); - font-weight: 800; - line-height: 1.2; - text-align: center; - overflow-wrap: anywhere; -} - -.youtube-live-section[data-live-state="live"] .youtube-live-pill { - color: var(--md-primary); - border-color: color-mix(in srgb, var(--md-primary) 42%, transparent); - background: color-mix(in srgb, var(--md-primary) 14%, transparent); -} - -.youtube-live-section[data-live-state="error"] .youtube-live-pill { - color: var(--md-error); - border-color: color-mix(in srgb, var(--md-error) 45%, transparent); - background: color-mix(in srgb, var(--md-error) 10%, transparent); -} - .youtube-live-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -137,29 +103,6 @@ overflow-wrap: anywhere; } -.youtube-live-error { - border-left: 3px solid var(--md-error); - padding: 8px 10px; - color: var(--md-error); - background: color-mix(in srgb, var(--md-error) 8%, transparent); - font-size: var(--fs-body-sm); - overflow-wrap: anywhere; -} - -.youtube-live-warnings { - display: grid; - gap: 6px; -} - -.youtube-live-warning { - border-left: 3px solid var(--md-tertiary, var(--md-primary)); - padding: 8px 10px; - color: var(--md-on-surface); - background: color-mix(in srgb, var(--md-tertiary, var(--md-primary)) 9%, transparent); - font-size: var(--fs-body-sm); - overflow-wrap: anywhere; -} - .youtube-live-actions { --ui-action-min: 136px; } @@ -177,11 +120,6 @@ } @media (max-width: 430px) { - .youtube-live-card__head { - align-items: stretch; - flex-direction: column; - } - .youtube-live-key-row { display: grid; grid-template-columns: 1fr; @@ -191,11 +129,6 @@ width: 100%; } - .youtube-live-pill { - max-width: none; - align-self: flex-start; - } - .youtube-live-metrics { grid-template-columns: 1fr; } diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 3744215e6a0..7eb66865f82 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -100,7 +100,7 @@ - + @@ -706,9 +706,9 @@

Home

- - - + + + @@ -723,7 +723,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index 45560869be5..f0f69bfd261 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -54,9 +54,7 @@ } function streamTitle(status) { - return status?.enabled - ? text("youtube_live_stream_active", "Streaming") - : text("youtube_live_stream_inactive", "Stream stopped"); + return stateLabel(status); } function streamSummary(status) { @@ -171,44 +169,30 @@ statusCard.className = "setting-group-card youtube-live-card youtube-live-status-card"; section.appendChild(statusCard); - const head = document.createElement("div"); - head.className = "youtube-live-card__head"; const summary = document.createElement("div"); summary.className = "youtube-live-card__summary"; const summaryTitle = document.createElement("div"); summaryTitle.className = "youtube-live-card__title"; summaryTitle.dataset.role = "title"; - summaryTitle.textContent = text("youtube_live_stream_inactive", "Stream stopped"); + summaryTitle.textContent = stateLabel({ state: "disabled" }); const summaryDesc = document.createElement("div"); summaryDesc.className = "youtube-live-card__desc"; summaryDesc.dataset.role = "summary"; summaryDesc.textContent = text("loading", "Loading..."); summary.appendChild(summaryTitle); summary.appendChild(summaryDesc); - const pill = document.createElement("span"); - pill.className = "youtube-live-pill"; - pill.dataset.role = "state"; - pill.textContent = "-"; - head.appendChild(summary); - head.appendChild(pill); - statusCard.appendChild(head); + statusCard.appendChild(summary); const metrics = document.createElement("div"); metrics.className = "youtube-live-metrics"; metrics.dataset.role = "metrics"; statusCard.appendChild(metrics); - const error = document.createElement("div"); - error.className = "youtube-live-error"; - error.dataset.role = "error"; - error.hidden = true; - statusCard.appendChild(error); - - const warnings = document.createElement("div"); - warnings.className = "youtube-live-warnings"; - warnings.dataset.role = "warnings"; - warnings.hidden = true; - statusCard.appendChild(warnings); + const log = document.createElement("pre"); + log.className = "muted text-mono"; + log.dataset.role = "runtime-log"; + log.hidden = true; + statusCard.appendChild(log); section.appendChild(helpCard); @@ -232,15 +216,11 @@ const card = ensureCard(); if (!card) return; const titleEl = card.querySelector('[data-role="title"]'); - const stateEl = card.querySelector('[data-role="state"]'); const keyStatusEl = document.querySelector(`#${KEY_SECTION_ID} [data-role="key-status"]`); const summaryEl = card.querySelector('[data-role="summary"]'); const metricsEl = card.querySelector('[data-role="metrics"]'); - const errorEl = card.querySelector('[data-role="error"]'); - const warningsEl = card.querySelector('[data-role="warnings"]'); - card.dataset.liveState = String(status?.state || "disabled"); + const logEl = card.querySelector('[data-role="runtime-log"]'); if (titleEl) titleEl.textContent = streamTitle(status); - if (stateEl) stateEl.textContent = stateLabel(status); if (keyStatusEl) keyStatusEl.textContent = configuredLabel(status); if (summaryEl) summaryEl.textContent = streamSummary(status); if (metricsEl) { @@ -252,12 +232,7 @@ metricsEl.appendChild(metric(text("youtube_live_metric_total", "Total"), `${status?.total_mb || 0} MB`)); metricsEl.appendChild(metric(text("youtube_live_metric_retry", "Retry"), retryLabel(status))); } - const error = String(status?.last_error || "").trim(); - if (errorEl) { - errorEl.hidden = !error; - errorEl.textContent = localizedRuntimeMessage(error); - } - renderWarnings(warningsEl, status); + renderRuntimeLog(logEl, status); } function formatSeconds(seconds) { @@ -276,22 +251,15 @@ return "-"; } - function renderWarnings(container, status) { + function renderRuntimeLog(container, status) { if (!container) return; - const warnings = Array.isArray(status?.warnings) ? status.warnings.filter(Boolean) : []; - if (!warnings.length) { - container.hidden = true; - container.innerHTML = ""; - return; - } - container.hidden = false; - container.innerHTML = ""; - warnings.slice(0, 3).forEach((warning) => { - const item = document.createElement("div"); - item.className = "youtube-live-warning"; - item.textContent = localizedRuntimeMessage(warning); - container.appendChild(item); - }); + const error = String(status?.last_error || "").trim(); + const warnings = uniqueLocalizedMessages(Array.isArray(status?.warnings) ? status.warnings : []); + const lines = []; + if (error) lines.push(`${text("youtube_live_error_label", "Error")}: ${localizedRuntimeMessage(error)}`); + warnings.forEach((warning) => lines.push(`${text("youtube_live_warning_label", "Notice")}: ${warning}`)); + container.hidden = lines.length === 0; + container.textContent = lines.join("\n"); } async function loadStatus() { @@ -432,6 +400,7 @@ "format looks valid": text("youtube_live_validation_format_ok", "Format looks valid."), "YouTube RTMPS ingest is reachable": text("youtube_live_validation_rtmps_ok", "YouTube RTMPS ingest is reachable."), "missing stream key or ffmpeg": text("youtube_live_test_missing", "Missing stream key or FFmpeg."), + "missing stream key, FFmpeg, or PyAV MPEG-TS support": text("youtube_live_test_muxer_missing", "Stream key, FFmpeg, or PyAV MPEG-TS support is missing."), "ready": text("youtube_live_check_ready", "Ready"), }; return map[raw] || raw; @@ -442,10 +411,13 @@ if (!raw) return "-"; const exact = { "Cluster HUD is enabled; monitor encoder load and thermal headroom while streaming.": text("youtube_live_warning_cluster", "Cluster HUD is enabled. Monitor encoder load and temperature while streaming."), + "Cluster HUD is enabled; monitor overall load and temperature during simultaneous use.": text("youtube_live_warning_cluster", "Cluster HUD is enabled. Monitor overall load and temperature during simultaneous use."), "Carrot Vision is enabled; YouTube Live shares camera/encoder/network resources.": text("youtube_live_warning_vision", "Carrot Vision is enabled. Camera, encoder, and network resources are shared."), + "Carrot Vision is enabled; simultaneous streaming increases network and memory bandwidth use.": text("youtube_live_warning_vision", "Carrot Vision is enabled. Simultaneous streaming increases network and memory bandwidth use."), "High quality mode is planned for Phase 3; Phase 2 keeps qRoadEncodeData only.": text("youtube_live_warning_quality", "High quality mode is not available yet."), "Error opening input files: Invalid argument": text("youtube_live_error_input_invalid", "Could not open the video input because an FFmpeg argument was invalid."), "ffmpeg not found": text("youtube_live_error_ffmpeg_missing", "FFmpeg is not installed."), + "PyAV MPEG-TS muxer is unavailable": text("youtube_live_error_muxer_missing", "PyAV MPEG-TS muxing is unavailable."), "no qRoadEncodeData frames": text("youtube_live_error_no_frames", "No qRoadEncodeData frames were received."), }; if (exact[raw]) return exact[raw]; @@ -454,6 +426,9 @@ if (/^ffmpeg pipe closed:/i.test(raw)) { return text("youtube_live_error_ffmpeg_pipe", "The FFmpeg input pipe was closed."); } + if (/^MPEG-TS mux failed:/i.test(raw)) { + return text("youtube_live_error_muxer_failed", "MPEG-TS muxing failed: {error}", { error: raw.split(":", 2)[1]?.trim() || "-" }); + } return raw; } @@ -520,6 +495,7 @@ `[${text("youtube_live_check_ready_section", "Readiness")}]`, `${text("youtube_live_key", "Stream key")}: ${test.configured ? pass : fail}`, `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${test.ffmpeg_available ? pass : fail}`, + `${text("youtube_live_muxer", "PyAV MPEG-TS")}: ${test.muxer_available ? pass : fail}`, `${text("youtube_live_metric_source", "Source")}: ${test.source || "-"}`, "", `[${text("youtube_live_check_stream_section", "Stream status")}]`, diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index d97af7890d4..dca484632cf 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -18,8 +18,6 @@ window.CarrotTranslations.register("en", { youtube_live_help_step_3: "Paste the key here, save it, then run Validate key.", youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", - youtube_live_stream_active: "Streaming", - youtube_live_stream_inactive: "Stream stopped", youtube_live_quality_standard: "Standard", youtube_live_visibility_studio: "Visibility: set in YouTube Studio", youtube_live_validate_key: "Validate key", @@ -53,17 +51,21 @@ window.CarrotTranslations.register("en", { youtube_live_resource_vision: "Carrot Vision", youtube_live_resource_enabled: "Enabled", youtube_live_resource_disabled: "Disabled", - youtube_live_warning_cluster: "Cluster HUD is enabled. Monitor encoder load and device temperature while streaming.", - youtube_live_warning_vision: "Carrot Vision is enabled. Camera, encoder, and network resources are shared.", + youtube_live_warning_cluster: "Cluster HUD is enabled. Monitor overall load and device temperature during simultaneous use.", + youtube_live_warning_vision: "Carrot Vision is enabled. Simultaneous streaming increases network and memory bandwidth use.", youtube_live_warning_quality: "High quality mode is not available yet.", youtube_live_error_input_invalid: "Could not open the video input because an FFmpeg argument was invalid.", youtube_live_error_ffmpeg_missing: "FFmpeg is not installed.", youtube_live_error_no_frames: "No qRoadEncodeData frames were received.", youtube_live_error_ffmpeg_exit: "FFmpeg exited (code {code}).", youtube_live_error_ffmpeg_pipe: "The FFmpeg input pipe was closed.", + youtube_live_error_muxer_missing: "PyAV MPEG-TS muxing is unavailable.", + youtube_live_error_muxer_failed: "MPEG-TS muxing failed: {error}", youtube_live_test: "Test", youtube_live_test_missing: "Missing stream key or FFmpeg.", + youtube_live_test_muxer_missing: "Stream key, FFmpeg, or PyAV MPEG-TS support is missing.", youtube_live_test_failed: "Test failed", + youtube_live_muxer: "PyAV MPEG-TS", youtube_live_metric_source: "Source", youtube_live_metric_runtime: "Runtime", youtube_live_metric_bitrate: "Bitrate", @@ -74,14 +76,17 @@ window.CarrotTranslations.register("en", { youtube_live_diagnostics: "Diagnostics", youtube_live_state_label: "State", youtube_live_warnings: "Warnings", - youtube_live_state_backoff: "Backoff", - youtube_live_state_disabled: "Disabled", - youtube_live_state_needs_setup: "Needs setup", - youtube_live_state_waiting_frame: "Waiting frame", - youtube_live_state_starting: "Starting", - youtube_live_state_live: "Live", - youtube_live_state_stopping: "Stopping", - youtube_live_state_error: "Error", + youtube_live_error_label: "Error", + youtube_live_warning_label: "Notice", + youtube_live_state_backoff: "Waiting to reconnect RTMPS", + youtube_live_state_disabled: "YouTube stream disabled", + youtube_live_state_needs_setup: "Stream key required", + youtube_live_state_waiting_frame: "Waiting for video frames", + youtube_live_state_waiting_keyframe: "Waiting for an H.264 keyframe", + youtube_live_state_starting: "Starting FFmpeg", + youtube_live_state_live: "Streaming to YouTube RTMPS", + youtube_live_state_stopping: "Stopping FFmpeg", + youtube_live_state_error: "YouTube stream error", home: "Drive", setting: "Setting", setting_tab_device: "Device", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index 21c987b4b4e..e24ebc03c3c 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -18,8 +18,6 @@ window.CarrotTranslations.register("ko", { youtube_live_help_step_3: "위 스트림 키 칸에 붙여넣고 저장 후 키 검증을 실행합니다.", youtube_live_help_step_4: "표준 모드는 qRoadEncodeData를 사용합니다. 영상은 약 526x330/20fps이며 YouTube의 240p-720p 인코더 범위에 맞고, 오디오는 AAC 128Kbps입니다.", youtube_live_help_note: "키 검증은 로컬 형식, FFmpeg, RTMPS 접속만 확인합니다. YouTube의 키 수락 여부는 실제 송출 시작 후 확정됩니다.", - youtube_live_stream_active: "송출 중", - youtube_live_stream_inactive: "송출 중지", youtube_live_quality_standard: "표준", youtube_live_visibility_studio: "공개 범위: YouTube Studio 설정", youtube_live_validate_key: "키 검증", @@ -53,17 +51,21 @@ window.CarrotTranslations.register("ko", { youtube_live_resource_vision: "Carrot Vision", youtube_live_resource_enabled: "사용 중", youtube_live_resource_disabled: "꺼짐", - youtube_live_warning_cluster: "Cluster HUD가 켜져 있습니다. 송출 중 인코더 부하와 기기 온도를 확인하세요.", - youtube_live_warning_vision: "Carrot Vision이 켜져 있습니다. 카메라, 인코더, 네트워크 자원을 함께 사용합니다.", + youtube_live_warning_cluster: "Cluster HUD가 켜져 있습니다. 동시 사용 중 전체 부하와 기기 온도를 확인하세요.", + youtube_live_warning_vision: "Carrot Vision이 켜져 있습니다. 동시 송출 시 네트워크와 메모리 대역폭 사용량이 증가합니다.", youtube_live_warning_quality: "고화질 모드는 아직 사용할 수 없습니다.", youtube_live_error_input_invalid: "FFmpeg 인자가 잘못되어 영상 입력을 열지 못했습니다.", youtube_live_error_ffmpeg_missing: "FFmpeg가 설치되어 있지 않습니다.", youtube_live_error_no_frames: "qRoadEncodeData 프레임이 수신되지 않습니다.", youtube_live_error_ffmpeg_exit: "FFmpeg가 종료되었습니다(코드 {code}).", youtube_live_error_ffmpeg_pipe: "FFmpeg 입력 파이프가 닫혔습니다.", + youtube_live_error_muxer_missing: "PyAV MPEG-TS mux를 사용할 수 없습니다.", + youtube_live_error_muxer_failed: "MPEG-TS mux 실패: {error}", youtube_live_test: "테스트", youtube_live_test_missing: "스트림 키 또는 FFmpeg가 없습니다.", + youtube_live_test_muxer_missing: "스트림 키, FFmpeg 또는 PyAV MPEG-TS 지원이 없습니다.", youtube_live_test_failed: "테스트 실패", + youtube_live_muxer: "PyAV MPEG-TS", youtube_live_metric_source: "소스", youtube_live_metric_runtime: "시간", youtube_live_metric_bitrate: "비트레이트", @@ -74,14 +76,17 @@ window.CarrotTranslations.register("ko", { youtube_live_diagnostics: "진단", youtube_live_state_label: "상태", youtube_live_warnings: "경고", - youtube_live_state_backoff: "재시도 대기", - youtube_live_state_disabled: "꺼짐", - youtube_live_state_needs_setup: "설정 필요", - youtube_live_state_waiting_frame: "프레임 대기", - youtube_live_state_starting: "시작 중", - youtube_live_state_live: "송출 중", - youtube_live_state_stopping: "중지 중", - youtube_live_state_error: "오류", + youtube_live_error_label: "오류", + youtube_live_warning_label: "주의", + youtube_live_state_backoff: "RTMPS 재연결 대기", + youtube_live_state_disabled: "YouTube 송출 꺼짐", + youtube_live_state_needs_setup: "스트림 키 필요", + youtube_live_state_waiting_frame: "영상 프레임 대기", + youtube_live_state_waiting_keyframe: "H.264 키프레임 대기", + youtube_live_state_starting: "FFmpeg 시작 중", + youtube_live_state_live: "YouTube RTMPS 송출 중", + youtube_live_state_stopping: "FFmpeg 종료 중", + youtube_live_state_error: "YouTube 송출 오류", home: "주행", setting: "설정", setting_tab_device: "디바이스", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 382858ac8ed..6ad2a04068a 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -18,8 +18,6 @@ window.CarrotTranslations.register("zh", { youtube_live_help_step_3: "Paste the key here, save it, then run Validate key.", youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", - youtube_live_stream_active: "Streaming", - youtube_live_stream_inactive: "Stream stopped", youtube_live_quality_standard: "Standard", youtube_live_visibility_studio: "Visibility: set in YouTube Studio", youtube_live_validate_key: "Validate key", @@ -53,17 +51,21 @@ window.CarrotTranslations.register("zh", { youtube_live_resource_vision: "Carrot Vision", youtube_live_resource_enabled: "已启用", youtube_live_resource_disabled: "已关闭", - youtube_live_warning_cluster: "Cluster HUD 已启用。推流时请注意编码负载和设备温度。", - youtube_live_warning_vision: "Carrot Vision 已启用。摄像头、编码器和网络资源将被共享。", + youtube_live_warning_cluster: "Cluster HUD 已启用。同时使用时请注意整体负载和设备温度。", + youtube_live_warning_vision: "Carrot Vision 已启用。同时推流会增加网络和内存带宽使用。", youtube_live_warning_quality: "高画质模式暂不可用。", youtube_live_error_input_invalid: "FFmpeg 参数无效,无法打开视频输入。", youtube_live_error_ffmpeg_missing: "未安装 FFmpeg。", youtube_live_error_no_frames: "未收到 qRoadEncodeData 帧。", youtube_live_error_ffmpeg_exit: "FFmpeg 已退出(代码 {code})。", youtube_live_error_ffmpeg_pipe: "FFmpeg 输入管道已关闭。", + youtube_live_error_muxer_missing: "无法使用 PyAV MPEG-TS 封装。", + youtube_live_error_muxer_failed: "MPEG-TS 封装失败:{error}", youtube_live_test: "Test", youtube_live_test_missing: "Missing stream key or FFmpeg.", + youtube_live_test_muxer_missing: "缺少串流密钥、FFmpeg 或 PyAV MPEG-TS 支持。", youtube_live_test_failed: "Test failed", + youtube_live_muxer: "PyAV MPEG-TS", youtube_live_metric_source: "Source", youtube_live_metric_runtime: "Runtime", youtube_live_metric_bitrate: "Bitrate", @@ -74,14 +76,17 @@ window.CarrotTranslations.register("zh", { youtube_live_diagnostics: "Diagnostics", youtube_live_state_label: "State", youtube_live_warnings: "Warnings", - youtube_live_state_backoff: "Backoff", - youtube_live_state_disabled: "Disabled", - youtube_live_state_needs_setup: "Needs setup", - youtube_live_state_waiting_frame: "Waiting frame", - youtube_live_state_starting: "Starting", - youtube_live_state_live: "Live", - youtube_live_state_stopping: "Stopping", - youtube_live_state_error: "Error", + youtube_live_error_label: "错误", + youtube_live_warning_label: "注意", + youtube_live_state_backoff: "等待重新连接 RTMPS", + youtube_live_state_disabled: "YouTube 推流已关闭", + youtube_live_state_needs_setup: "需要串流密钥", + youtube_live_state_waiting_frame: "等待视频帧", + youtube_live_state_waiting_keyframe: "等待 H.264 关键帧", + youtube_live_state_starting: "正在启动 FFmpeg", + youtube_live_state_live: "正在推流到 YouTube RTMPS", + youtube_live_state_stopping: "正在停止 FFmpeg", + youtube_live_state_error: "YouTube 推流错误", home: "驾驶", setting: "设置", setting_tab_device: "设备", From 4b24f5d5e1bd80c1d5065549399289d8882cb071 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 14:11:21 +0900 Subject: [PATCH 45/61] f --- selfdrive/carrot/server/services/youtube_live.py | 8 ++++++++ .../carrot/web/css/pages/settings/youtube_live.css | 9 +-------- selfdrive/carrot/web/index.html | 8 ++++---- .../carrot/web/js/pages/setting_youtube_live.js | 14 ++++++++------ selfdrive/carrot/web/js/translations/en.js | 2 -- selfdrive/carrot/web/js/translations/ko.js | 2 -- selfdrive/carrot/web/js/translations/zh.js | 2 -- 7 files changed, 21 insertions(+), 24 deletions(-) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index f5efc8969a3..21b93127f8c 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -131,6 +131,9 @@ def __init__( self._started_at = 0.0 self._last_frame_at = 0.0 self._last_frame_id: int | None = None + self._frame_width = 526 + self._frame_height = 330 + self._frame_fps = 20 self._bytes_sent = 0 self._session_started_bytes = 0 self._restart_count = 0 @@ -207,6 +210,9 @@ def status(self) -> dict[str, Any]: "last_frame_at": self._last_frame_at, "last_frame_age_ms": last_frame_age_ms, "last_frame_id": self._last_frame_id, + "frame_width": self._frame_width, + "frame_height": self._frame_height, + "frame_fps": self._frame_fps, "stderr_tail": list(self._stderr_tail)[-5:], "resource_status": resource_status, "warnings": warnings, @@ -386,6 +392,8 @@ async def _tick(self) -> None: self._last_frame_at = _now() self._last_frame_id = frame_id + self._frame_width = width + self._frame_height = height if self._process is None: if not keyframe: diff --git a/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/selfdrive/carrot/web/css/pages/settings/youtube_live.css index fd51b15c317..103c2d50ce2 100644 --- a/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -42,18 +42,11 @@ letter-spacing: 0; } -.youtube-live-help-list > li, -.youtube-live-help-note { +.youtube-live-help-list > li { min-width: 0; overflow-wrap: anywhere; } -.youtube-live-help-note { - color: var(--md-on-surface-var); - font-size: var(--fs-body-sm); - letter-spacing: 0; -} - .youtube-live-key-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index 7eb66865f82..c86eb80af64 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -706,9 +706,9 @@

Home

- - - + + + @@ -723,7 +723,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index f0f69bfd261..be0238b44b3 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -58,10 +58,7 @@ } function streamSummary(status) { - const quality = text("youtube_live_quality_standard", "Standard"); - const source = status?.source || "qRoadEncodeData"; - const visibility = text("youtube_live_visibility_studio", "Visibility: set in YouTube Studio"); - return `${quality} · ${source} · ${visibility}`; + return videoFormatLabel(status); } function metric(label, value) { @@ -154,8 +151,6 @@
  • ${escapeHtmlLocal(text("youtube_live_help_step_2", "In Live Control Room, open Stream and copy Stream key."))}
  • ${escapeHtmlLocal(text("youtube_live_help_step_3", "Paste the key here, save it, then run Validate key."))}
  • -
    ${escapeHtmlLocal(text("youtube_live_help_step_4", "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps."))}
    -
    ${escapeHtmlLocal(text("youtube_live_help_note", "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts."))}
    `; @@ -243,6 +238,13 @@ return [h, m, s].map((v) => String(v).padStart(2, "0")).join(":"); } + function videoFormatLabel(status) { + const width = Number(status?.frame_width || 526); + const height = Number(status?.frame_height || 330); + const fps = Number(status?.frame_fps || 20); + return `${width}*${height} * ${fps}fps`; + } + function retryLabel(status) { const retry = Number(status?.retry_in_sec || 0); if (String(status?.state || "") === "backoff" && retry > 0) { diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index dca484632cf..5313e2ce8f4 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -16,8 +16,6 @@ window.CarrotTranslations.register("en", { youtube_live_help_step_1: "Open YouTube Studio and click Create > Go live.", youtube_live_help_step_2: "In Live Control Room, open the Stream tab, create a Stream key, then copy it.", youtube_live_help_step_3: "Paste the key here, save it, then run Validate key.", - youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", - youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", youtube_live_quality_standard: "Standard", youtube_live_visibility_studio: "Visibility: set in YouTube Studio", youtube_live_validate_key: "Validate key", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index e24ebc03c3c..a2adca39f2e 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -16,8 +16,6 @@ window.CarrotTranslations.register("ko", { youtube_live_help_step_1: "YouTube Studio에서 만들기 > 라이브 스트리밍을 엽니다.", youtube_live_help_step_2: "Live Control Room의 스트림 탭에서 Stream key를 생성한 뒤 복사합니다.", youtube_live_help_step_3: "위 스트림 키 칸에 붙여넣고 저장 후 키 검증을 실행합니다.", - youtube_live_help_step_4: "표준 모드는 qRoadEncodeData를 사용합니다. 영상은 약 526x330/20fps이며 YouTube의 240p-720p 인코더 범위에 맞고, 오디오는 AAC 128Kbps입니다.", - youtube_live_help_note: "키 검증은 로컬 형식, FFmpeg, RTMPS 접속만 확인합니다. YouTube의 키 수락 여부는 실제 송출 시작 후 확정됩니다.", youtube_live_quality_standard: "표준", youtube_live_visibility_studio: "공개 범위: YouTube Studio 설정", youtube_live_validate_key: "키 검증", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 6ad2a04068a..1e8ff83a2c1 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -16,8 +16,6 @@ window.CarrotTranslations.register("zh", { youtube_live_help_step_1: "Open YouTube Studio and click Create > Go live.", youtube_live_help_step_2: "In Live Control Room, open the Stream tab, create a Stream key, then copy it.", youtube_live_help_step_3: "Paste the key here, save it, then run Validate key.", - youtube_live_help_step_4: "Standard mode uses qRoadEncodeData, about 526x330 at 20fps. This fits YouTube's 240p-720p encoder range; stereo audio uses AAC 128Kbps.", - youtube_live_help_note: "Validation checks local format, FFmpeg, and RTMPS reachability. YouTube confirms the key only after streaming starts.", youtube_live_quality_standard: "Standard", youtube_live_visibility_studio: "Visibility: set in YouTube Studio", youtube_live_validate_key: "Validate key", From e48573c8818080ac7a4e674e8798369715f5a25e Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 14:43:46 +0900 Subject: [PATCH 46/61] f --- .../carrot/server/features/youtube_live.py | 2 +- .../carrot/server/services/youtube_live.py | 322 +++++++----------- .../server/services/youtube_live_muxer.py | 163 +++++---- .../server/services/youtube_live_transport.py | 169 +++++++++ selfdrive/carrot/web/index.html | 8 +- .../web/js/pages/setting_youtube_live.js | 46 ++- selfdrive/carrot/web/js/translations/en.js | 26 +- selfdrive/carrot/web/js/translations/ko.js | 26 +- selfdrive/carrot/web/js/translations/zh.js | 26 +- 9 files changed, 461 insertions(+), 327 deletions(-) create mode 100644 selfdrive/carrot/server/services/youtube_live_transport.py diff --git a/selfdrive/carrot/server/features/youtube_live.py b/selfdrive/carrot/server/features/youtube_live.py index 6389832e9cb..3842a4d5f9e 100644 --- a/selfdrive/carrot/server/features/youtube_live.py +++ b/selfdrive/carrot/server/features/youtube_live.py @@ -55,7 +55,7 @@ async def api_clear_stream_key(request: web.Request) -> web.Response: async def api_test(request: web.Request) -> web.Response: - result = _service(request).test_config() + result = await asyncio.to_thread(_service(request).test_config) return web.json_response(result, status=200 if result.get("ok") else 409) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index 21b93127f8c..dc96a86547f 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -4,23 +4,22 @@ import json import os import re -import shutil import socket import ssl import time -from collections import deque from pathlib import Path from typing import Any from ..config import CARROT_YOUTUBE_LIVE_SECRET_PATH, CARROT_YOUTUBE_LIVE_STATE_PATH -from .youtube_live_muxer import H264MpegTsMuxer, pyav_capabilities +from .youtube_live_muxer import H264FlvMuxer, pyav_capabilities +from .youtube_live_transport import LibrtmpClient, RtmpSink, librtmp_capabilities YOUTUBE_LIVE_PARAM = "CarrotYouTubeLive" YOUTUBE_QUALITY_PARAM = "CarrotYouTubeQuality" PHASE1_SOURCE_SERVICE = "qRoadEncodeData" PHASE1_QUALITY = "standard" -YOUTUBE_RTMPS_BASE = "rtmps://a.rtmps.youtube.com/live2" +YOUTUBE_RTMPS_BASE = "rtmps://a.rtmps.youtube.com:443/live2" YOUTUBE_RTMPS_HOST = "a.rtmps.youtube.com" YOUTUBE_RTMPS_PORT = 443 NO_FRAME_STOP_SECONDS = 8.0 @@ -28,7 +27,7 @@ STATUS_WRITE_MIN_INTERVAL = 1.0 BACKOFF_BASE_SECONDS = 3.0 BACKOFF_MAX_SECONDS = 60.0 -PROCESS_STABLE_SECONDS = 10.0 +STREAM_STABLE_SECONDS = 10.0 _PROCESS_MATCHES = { "carrot_cluster": "selfdrive.carrot.cluster_autorun", @@ -120,10 +119,9 @@ def __init__( self.secret_path = Path(secret_path) self._task: asyncio.Task | None = None self._stop_event = asyncio.Event() - self._process: asyncio.subprocess.Process | None = None - self._muxer: H264MpegTsMuxer | None = None - self._stderr_task: asyncio.Task | None = None - self._stderr_tail: deque[str] = deque(maxlen=20) + self._transport: LibrtmpClient | None = None + self._transport_connected = False + self._muxer: H264FlvMuxer | None = None self._messaging: Any | None = None self._socket: Any | None = None self._params: Any | None = None @@ -142,6 +140,7 @@ def __init__( self._last_error = "" self._state = "disabled" self._muxer_capabilities = pyav_capabilities() + self._transport_capabilities = librtmp_capabilities() persisted = _read_json(self.state_path) try: self._bytes_sent = int(persisted.get("bytes_sent") or 0) @@ -164,16 +163,16 @@ async def stop(self) -> None: await task except asyncio.CancelledError: pass - await self._stop_process() + await self._stop_stream() self._task = None self._write_status(force=True) def status(self) -> dict[str, Any]: stream_key = self.get_stream_key() - pid = self._process.pid if self._process and self._process.returncode is None else None + running = self._transport_connected now = _now() - uptime = int(now - self._started_at) if self._started_at and pid else 0 - elapsed = max(1.0, now - self._started_at) if self._started_at and pid else 1.0 + uptime = int(now - self._started_at) if self._started_at and running else 0 + elapsed = max(1.0, now - self._started_at) if self._started_at and running else 1.0 last_frame_age_ms = int((now - self._last_frame_at) * 1000) if self._last_frame_at else None resource_status = self._resource_status() warnings = self._warnings(resource_status) @@ -183,25 +182,27 @@ def status(self) -> dict[str, Any]: "configured": bool(stream_key), "masked_key": _mask_stream_key(stream_key), "source": PHASE1_SOURCE_SERVICE, - "muxer": "pyav-mpegts", + "muxer": "pyav-flv-aac", "muxer_available": bool( self._muxer_capabilities.get("available") - and self._muxer_capabilities.get("mpegts") + and self._muxer_capabilities.get("flv") and self._muxer_capabilities.get("h264") + and self._muxer_capabilities.get("aac") ), + "transport": "librtmp-rtmps", + "transport_available": bool(self._transport_capabilities.get("available")), "quality": PHASE1_QUALITY, "requested_quality": self._param_int(YOUTUBE_QUALITY_PARAM, 0), "phase": 1, - "running": bool(pid), - "pid": pid, - "ffmpeg_available": bool(shutil.which("ffmpeg")), - "started_at": self._started_at if pid else 0, + "running": running, + "pid": None, + "started_at": self._started_at if running else 0, "uptime_sec": uptime, "bytes_sent": self._bytes_sent, "total_mb": round(max(0, self._bytes_sent) / (1024 * 1024), 2), "session_bytes": max(0, self._bytes_sent - self._session_started_bytes), "session_mb": round(max(0, self._bytes_sent - self._session_started_bytes) / (1024 * 1024), 2), - "estimated_kbps": int(((self._bytes_sent - self._session_started_bytes) * 8 / 1000) / elapsed) if pid else 0, + "estimated_kbps": int(((self._bytes_sent - self._session_started_bytes) * 8 / 1000) / elapsed) if running else 0, "restart_count": self._restart_count, "consecutive_failures": self._consecutive_failures, "next_retry_at": self._next_retry_at, @@ -213,42 +214,52 @@ def status(self) -> dict[str, Any]: "frame_width": self._frame_width, "frame_height": self._frame_height, "frame_fps": self._frame_fps, - "stderr_tail": list(self._stderr_tail)[-5:], + "log_tail": [self._last_error] if self._last_error else [], "resource_status": resource_status, "warnings": warnings, } def test_config(self) -> dict[str, Any]: stream_key = self.get_stream_key() - ffmpeg_path = shutil.which("ffmpeg") + rtmps_ok, rtmps_message = _check_rtmps_reachable() muxer_ok = bool( self._muxer_capabilities.get("available") - and self._muxer_capabilities.get("mpegts") + and self._muxer_capabilities.get("flv") and self._muxer_capabilities.get("h264") + and self._muxer_capabilities.get("aac") ) - ok = bool(stream_key and ffmpeg_path and muxer_ok) + transport_ok = bool(self._transport_capabilities.get("available")) + ok = bool(stream_key and muxer_ok and transport_ok and rtmps_ok) status = self.status() return { "ok": ok, "configured": bool(stream_key), - "ffmpeg_available": bool(ffmpeg_path), - "ffmpeg_path": ffmpeg_path or "", + "transport_available": transport_ok, + "transport": dict(self._transport_capabilities), + "rtmps_reachable": rtmps_ok, + "rtmps_message": rtmps_message, "muxer_available": muxer_ok, "muxer": dict(self._muxer_capabilities), "source": PHASE1_SOURCE_SERVICE, "quality": PHASE1_QUALITY, "resource_status": status["resource_status"], "warnings": status["warnings"], - "stderr_tail": list(self._stderr_tail), - "message": "ready" if ok else "missing stream key, FFmpeg, or PyAV MPEG-TS support", + "log_tail": [self._last_error] if self._last_error else [], + "message": "ready" if ok else "stream key, RTMPS network, librtmp, or PyAV FLV/AAC is unavailable", } def validate_stream_key(self, value: str | None = None) -> dict[str, Any]: stream_key = _extract_stream_key(value) if value is not None else self.get_stream_key() format_ok, format_message = _validate_stream_key_format(stream_key) rtmps_ok, rtmps_message = _check_rtmps_reachable() - ffmpeg_path = shutil.which("ffmpeg") - ok = bool(format_ok and rtmps_ok and ffmpeg_path) + transport_ok = bool(self._transport_capabilities.get("available")) + muxer_ok = bool( + self._muxer_capabilities.get("available") + and self._muxer_capabilities.get("flv") + and self._muxer_capabilities.get("h264") + and self._muxer_capabilities.get("aac") + ) + ok = bool(format_ok and rtmps_ok and transport_ok and muxer_ok) return { "ok": ok, "configured": bool(self.get_stream_key()), @@ -256,8 +267,8 @@ def validate_stream_key(self, value: str | None = None) -> dict[str, Any]: "format_message": format_message, "rtmps_reachable": rtmps_ok, "rtmps_message": rtmps_message, - "ffmpeg_available": bool(ffmpeg_path), - "ffmpeg_path": ffmpeg_path or "", + "transport_available": transport_ok, + "muxer_available": muxer_ok, "masked_key": _mask_stream_key(stream_key), "note": "YouTube only confirms whether the key is accepted when an encoder starts streaming.", } @@ -282,13 +293,14 @@ def diagnostics(self) -> dict[str, Any]: "DisableDM": self._param_int("DisableDM", 0), "IsOnroad": self._param_bool("IsOnroad", False), }, - "ffmpeg": { - "available": bool(shutil.which("ffmpeg")), - "path": shutil.which("ffmpeg") or "", - "stderr_tail": list(self._stderr_tail), + "transport": { + "name": "librtmp-rtmps", + **self._transport_capabilities, + "connected": self._transport_connected, + "log_tail": [self._last_error] if self._last_error else [], }, "muxer": { - "name": "pyav-mpegts", + "name": "pyav-flv-aac", **self._muxer_capabilities, }, "processes": self._process_status(), @@ -325,16 +337,15 @@ async def _run(self) -> None: except Exception as exc: self._last_error = str(exc) self._set_state("error") - await self._stop_process() + await self._stop_stream() await asyncio.sleep(START_BACKOFF_SECONDS) - await asyncio.sleep(0.02 if self._process else 0.25) + await asyncio.sleep(0.02 if self._transport else 0.25) async def _tick(self) -> None: if not self._param_enabled(): - if self._process is not None: - await self._stop_process() + if self._transport is not None: + await self._stop_stream() self._last_error = "" - self._stderr_tail.clear() self._consecutive_failures = 0 self._next_retry_at = 0.0 self._set_state("disabled") @@ -342,28 +353,29 @@ async def _tick(self) -> None: stream_key = self.get_stream_key() if not stream_key: - await self._stop_process() + await self._stop_stream() self._set_state("needs_setup") return - if not shutil.which("ffmpeg"): - await self._stop_process() - self._last_error = "ffmpeg not found" + if not self._transport_capabilities.get("available"): + await self._stop_stream() + self._last_error = self._transport_capabilities.get("error") or "librtmp is unavailable" self._set_state("error") return if not ( self._muxer_capabilities.get("available") - and self._muxer_capabilities.get("mpegts") + and self._muxer_capabilities.get("flv") and self._muxer_capabilities.get("h264") + and self._muxer_capabilities.get("aac") ): - await self._stop_process() - self._last_error = "PyAV MPEG-TS muxer is unavailable" + await self._stop_stream() + self._last_error = "PyAV FLV/AAC muxer is unavailable" self._set_state("error") return if self._param_int(YOUTUBE_QUALITY_PARAM, 0) > 0: - await self._stop_process() + await self._stop_stream() self._last_error = "high quality YouTube Live is planned for Phase 3" self._set_state("error") return @@ -372,22 +384,21 @@ async def _tick(self) -> None: self._set_state("backoff") return - if self._process is not None and self._process.returncode is not None: - exit_code = self._process.returncode - if not self._last_error: - self._last_error = f"ffmpeg exited code={exit_code}" - await self._stop_process() - self._schedule_backoff("ffmpeg exited") + if self._transport is not None and not await asyncio.to_thread(self._transport.is_connected): + self._transport_connected = False + self._last_error = "YouTube RTMPS connection closed" + await self._stop_stream() + self._schedule_backoff("RTMPS connection closed") self._set_state("backoff") return - payload, frame_id, keyframe, width, height = self._recv_payload() - if not payload: - if self._process is not None and self._last_frame_at and _now() - self._last_frame_at > NO_FRAME_STOP_SECONDS: + header, data, frame_id, keyframe, width, height = self._recv_frame() + if not data: + if self._transport is not None and self._last_frame_at and _now() - self._last_frame_at > NO_FRAME_STOP_SECONDS: self._last_error = "no qRoadEncodeData frames" - await self._stop_process() + await self._stop_stream() self._schedule_backoff("frame timeout") - self._set_state("waiting_frame" if self._process is None else "live") + self._set_state("waiting_frame" if self._transport is None else "live") return self._last_frame_at = _now() @@ -395,13 +406,19 @@ async def _tick(self) -> None: self._frame_width = width self._frame_height = height - if self._process is None: - if not keyframe: + if self._transport is None: + if not keyframe or not header: self._set_state("waiting_keyframe") return - await self._start_process(stream_key, width=width, height=height) + try: + await self._start_stream(stream_key, codec_header=header, width=width, height=height) + except Exception as exc: + self._last_error = f"YouTube RTMPS start failed: {exc}" + self._schedule_backoff("RTMPS start failed") + self._set_state("backoff") + return - if await self._write_frame(payload, keyframe=keyframe): + if await self._write_frame(data, keyframe=keyframe): self._set_state("live") def _set_state(self, state: str) -> None: @@ -487,19 +504,19 @@ def _get_socket(self) -> Any | None: self._socket = None return self._socket - def _recv_payload(self) -> tuple[bytes, int | None, bool, int, int]: + def _recv_frame(self) -> tuple[bytes, bytes, int | None, bool, int, int]: messaging = self._get_messaging() sock = self._get_socket() if messaging is None or sock is None: - return b"", None, False, 526, 330 + return b"", b"", None, False, 526, 330 try: msg = messaging.recv_one_or_none(sock) if msg is None: - return b"", None, False, 526, 330 + return b"", b"", None, False, 526, 330 which = msg.which() frame = getattr(msg, which, None) if frame is None: - return b"", None, False, 526, 330 + return b"", b"", None, False, 526, 330 header = bytes(getattr(frame, "header", b"") or b"") data = bytes(getattr(frame, "data", b"") or b"") frame_id = None @@ -514,162 +531,85 @@ def _recv_payload(self) -> tuple[bytes, int | None, bool, int, int]: flags = int(getattr(idx, "flags", 0) or 0) except Exception: flags = 0 - if frame_id is None: - try: - frame_id = int(getattr(frame, "frameId", 0) or 0) or None - except Exception: - frame_id = None width = int(getattr(frame, "width", 0) or 526) height = int(getattr(frame, "height", 0) or 330) keyframe = bool(header) or bool(flags & 0x8) - return header + data, frame_id, keyframe, width, height + return header, data, frame_id, keyframe, width, height except Exception as exc: self._last_error = f"{PHASE1_SOURCE_SERVICE} recv failed: {exc}" - return b"", None, False, 526, 330 + return b"", b"", None, False, 526, 330 - async def _start_process(self, stream_key: str, *, width: int, height: int) -> None: - await self._stop_process() - self._stderr_tail.clear() + async def _start_stream(self, stream_key: str, *, codec_header: bytes, width: int, height: int) -> None: + await self._stop_stream() self._last_error = "" + self._restart_count += 1 self._set_state("starting") - self._started_at = _now() - self._session_started_bytes = self._bytes_sent rtmp_url = f"{YOUTUBE_RTMPS_BASE}/{stream_key}" - muxer = H264MpegTsMuxer(fps=20, width=width, height=height) - cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", - "warning", - "-fflags", - "nobuffer", - "-f", - "mpegts", - "-i", - "pipe:0", - "-f", - "lavfi", - "-i", - "anullsrc=r=44100:cl=stereo", - "-map", - "0:v:0", - "-map", - "1:a:0", - "-c:v", - "copy", - "-c:a", - "aac", - "-b:a", - "128k", - "-shortest", - "-f", - "flv", - rtmp_url, - ] + transport = LibrtmpClient(rtmp_url) try: - self._process = await asyncio.create_subprocess_exec( - *cmd, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.PIPE, + await asyncio.to_thread(transport.connect) + muxer = await asyncio.to_thread( + H264FlvMuxer, + RtmpSink(transport), + codec_header=codec_header, + fps=self._frame_fps, + width=width, + height=height, ) except Exception: - muxer.close() + await asyncio.to_thread(transport.close) raise + self._transport = transport + self._transport_connected = True self._muxer = muxer - self._restart_count += 1 + self._started_at = _now() + self._session_started_bytes = self._bytes_sent self._next_retry_at = 0.0 - self._stderr_task = asyncio.create_task(self._read_stderr(self._process), name="carrot-youtube-live-stderr") async def _write_frame(self, payload: bytes, *, keyframe: bool) -> bool: - proc = self._process + transport = self._transport muxer = self._muxer - if proc is None or proc.stdin is None or muxer is None: + if transport is None or muxer is None: return False - if proc.returncode is not None: - self._last_error = f"ffmpeg exited code={proc.returncode}" - self._schedule_backoff("ffmpeg exited") - await self._stop_process() + if not await asyncio.to_thread(transport.is_connected): + self._transport_connected = False + self._last_error = "YouTube RTMPS connection closed" + self._schedule_backoff("RTMPS connection closed") + await self._stop_stream() return False try: - chunk = muxer.mux(payload, keyframe=keyframe) - if chunk: - proc.stdin.write(chunk) - await proc.stdin.drain() - self._bytes_sent += len(chunk) - if self._started_at and _now() - self._started_at >= PROCESS_STABLE_SECONDS: + await asyncio.to_thread(muxer.mux, payload, keyframe=keyframe) + self._bytes_sent = self._session_started_bytes + transport.bytes_written + if self._started_at and _now() - self._started_at >= STREAM_STABLE_SECONDS: self._consecutive_failures = 0 return True - except (BrokenPipeError, ConnectionResetError) as exc: - self._last_error = f"ffmpeg pipe closed: {exc}" - self._schedule_backoff("ffmpeg pipe closed") - await self._stop_process() - return False except Exception as exc: - self._last_error = f"MPEG-TS mux failed: {exc}" - self._schedule_backoff("MPEG-TS mux failed") - await self._stop_process() + self._transport_connected = False + self._last_error = f"YouTube RTMPS publish failed: {exc}" + self._schedule_backoff("RTMPS publish failed") + await self._stop_stream() return False - async def _read_stderr(self, proc: asyncio.subprocess.Process) -> None: - if proc.stderr is None: - return - while True: - line = await proc.stderr.readline() - if not line: - break - text = line.decode("utf-8", errors="replace").strip() - if text: - self._stderr_tail.append(text[-300:]) - self._last_error = text[-300:] - - async def _stop_process(self) -> None: - proc = self._process - self._process = None + async def _stop_stream(self) -> None: + transport = self._transport + self._transport = None + self._transport_connected = False muxer = self._muxer self._muxer = None - stderr_task = self._stderr_task - self._stderr_task = None - trailer = b"" - if muxer is not None: - try: - trailer = muxer.close() - except Exception: - trailer = b"" - if proc is None: - if stderr_task is not None and not stderr_task.done(): - stderr_task.cancel() + if transport is None and muxer is None: + self._started_at = 0.0 return self._set_state("stopping") - if proc.stdin is not None: + if muxer is not None: try: - if trailer and proc.returncode is None: - proc.stdin.write(trailer) - await proc.stdin.drain() - proc.stdin.close() - await proc.stdin.wait_closed() + await asyncio.to_thread(muxer.close) except Exception: pass - try: - await asyncio.wait_for(proc.wait(), timeout=2.0) - except asyncio.TimeoutError: - try: - proc.terminate() - except ProcessLookupError: - pass + if transport is not None: + self._bytes_sent = self._session_started_bytes + transport.bytes_written try: - await asyncio.wait_for(proc.wait(), timeout=3.0) - except asyncio.TimeoutError: - try: - proc.kill() - except ProcessLookupError: - pass - await proc.wait() - if stderr_task is not None and not stderr_task.done(): - stderr_task.cancel() - try: - await stderr_task - except asyncio.CancelledError: + await asyncio.to_thread(transport.close) + except Exception: pass self._started_at = 0.0 diff --git a/selfdrive/carrot/server/services/youtube_live_muxer.py b/selfdrive/carrot/server/services/youtube_live_muxer.py index 41e9150f57f..9fcee971904 100644 --- a/selfdrive/carrot/server/services/youtube_live_muxer.py +++ b/selfdrive/carrot/server/services/youtube_live_muxer.py @@ -1,41 +1,16 @@ from __future__ import annotations -import io +import threading from fractions import Fraction -from typing import Any +from typing import Any, BinaryIO -MPEGTS_FORMAT = "mpegts" +FLV_FORMAT = "flv" H264_CODEC = "h264" - - -class _StreamingBuffer(io.RawIOBase): - def __init__(self) -> None: - super().__init__() - self._chunks = bytearray() - self._position = 0 - - def writable(self) -> bool: - return True - - def seekable(self) -> bool: - return False - - def write(self, data: bytes | bytearray | memoryview) -> int: - chunk = bytes(data) - self._chunks.extend(chunk) - self._position += len(chunk) - return len(chunk) - - def tell(self) -> int: - return self._position - - def drain(self) -> bytes: - if not self._chunks: - return b"" - chunk = bytes(self._chunks) - self._chunks.clear() - return chunk +AAC_CODEC = "aac" +AUDIO_RATE = 44_100 +AUDIO_BITRATE = 128_000 +AUDIO_SAMPLES = 1_024 def pyav_capabilities() -> dict[str, Any]: @@ -45,8 +20,9 @@ def pyav_capabilities() -> dict[str, Any]: return { "available": False, "version": "", - "mpegts": False, + "flv": False, "h264": False, + "aac": False, "error": str(exc), } formats = getattr(av, "formats_available", set()) @@ -54,59 +30,98 @@ def pyav_capabilities() -> dict[str, Any]: return { "available": True, "version": str(getattr(av, "__version__", "")), - "mpegts": MPEGTS_FORMAT in formats, + "flv": FLV_FORMAT in formats, "h264": H264_CODEC in codecs, + "aac": AAC_CODEC in codecs, "error": "", } -class H264MpegTsMuxer: - def __init__(self, *, fps: int = 20, width: int = 526, height: int = 330) -> None: +class H264FlvMuxer: + def __init__( + self, + output: BinaryIO, + *, + codec_header: bytes, + fps: int = 20, + width: int = 526, + height: int = 330, + ) -> None: + if not codec_header: + raise ValueError("H.264 codec header is required") + import av self._av = av self._fps = max(1, int(fps)) - self._time_base = Fraction(1, self._fps) - self._buffer = _StreamingBuffer() + self._video_time_base = Fraction(1, self._fps) + self._audio_time_base = Fraction(1, AUDIO_RATE) self._container = av.open( - self._buffer, + output, mode="w", - format=MPEGTS_FORMAT, - options={"flush_packets": "1", "mpegts_flags": "+resend_headers"}, + format=FLV_FORMAT, + options={"flvflags": "no_duration_filesize", "flush_packets": "1"}, ) - self._stream = self._container.add_stream(H264_CODEC, rate=self._fps) - self._stream.width = max(1, int(width)) - self._stream.height = max(1, int(height)) - self._stream.time_base = self._time_base + self._video_stream = self._container.add_stream(H264_CODEC, rate=self._fps) + self._video_stream.width = max(1, int(width)) + self._video_stream.height = max(1, int(height)) + self._video_stream.time_base = self._video_time_base + self._video_stream.codec_context.extradata = bytes(codec_header) + + self._audio_stream = self._container.add_stream(AAC_CODEC, rate=AUDIO_RATE) + self._audio_stream.bit_rate = AUDIO_BITRATE + self._audio_stream.layout = "stereo" + self._container.start_encoding() self._packet_index = 0 + self._audio_pts = 0 self._closed = False - - def mux(self, payload: bytes, *, keyframe: bool = False) -> bytes: - if self._closed: - raise RuntimeError("MPEG-TS muxer is closed") - if not payload: - return b"" - packet = self._av.Packet(payload) - packet.stream = self._stream - packet.pts = self._packet_index - packet.dts = self._packet_index - packet.duration = 1 - packet.time_base = self._time_base - if keyframe: + self._lock = threading.RLock() + + def mux(self, payload: bytes, *, keyframe: bool = False) -> None: + with self._lock: + if self._closed: + raise RuntimeError("FLV muxer is closed") + if not payload: + return + + target_audio_pts = int(self._packet_index * AUDIO_RATE / self._fps) + self._mux_silence_until(target_audio_pts) + + packet = self._av.Packet(payload) + packet.stream = self._video_stream + packet.pts = self._packet_index + packet.dts = self._packet_index + packet.duration = 1 + packet.time_base = self._video_time_base + if keyframe: + try: + packet.is_keyframe = True + except Exception: + pass + self._container.mux(packet) + self._packet_index += 1 + + def close(self) -> None: + with self._lock: + if self._closed: + return + self._closed = True try: - packet.is_keyframe = True - except Exception: - pass - self._container.mux(packet) - self._packet_index += 1 - return self._buffer.drain() - - def close(self) -> bytes: - if self._closed: - return self._buffer.drain() - self._closed = True - try: - self._container.close() - except Exception: - pass - return self._buffer.drain() + target_audio_pts = int(self._packet_index * AUDIO_RATE / self._fps) + self._mux_silence_until(target_audio_pts) + for packet in self._audio_stream.encode(None): + self._container.mux(packet) + finally: + self._container.close() + + def _mux_silence_until(self, target_pts: int) -> None: + while self._audio_pts <= target_pts: + frame = self._av.AudioFrame(format="fltp", layout="stereo", samples=AUDIO_SAMPLES) + frame.sample_rate = AUDIO_RATE + frame.pts = self._audio_pts + frame.time_base = self._audio_time_base + for plane in frame.planes: + plane.update(bytes(plane.buffer_size)) + for packet in self._audio_stream.encode(frame): + self._container.mux(packet) + self._audio_pts += AUDIO_SAMPLES diff --git a/selfdrive/carrot/server/services/youtube_live_transport.py b/selfdrive/carrot/server/services/youtube_live_transport.py new file mode 100644 index 00000000000..ffe50095d61 --- /dev/null +++ b/selfdrive/carrot/server/services/youtube_live_transport.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +import ctypes +import ctypes.util +import io +import threading +from typing import Any + + +REQUIRED_RTMP_SYMBOLS = ( + "RTMP_Alloc", + "RTMP_Init", + "RTMP_SetupURL", + "RTMP_EnableWrite", + "RTMP_Connect", + "RTMP_ConnectStream", + "RTMP_Write", + "RTMP_IsConnected", + "RTMP_Close", + "RTMP_Free", +) + + +class LibrtmpError(RuntimeError): + pass + + +def librtmp_capabilities() -> dict[str, Any]: + path = ctypes.util.find_library("rtmp") or "" + if not path: + return {"available": False, "path": "", "missing_symbols": [], "error": "librtmp not found"} + try: + library = ctypes.CDLL(path) + missing = [name for name in REQUIRED_RTMP_SYMBOLS if not hasattr(library, name)] + return { + "available": not missing, + "path": path, + "missing_symbols": missing, + "error": f"missing librtmp symbols: {', '.join(missing)}" if missing else "", + } + except Exception as exc: + return {"available": False, "path": path, "missing_symbols": [], "error": str(exc)} + + +class LibrtmpClient: + def __init__(self, url: str) -> None: + path = ctypes.util.find_library("rtmp") + if not path: + raise LibrtmpError("librtmp not found") + self._library = ctypes.CDLL(path) + self._configure_library() + self._url = url + self._url_buffer: ctypes.Array[ctypes.c_char] | None = None + self._handle: int | None = None + self._lock = threading.RLock() + self._bytes_written = 0 + + @property + def bytes_written(self) -> int: + with self._lock: + return self._bytes_written + + def connect(self) -> None: + with self._lock: + if self._handle is not None: + return + handle = self._library.RTMP_Alloc() + if not handle: + raise LibrtmpError("librtmp allocation failed") + self._handle = handle + try: + self._library.RTMP_Init(handle) + self._url_buffer = ctypes.create_string_buffer(self._url.encode("utf-8")) + if not self._library.RTMP_SetupURL(handle, self._url_buffer): + raise LibrtmpError("librtmp URL setup failed") + self._library.RTMP_EnableWrite(handle) + if not self._library.RTMP_Connect(handle, None): + raise LibrtmpError("YouTube RTMPS connection failed") + if not self._library.RTMP_ConnectStream(handle, 0): + raise LibrtmpError("YouTube rejected the publish connection") + except Exception: + self._close_locked() + raise + + def write(self, data: bytes | bytearray | memoryview) -> int: + payload = bytes(data) + if not payload: + return 0 + with self._lock: + handle = self._handle + if handle is None or not self._library.RTMP_IsConnected(handle): + raise LibrtmpError("YouTube RTMPS connection is closed") + offset = 0 + while offset < len(payload): + buffer = ctypes.create_string_buffer(payload[offset:]) + written = int(self._library.RTMP_Write(handle, buffer, len(payload) - offset)) + if written <= 0: + raise LibrtmpError("YouTube RTMPS write failed") + offset += written + self._bytes_written += offset + return offset + + def is_connected(self) -> bool: + with self._lock: + return bool(self._handle and self._library.RTMP_IsConnected(self._handle)) + + def close(self) -> None: + with self._lock: + self._close_locked() + + def _close_locked(self) -> None: + handle = self._handle + self._handle = None + if handle is None: + self._url_buffer = None + return + try: + self._library.RTMP_Close(handle) + finally: + self._library.RTMP_Free(handle) + self._url_buffer = None + + def _configure_library(self) -> None: + missing = [name for name in REQUIRED_RTMP_SYMBOLS if not hasattr(self._library, name)] + if missing: + raise LibrtmpError(f"missing librtmp symbols: {', '.join(missing)}") + + self._library.RTMP_Alloc.restype = ctypes.c_void_p + self._library.RTMP_Init.argtypes = [ctypes.c_void_p] + self._library.RTMP_SetupURL.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + self._library.RTMP_SetupURL.restype = ctypes.c_int + self._library.RTMP_EnableWrite.argtypes = [ctypes.c_void_p] + self._library.RTMP_Connect.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + self._library.RTMP_Connect.restype = ctypes.c_int + self._library.RTMP_ConnectStream.argtypes = [ctypes.c_void_p, ctypes.c_int] + self._library.RTMP_ConnectStream.restype = ctypes.c_int + self._library.RTMP_Write.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] + self._library.RTMP_Write.restype = ctypes.c_int + self._library.RTMP_IsConnected.argtypes = [ctypes.c_void_p] + self._library.RTMP_IsConnected.restype = ctypes.c_int + self._library.RTMP_Close.argtypes = [ctypes.c_void_p] + self._library.RTMP_Free.argtypes = [ctypes.c_void_p] + if hasattr(self._library, "RTMP_LogSetLevel"): + self._library.RTMP_LogSetLevel.argtypes = [ctypes.c_int] + self._library.RTMP_LogSetLevel(1) + + +class RtmpSink(io.RawIOBase): + def __init__(self, client: LibrtmpClient) -> None: + super().__init__() + self._client = client + self._position = 0 + + def writable(self) -> bool: + return True + + def seekable(self) -> bool: + return False + + def write(self, data: bytes | bytearray | memoryview) -> int: + written = self._client.write(data) + self._position += written + return written + + def tell(self) -> int: + return self._position + + def flush(self) -> None: + return None diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index c86eb80af64..f0a6369e39b 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -706,9 +706,9 @@

    Home

    - - - + + + @@ -723,7 +723,7 @@

    Home

    - + diff --git a/selfdrive/carrot/web/js/pages/setting_youtube_live.js b/selfdrive/carrot/web/js/pages/setting_youtube_live.js index be0238b44b3..d1f49623e82 100644 --- a/selfdrive/carrot/web/js/pages/setting_youtube_live.js +++ b/selfdrive/carrot/web/js/pages/setting_youtube_live.js @@ -335,7 +335,8 @@ ok: false, format_ok: false, format_message: err?.message || String(err), - ffmpeg_available: false, + transport_available: false, + muxer_available: false, rtmps_reachable: false, rtmps_message: "-", }; @@ -380,7 +381,8 @@ const fail = text("youtube_live_validation_fail", "FAIL"); const lines = [ `${text("youtube_live_validation_format", "Key format")}: ${result?.format_ok ? pass : fail} - ${localizedValidationMessage(result?.format_message)}`, - `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${result?.ffmpeg_available ? pass : fail}`, + `${text("youtube_live_muxer", "PyAV FLV/AAC")}: ${result?.muxer_available ? pass : fail}`, + `${text("youtube_live_validation_transport", "librtmp")}: ${result?.transport_available ? pass : fail}`, `${text("youtube_live_validation_rtmps", "YouTube RTMPS")}: ${result?.rtmps_reachable ? pass : fail} - ${localizedValidationMessage(result?.rtmps_message)}`, ]; const visibleKey = String(streamKey || ""); @@ -401,10 +403,15 @@ "stream key contains unsupported characters": text("youtube_live_validation_bad_chars", "Stream key contains unsupported characters."), "format looks valid": text("youtube_live_validation_format_ok", "Format looks valid."), "YouTube RTMPS ingest is reachable": text("youtube_live_validation_rtmps_ok", "YouTube RTMPS ingest is reachable."), - "missing stream key or ffmpeg": text("youtube_live_test_missing", "Missing stream key or FFmpeg."), - "missing stream key, FFmpeg, or PyAV MPEG-TS support": text("youtube_live_test_muxer_missing", "Stream key, FFmpeg, or PyAV MPEG-TS support is missing."), + "missing stream key, librtmp, or PyAV FLV/AAC support": text("youtube_live_test_muxer_missing", "Stream key, librtmp, or PyAV FLV/AAC support is missing."), + "stream key, RTMPS network, librtmp, or PyAV FLV/AAC is unavailable": text("youtube_live_test_muxer_missing", "Stream key, RTMPS network, librtmp, or PyAV FLV/AAC is unavailable."), "ready": text("youtube_live_check_ready", "Ready"), }; + if (raw.startsWith("YouTube RTMPS ingest unreachable:")) { + return text("youtube_live_validation_rtmps_failed", "Cannot reach YouTube RTMPS: {error}", { + error: raw.slice(raw.indexOf(":") + 1).trim() || "-", + }); + } return map[raw] || raw; } @@ -417,19 +424,18 @@ "Carrot Vision is enabled; YouTube Live shares camera/encoder/network resources.": text("youtube_live_warning_vision", "Carrot Vision is enabled. Camera, encoder, and network resources are shared."), "Carrot Vision is enabled; simultaneous streaming increases network and memory bandwidth use.": text("youtube_live_warning_vision", "Carrot Vision is enabled. Simultaneous streaming increases network and memory bandwidth use."), "High quality mode is planned for Phase 3; Phase 2 keeps qRoadEncodeData only.": text("youtube_live_warning_quality", "High quality mode is not available yet."), - "Error opening input files: Invalid argument": text("youtube_live_error_input_invalid", "Could not open the video input because an FFmpeg argument was invalid."), - "ffmpeg not found": text("youtube_live_error_ffmpeg_missing", "FFmpeg is not installed."), - "PyAV MPEG-TS muxer is unavailable": text("youtube_live_error_muxer_missing", "PyAV MPEG-TS muxing is unavailable."), + "librtmp is unavailable": text("youtube_live_error_transport_missing", "librtmp is unavailable."), + "librtmp not found": text("youtube_live_error_transport_missing", "librtmp is unavailable."), + "PyAV FLV/AAC muxer is unavailable": text("youtube_live_error_muxer_missing", "PyAV FLV/AAC muxing is unavailable."), + "YouTube RTMPS connection closed": text("youtube_live_error_transport_closed", "The YouTube RTMPS connection was closed."), "no qRoadEncodeData frames": text("youtube_live_error_no_frames", "No qRoadEncodeData frames were received."), }; if (exact[raw]) return exact[raw]; - const exitMatch = raw.match(/^ffmpeg exited code=(-?\d+)$/i); - if (exitMatch) return text("youtube_live_error_ffmpeg_exit", "FFmpeg exited (code {code}).", { code: exitMatch[1] }); - if (/^ffmpeg pipe closed:/i.test(raw)) { - return text("youtube_live_error_ffmpeg_pipe", "The FFmpeg input pipe was closed."); + if (/^YouTube RTMPS publish failed:/i.test(raw)) { + return text("youtube_live_error_publish_failed", "YouTube RTMPS publishing failed: {error}", { error: raw.split(":", 2)[1]?.trim() || "-" }); } - if (/^MPEG-TS mux failed:/i.test(raw)) { - return text("youtube_live_error_muxer_failed", "MPEG-TS muxing failed: {error}", { error: raw.split(":", 2)[1]?.trim() || "-" }); + if (/^YouTube RTMPS start failed:/i.test(raw)) { + return text("youtube_live_error_start_failed", "YouTube RTMPS connection failed: {error}", { error: raw.split(":", 2)[1]?.trim() || "-" }); } return raw; } @@ -496,8 +502,9 @@ const lines = [ `[${text("youtube_live_check_ready_section", "Readiness")}]`, `${text("youtube_live_key", "Stream key")}: ${test.configured ? pass : fail}`, - `${text("youtube_live_validation_ffmpeg", "FFmpeg")}: ${test.ffmpeg_available ? pass : fail}`, - `${text("youtube_live_muxer", "PyAV MPEG-TS")}: ${test.muxer_available ? pass : fail}`, + `${text("youtube_live_muxer", "PyAV FLV/AAC")}: ${test.muxer_available ? pass : fail}`, + `${text("youtube_live_validation_transport", "librtmp")}: ${test.transport_available ? pass : fail}`, + `${text("youtube_live_validation_rtmps", "YouTube RTMPS")}: ${test.rtmps_reachable ? pass : fail}`, `${text("youtube_live_metric_source", "Source")}: ${test.source || "-"}`, "", `[${text("youtube_live_check_stream_section", "Stream status")}]`, @@ -519,16 +526,19 @@ lines.push(`[${text("youtube_live_warnings", "Warnings")}]`); warnings.forEach((warning) => lines.push(`- ${warning}`)); } - const stderr = diag.ffmpeg?.stderr_tail || status.stderr_tail || []; + const streamLogs = diag.transport?.log_tail || status.log_tail || []; const errors = uniqueLocalizedMessages([ status.last_error, - ...(Array.isArray(stderr) ? stderr.slice(-8) : []), + ...(Array.isArray(streamLogs) ? streamLogs.slice(-8) : []), + test.transport?.error, + test.muxer?.error, + test.rtmps_reachable ? "" : test.rtmps_message, diagnosticsResult?.ok ? "" : diagnosticsResult?.error, testResult?.ok || test.message === "ready" ? "" : (testResult?.error || test.message), ]); if (errors.length) { lines.push(""); - lines.push(`[${text("youtube_live_check_error_section", "Errors and FFmpeg logs")}]`); + lines.push(`[${text("youtube_live_check_error_section", "Errors and stream logs")}]`); errors.forEach((error) => lines.push(`- ${localizedValidationMessage(error)}`)); } return lines.join("\n"); diff --git a/selfdrive/carrot/web/js/translations/en.js b/selfdrive/carrot/web/js/translations/en.js index 5313e2ce8f4..b7bbdc9e6e2 100644 --- a/selfdrive/carrot/web/js/translations/en.js +++ b/selfdrive/carrot/web/js/translations/en.js @@ -20,7 +20,7 @@ window.CarrotTranslations.register("en", { youtube_live_visibility_studio: "Visibility: set in YouTube Studio", youtube_live_validate_key: "Validate key", youtube_live_validation_format: "Key format", - youtube_live_validation_ffmpeg: "FFmpeg", + youtube_live_validation_transport: "librtmp transport", youtube_live_validation_rtmps: "YouTube RTMPS", youtube_live_validation_note: "YouTube confirms the key only when streaming starts.", youtube_live_validation_pass: "OK", @@ -32,6 +32,7 @@ window.CarrotTranslations.register("en", { youtube_live_validation_bad_chars: "Stream key contains unsupported characters.", youtube_live_validation_format_ok: "Format looks valid.", youtube_live_validation_rtmps_ok: "YouTube RTMPS ingest is reachable.", + youtube_live_validation_rtmps_failed: "Cannot reach YouTube RTMPS: {error}", youtube_live_key: "Stream key", youtube_live_clear_key: "Clear key", youtube_live_clear_confirm: "Clear saved stream key?", @@ -44,7 +45,7 @@ window.CarrotTranslations.register("en", { youtube_live_check_ready_section: "Readiness", youtube_live_check_stream_section: "Stream status", youtube_live_check_resource_section: "Resources", - youtube_live_check_error_section: "Errors and FFmpeg logs", + youtube_live_check_error_section: "Errors and stream logs", youtube_live_resource_cluster: "Cluster HUD", youtube_live_resource_vision: "Carrot Vision", youtube_live_resource_enabled: "Enabled", @@ -52,18 +53,17 @@ window.CarrotTranslations.register("en", { youtube_live_warning_cluster: "Cluster HUD is enabled. Monitor overall load and device temperature during simultaneous use.", youtube_live_warning_vision: "Carrot Vision is enabled. Simultaneous streaming increases network and memory bandwidth use.", youtube_live_warning_quality: "High quality mode is not available yet.", - youtube_live_error_input_invalid: "Could not open the video input because an FFmpeg argument was invalid.", - youtube_live_error_ffmpeg_missing: "FFmpeg is not installed.", + youtube_live_error_transport_missing: "librtmp RTMPS transport is unavailable.", + youtube_live_error_transport_closed: "The YouTube RTMPS connection was closed.", youtube_live_error_no_frames: "No qRoadEncodeData frames were received.", - youtube_live_error_ffmpeg_exit: "FFmpeg exited (code {code}).", - youtube_live_error_ffmpeg_pipe: "The FFmpeg input pipe was closed.", - youtube_live_error_muxer_missing: "PyAV MPEG-TS muxing is unavailable.", - youtube_live_error_muxer_failed: "MPEG-TS muxing failed: {error}", + youtube_live_error_muxer_missing: "PyAV FLV/AAC muxing is unavailable.", + youtube_live_error_publish_failed: "YouTube RTMPS publishing failed: {error}", + youtube_live_error_start_failed: "YouTube RTMPS connection failed: {error}", youtube_live_test: "Test", - youtube_live_test_missing: "Missing stream key or FFmpeg.", - youtube_live_test_muxer_missing: "Stream key, FFmpeg, or PyAV MPEG-TS support is missing.", + youtube_live_test_missing: "Missing stream key or RTMPS transport.", + youtube_live_test_muxer_missing: "Stream key, RTMPS network, librtmp, or PyAV FLV/AAC is unavailable.", youtube_live_test_failed: "Test failed", - youtube_live_muxer: "PyAV MPEG-TS", + youtube_live_muxer: "PyAV FLV/AAC", youtube_live_metric_source: "Source", youtube_live_metric_runtime: "Runtime", youtube_live_metric_bitrate: "Bitrate", @@ -81,9 +81,9 @@ window.CarrotTranslations.register("en", { youtube_live_state_needs_setup: "Stream key required", youtube_live_state_waiting_frame: "Waiting for video frames", youtube_live_state_waiting_keyframe: "Waiting for an H.264 keyframe", - youtube_live_state_starting: "Starting FFmpeg", + youtube_live_state_starting: "Connecting to YouTube RTMPS", youtube_live_state_live: "Streaming to YouTube RTMPS", - youtube_live_state_stopping: "Stopping FFmpeg", + youtube_live_state_stopping: "Closing YouTube RTMPS connection", youtube_live_state_error: "YouTube stream error", home: "Drive", setting: "Setting", diff --git a/selfdrive/carrot/web/js/translations/ko.js b/selfdrive/carrot/web/js/translations/ko.js index a2adca39f2e..a4dd5dd154d 100644 --- a/selfdrive/carrot/web/js/translations/ko.js +++ b/selfdrive/carrot/web/js/translations/ko.js @@ -20,7 +20,7 @@ window.CarrotTranslations.register("ko", { youtube_live_visibility_studio: "공개 범위: YouTube Studio 설정", youtube_live_validate_key: "키 검증", youtube_live_validation_format: "키 형식", - youtube_live_validation_ffmpeg: "FFmpeg", + youtube_live_validation_transport: "librtmp 전송", youtube_live_validation_rtmps: "YouTube RTMPS", youtube_live_validation_note: "YouTube는 송출 시작 시점에만 키 수락 여부를 확정합니다.", youtube_live_validation_pass: "정상", @@ -32,6 +32,7 @@ window.CarrotTranslations.register("ko", { youtube_live_validation_bad_chars: "스트림 키에 지원하지 않는 문자가 있습니다.", youtube_live_validation_format_ok: "형식은 정상입니다.", youtube_live_validation_rtmps_ok: "YouTube RTMPS 서버에 접속 가능합니다.", + youtube_live_validation_rtmps_failed: "YouTube RTMPS 서버에 접속할 수 없습니다: {error}", youtube_live_key: "스트림 키", youtube_live_clear_key: "키 삭제", youtube_live_clear_confirm: "저장된 스트림 키를 삭제할까요?", @@ -44,7 +45,7 @@ window.CarrotTranslations.register("ko", { youtube_live_check_ready_section: "준비 상태", youtube_live_check_stream_section: "송출 상태", youtube_live_check_resource_section: "리소스", - youtube_live_check_error_section: "오류 및 FFmpeg 로그", + youtube_live_check_error_section: "오류 및 송출 로그", youtube_live_resource_cluster: "Cluster HUD", youtube_live_resource_vision: "Carrot Vision", youtube_live_resource_enabled: "사용 중", @@ -52,18 +53,17 @@ window.CarrotTranslations.register("ko", { youtube_live_warning_cluster: "Cluster HUD가 켜져 있습니다. 동시 사용 중 전체 부하와 기기 온도를 확인하세요.", youtube_live_warning_vision: "Carrot Vision이 켜져 있습니다. 동시 송출 시 네트워크와 메모리 대역폭 사용량이 증가합니다.", youtube_live_warning_quality: "고화질 모드는 아직 사용할 수 없습니다.", - youtube_live_error_input_invalid: "FFmpeg 인자가 잘못되어 영상 입력을 열지 못했습니다.", - youtube_live_error_ffmpeg_missing: "FFmpeg가 설치되어 있지 않습니다.", + youtube_live_error_transport_missing: "librtmp RTMPS 전송 기능을 사용할 수 없습니다.", + youtube_live_error_transport_closed: "YouTube RTMPS 연결이 종료되었습니다.", youtube_live_error_no_frames: "qRoadEncodeData 프레임이 수신되지 않습니다.", - youtube_live_error_ffmpeg_exit: "FFmpeg가 종료되었습니다(코드 {code}).", - youtube_live_error_ffmpeg_pipe: "FFmpeg 입력 파이프가 닫혔습니다.", - youtube_live_error_muxer_missing: "PyAV MPEG-TS mux를 사용할 수 없습니다.", - youtube_live_error_muxer_failed: "MPEG-TS mux 실패: {error}", + youtube_live_error_muxer_missing: "PyAV FLV/AAC mux를 사용할 수 없습니다.", + youtube_live_error_publish_failed: "YouTube RTMPS 송출 실패: {error}", + youtube_live_error_start_failed: "YouTube RTMPS 연결 실패: {error}", youtube_live_test: "테스트", - youtube_live_test_missing: "스트림 키 또는 FFmpeg가 없습니다.", - youtube_live_test_muxer_missing: "스트림 키, FFmpeg 또는 PyAV MPEG-TS 지원이 없습니다.", + youtube_live_test_missing: "스트림 키 또는 RTMPS 전송 기능이 없습니다.", + youtube_live_test_muxer_missing: "스트림 키, RTMPS 네트워크, librtmp 또는 PyAV FLV/AAC를 사용할 수 없습니다.", youtube_live_test_failed: "테스트 실패", - youtube_live_muxer: "PyAV MPEG-TS", + youtube_live_muxer: "PyAV FLV/AAC", youtube_live_metric_source: "소스", youtube_live_metric_runtime: "시간", youtube_live_metric_bitrate: "비트레이트", @@ -81,9 +81,9 @@ window.CarrotTranslations.register("ko", { youtube_live_state_needs_setup: "스트림 키 필요", youtube_live_state_waiting_frame: "영상 프레임 대기", youtube_live_state_waiting_keyframe: "H.264 키프레임 대기", - youtube_live_state_starting: "FFmpeg 시작 중", + youtube_live_state_starting: "YouTube RTMPS 연결 중", youtube_live_state_live: "YouTube RTMPS 송출 중", - youtube_live_state_stopping: "FFmpeg 종료 중", + youtube_live_state_stopping: "YouTube RTMPS 연결 종료 중", youtube_live_state_error: "YouTube 송출 오류", home: "주행", setting: "설정", diff --git a/selfdrive/carrot/web/js/translations/zh.js b/selfdrive/carrot/web/js/translations/zh.js index 1e8ff83a2c1..b2514062081 100644 --- a/selfdrive/carrot/web/js/translations/zh.js +++ b/selfdrive/carrot/web/js/translations/zh.js @@ -20,7 +20,7 @@ window.CarrotTranslations.register("zh", { youtube_live_visibility_studio: "Visibility: set in YouTube Studio", youtube_live_validate_key: "Validate key", youtube_live_validation_format: "Key format", - youtube_live_validation_ffmpeg: "FFmpeg", + youtube_live_validation_transport: "librtmp 传输", youtube_live_validation_rtmps: "YouTube RTMPS", youtube_live_validation_note: "YouTube confirms the key only when streaming starts.", youtube_live_validation_pass: "OK", @@ -32,6 +32,7 @@ window.CarrotTranslations.register("zh", { youtube_live_validation_bad_chars: "Stream key contains unsupported characters.", youtube_live_validation_format_ok: "Format looks valid.", youtube_live_validation_rtmps_ok: "YouTube RTMPS ingest is reachable.", + youtube_live_validation_rtmps_failed: "无法连接 YouTube RTMPS:{error}", youtube_live_key: "Stream key", youtube_live_clear_key: "Clear key", youtube_live_clear_confirm: "Clear saved stream key?", @@ -44,7 +45,7 @@ window.CarrotTranslations.register("zh", { youtube_live_check_ready_section: "准备状态", youtube_live_check_stream_section: "推流状态", youtube_live_check_resource_section: "资源", - youtube_live_check_error_section: "错误和 FFmpeg 日志", + youtube_live_check_error_section: "错误和推流日志", youtube_live_resource_cluster: "Cluster HUD", youtube_live_resource_vision: "Carrot Vision", youtube_live_resource_enabled: "已启用", @@ -52,18 +53,17 @@ window.CarrotTranslations.register("zh", { youtube_live_warning_cluster: "Cluster HUD 已启用。同时使用时请注意整体负载和设备温度。", youtube_live_warning_vision: "Carrot Vision 已启用。同时推流会增加网络和内存带宽使用。", youtube_live_warning_quality: "高画质模式暂不可用。", - youtube_live_error_input_invalid: "FFmpeg 参数无效,无法打开视频输入。", - youtube_live_error_ffmpeg_missing: "未安装 FFmpeg。", + youtube_live_error_transport_missing: "无法使用 librtmp RTMPS 传输。", + youtube_live_error_transport_closed: "YouTube RTMPS 连接已关闭。", youtube_live_error_no_frames: "未收到 qRoadEncodeData 帧。", - youtube_live_error_ffmpeg_exit: "FFmpeg 已退出(代码 {code})。", - youtube_live_error_ffmpeg_pipe: "FFmpeg 输入管道已关闭。", - youtube_live_error_muxer_missing: "无法使用 PyAV MPEG-TS 封装。", - youtube_live_error_muxer_failed: "MPEG-TS 封装失败:{error}", + youtube_live_error_muxer_missing: "无法使用 PyAV FLV/AAC 封装。", + youtube_live_error_publish_failed: "YouTube RTMPS 推流失败:{error}", + youtube_live_error_start_failed: "YouTube RTMPS 连接失败:{error}", youtube_live_test: "Test", - youtube_live_test_missing: "Missing stream key or FFmpeg.", - youtube_live_test_muxer_missing: "缺少串流密钥、FFmpeg 或 PyAV MPEG-TS 支持。", + youtube_live_test_missing: "缺少串流密钥或 RTMPS 传输。", + youtube_live_test_muxer_missing: "无法使用串流密钥、RTMPS 网络、librtmp 或 PyAV FLV/AAC。", youtube_live_test_failed: "Test failed", - youtube_live_muxer: "PyAV MPEG-TS", + youtube_live_muxer: "PyAV FLV/AAC", youtube_live_metric_source: "Source", youtube_live_metric_runtime: "Runtime", youtube_live_metric_bitrate: "Bitrate", @@ -81,9 +81,9 @@ window.CarrotTranslations.register("zh", { youtube_live_state_needs_setup: "需要串流密钥", youtube_live_state_waiting_frame: "等待视频帧", youtube_live_state_waiting_keyframe: "等待 H.264 关键帧", - youtube_live_state_starting: "正在启动 FFmpeg", + youtube_live_state_starting: "正在连接 YouTube RTMPS", youtube_live_state_live: "正在推流到 YouTube RTMPS", - youtube_live_state_stopping: "正在停止 FFmpeg", + youtube_live_state_stopping: "正在关闭 YouTube RTMPS 连接", youtube_live_state_error: "YouTube 推流错误", home: "驾驶", setting: "设置", From 5c7b5c6e4736a9f1f359497d63f1ea76b1938e67 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 14:55:50 +0900 Subject: [PATCH 47/61] f --- .../carrot/server/services/youtube_live.py | 7 +- .../server/services/youtube_live_transport.py | 120 +++++++++++++++++- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index dc96a86547f..6e256d61eda 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -173,6 +173,7 @@ def status(self) -> dict[str, Any]: now = _now() uptime = int(now - self._started_at) if self._started_at and running else 0 elapsed = max(1.0, now - self._started_at) if self._started_at and running else 1.0 + session_bytes = max(0, self._bytes_sent - self._session_started_bytes) if running else 0 last_frame_age_ms = int((now - self._last_frame_at) * 1000) if self._last_frame_at else None resource_status = self._resource_status() warnings = self._warnings(resource_status) @@ -200,9 +201,9 @@ def status(self) -> dict[str, Any]: "uptime_sec": uptime, "bytes_sent": self._bytes_sent, "total_mb": round(max(0, self._bytes_sent) / (1024 * 1024), 2), - "session_bytes": max(0, self._bytes_sent - self._session_started_bytes), - "session_mb": round(max(0, self._bytes_sent - self._session_started_bytes) / (1024 * 1024), 2), - "estimated_kbps": int(((self._bytes_sent - self._session_started_bytes) * 8 / 1000) / elapsed) if running else 0, + "session_bytes": session_bytes, + "session_mb": round(session_bytes / (1024 * 1024), 2), + "estimated_kbps": int((session_bytes * 8 / 1000) / elapsed) if running else 0, "restart_count": self._restart_count, "consecutive_failures": self._consecutive_failures, "next_retry_at": self._next_retry_at, diff --git a/selfdrive/carrot/server/services/youtube_live_transport.py b/selfdrive/carrot/server/services/youtube_live_transport.py index ffe50095d61..df40ba5594e 100644 --- a/selfdrive/carrot/server/services/youtube_live_transport.py +++ b/selfdrive/carrot/server/services/youtube_live_transport.py @@ -3,14 +3,19 @@ import ctypes import ctypes.util import io +import select +import socket +import ssl import threading from typing import Any +from urllib.parse import urlsplit, urlunsplit REQUIRED_RTMP_SYMBOLS = ( "RTMP_Alloc", "RTMP_Init", "RTMP_SetupURL", + "RTMP_SetOpt", "RTMP_EnableWrite", "RTMP_Connect", "RTMP_ConnectStream", @@ -25,6 +30,76 @@ class LibrtmpError(RuntimeError): pass +class _AVal(ctypes.Structure): + _fields_ = [("value", ctypes.c_char_p), ("length", ctypes.c_int)] + + +class _TlsTunnel: + def __init__(self, host: str, port: int) -> None: + self._host = host + self._port = port + self._stop_event = threading.Event() + self._listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._listener.bind(("127.0.0.1", 0)) + self._listener.listen(1) + self.local_port = int(self._listener.getsockname()[1]) + self.error = "" + self._local: socket.socket | None = None + self._remote: ssl.SSLSocket | None = None + self._thread = threading.Thread(target=self._run, name="youtube-rtmps-tunnel", daemon=True) + + def start(self) -> None: + self._thread.start() + + def close(self) -> None: + self._stop_event.set() + for sock in (self._local, self._remote, self._listener): + if sock is not None: + try: + sock.shutdown(socket.SHUT_RDWR) + except OSError: + pass + try: + sock.close() + except OSError: + pass + if self._thread.is_alive() and threading.current_thread() is not self._thread: + self._thread.join(timeout=2.0) + + def _run(self) -> None: + raw_remote: socket.socket | None = None + try: + self._listener.settimeout(10.0) + self._local, _ = self._listener.accept() + raw_remote = socket.create_connection((self._host, self._port), timeout=8.0) + self._remote = ssl.create_default_context().wrap_socket(raw_remote, server_hostname=self._host) + raw_remote = None + self._local.settimeout(None) + self._remote.settimeout(None) + sockets = (self._local, self._remote) + while not self._stop_event.is_set(): + readable, _, _ = select.select(sockets, [], [], 0.5) + for source in readable: + payload = source.recv(64 * 1024) + if not payload: + return + target = self._remote if source is self._local else self._local + target.sendall(payload) + except Exception as exc: + if not self._stop_event.is_set(): + self.error = str(exc) + finally: + if raw_remote is not None: + raw_remote.close() + for sock in (self._local, self._remote): + if sock is not None: + try: + sock.close() + except OSError: + pass + + def librtmp_capabilities() -> dict[str, Any]: path = ctypes.util.find_library("rtmp") or "" if not path: @@ -35,6 +110,7 @@ def librtmp_capabilities() -> dict[str, Any]: return { "available": not missing, "path": path, + "rtmps_mode": "python-tls-tunnel", "missing_symbols": missing, "error": f"missing librtmp symbols: {', '.join(missing)}" if missing else "", } @@ -51,6 +127,8 @@ def __init__(self, url: str) -> None: self._configure_library() self._url = url self._url_buffer: ctypes.Array[ctypes.c_char] | None = None + self._tc_url_buffer: ctypes.Array[ctypes.c_char] | None = None + self._tunnel: _TlsTunnel | None = None self._handle: int | None = None self._lock = threading.RLock() self._bytes_written = 0 @@ -70,12 +148,16 @@ def connect(self) -> None: self._handle = handle try: self._library.RTMP_Init(handle) - self._url_buffer = ctypes.create_string_buffer(self._url.encode("utf-8")) + setup_url, tc_url = self._prepare_url() + self._url_buffer = ctypes.create_string_buffer(setup_url.encode("utf-8")) if not self._library.RTMP_SetupURL(handle, self._url_buffer): raise LibrtmpError("librtmp URL setup failed") + if tc_url: + self._set_string_option(handle, "tcUrl", tc_url) self._library.RTMP_EnableWrite(handle) if not self._library.RTMP_Connect(handle, None): - raise LibrtmpError("YouTube RTMPS connection failed") + detail = f": {self._tunnel.error}" if self._tunnel and self._tunnel.error else "" + raise LibrtmpError(f"YouTube RTMPS connection failed{detail}") if not self._library.RTMP_ConnectStream(handle, 0): raise LibrtmpError("YouTube rejected the publish connection") except Exception: @@ -113,12 +195,44 @@ def _close_locked(self) -> None: self._handle = None if handle is None: self._url_buffer = None + self._tc_url_buffer = None + if self._tunnel is not None: + self._tunnel.close() + self._tunnel = None return try: self._library.RTMP_Close(handle) finally: self._library.RTMP_Free(handle) self._url_buffer = None + self._tc_url_buffer = None + if self._tunnel is not None: + self._tunnel.close() + self._tunnel = None + + def _prepare_url(self) -> tuple[str, str]: + parsed = urlsplit(self._url) + if parsed.scheme.lower() != "rtmps": + return self._url, "" + host = parsed.hostname or "" + if not host: + raise LibrtmpError("RTMPS host is missing") + port = parsed.port or 443 + self._tunnel = _TlsTunnel(host, port) + self._tunnel.start() + setup_url = urlunsplit(("rtmp", f"127.0.0.1:{self._tunnel.local_port}", parsed.path, parsed.query, "")) + path_parts = [part for part in parsed.path.split("/") if part] + app_path = f"/{path_parts[0]}" if path_parts else "/" + tc_url = urlunsplit(("rtmps", parsed.netloc, app_path, "", "")) + return setup_url, tc_url + + def _set_string_option(self, handle: int, name: str, value: str) -> None: + name_buffer = ctypes.create_string_buffer(name.encode("utf-8")) + self._tc_url_buffer = ctypes.create_string_buffer(value.encode("utf-8")) + option = _AVal(ctypes.cast(name_buffer, ctypes.c_char_p), len(name_buffer.value)) + argument = _AVal(ctypes.cast(self._tc_url_buffer, ctypes.c_char_p), len(self._tc_url_buffer.value)) + if not self._library.RTMP_SetOpt(handle, ctypes.byref(option), ctypes.byref(argument)): + raise LibrtmpError(f"librtmp option failed: {name}") def _configure_library(self) -> None: missing = [name for name in REQUIRED_RTMP_SYMBOLS if not hasattr(self._library, name)] @@ -129,6 +243,8 @@ def _configure_library(self) -> None: self._library.RTMP_Init.argtypes = [ctypes.c_void_p] self._library.RTMP_SetupURL.argtypes = [ctypes.c_void_p, ctypes.c_char_p] self._library.RTMP_SetupURL.restype = ctypes.c_int + self._library.RTMP_SetOpt.argtypes = [ctypes.c_void_p, ctypes.POINTER(_AVal), ctypes.POINTER(_AVal)] + self._library.RTMP_SetOpt.restype = ctypes.c_int self._library.RTMP_EnableWrite.argtypes = [ctypes.c_void_p] self._library.RTMP_Connect.argtypes = [ctypes.c_void_p, ctypes.c_void_p] self._library.RTMP_Connect.restype = ctypes.c_int From ae8757bf27a82def9a61c138f5919846cdd18740 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 15:04:55 +0900 Subject: [PATCH 48/61] f --- .../server/services/youtube_live_muxer.py | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/selfdrive/carrot/server/services/youtube_live_muxer.py b/selfdrive/carrot/server/services/youtube_live_muxer.py index 9fcee971904..3fb58b16055 100644 --- a/selfdrive/carrot/server/services/youtube_live_muxer.py +++ b/selfdrive/carrot/server/services/youtube_live_muxer.py @@ -13,6 +13,61 @@ AUDIO_SAMPLES = 1_024 +def _annexb_nalus(payload: bytes) -> list[bytes]: + starts: list[tuple[int, int]] = [] + index = 0 + size = len(payload) + while index + 3 <= size: + if index + 4 <= size and payload[index:index + 4] == b"\x00\x00\x00\x01": + starts.append((index, 4)) + index += 4 + elif payload[index:index + 3] == b"\x00\x00\x01": + starts.append((index, 3)) + index += 3 + else: + index += 1 + + nalus = [] + for position, (offset, start_size) in enumerate(starts): + start = offset + start_size + end = starts[position + 1][0] if position + 1 < len(starts) else size + while end > start and payload[end - 1] == 0: + end -= 1 + if end > start: + nalus.append(payload[start:end]) + return nalus + + +def _avc_decoder_configuration(codec_header: bytes) -> bytes: + if codec_header[:1] == b"\x01" and not codec_header.startswith((b"\x00\x00\x01", b"\x00\x00\x00\x01")): + return bytes(codec_header) + nalus = _annexb_nalus(codec_header) + sps_units = [nalu for nalu in nalus if nalu and nalu[0] & 0x1F == 7] + pps_units = [nalu for nalu in nalus if nalu and nalu[0] & 0x1F == 8] + if not sps_units or not pps_units or len(sps_units[0]) < 4: + raise ValueError("H.264 codec header has no SPS/PPS") + if len(sps_units) > 31 or len(pps_units) > 255: + raise ValueError("H.264 codec header has too many parameter sets") + + first_sps = sps_units[0] + result = bytearray((1, first_sps[1], first_sps[2], first_sps[3], 0xFF, 0xE0 | len(sps_units))) + for sps in sps_units: + result.extend(len(sps).to_bytes(2, "big")) + result.extend(sps) + result.append(len(pps_units)) + for pps in pps_units: + result.extend(len(pps).to_bytes(2, "big")) + result.extend(pps) + return bytes(result) + + +def _annexb_to_avcc(payload: bytes) -> bytes: + nalus = _annexb_nalus(payload) + if not nalus: + return bytes(payload) + return b"".join(len(nalu).to_bytes(4, "big") + nalu for nalu in nalus) + + def pyav_capabilities() -> dict[str, Any]: try: import av @@ -66,7 +121,7 @@ def __init__( self._video_stream.width = max(1, int(width)) self._video_stream.height = max(1, int(height)) self._video_stream.time_base = self._video_time_base - self._video_stream.codec_context.extradata = bytes(codec_header) + self._video_stream.codec_context.extradata = _avc_decoder_configuration(codec_header) self._audio_stream = self._container.add_stream(AAC_CODEC, rate=AUDIO_RATE) self._audio_stream.bit_rate = AUDIO_BITRATE @@ -87,7 +142,7 @@ def mux(self, payload: bytes, *, keyframe: bool = False) -> None: target_audio_pts = int(self._packet_index * AUDIO_RATE / self._fps) self._mux_silence_until(target_audio_pts) - packet = self._av.Packet(payload) + packet = self._av.Packet(_annexb_to_avcc(payload)) packet.stream = self._video_stream packet.pts = self._packet_index packet.dts = self._packet_index From 2a927acba56c2b6444c226f943030c44a204b236 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 15:17:25 +0900 Subject: [PATCH 49/61] f --- .../server/services/youtube_live_muxer.py | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/selfdrive/carrot/server/services/youtube_live_muxer.py b/selfdrive/carrot/server/services/youtube_live_muxer.py index 3fb58b16055..a11138f9766 100644 --- a/selfdrive/carrot/server/services/youtube_live_muxer.py +++ b/selfdrive/carrot/server/services/youtube_live_muxer.py @@ -5,7 +5,6 @@ from typing import Any, BinaryIO -FLV_FORMAT = "flv" H264_CODEC = "h264" AAC_CODEC = "aac" AUDIO_RATE = 44_100 @@ -80,12 +79,11 @@ def pyav_capabilities() -> dict[str, Any]: "aac": False, "error": str(exc), } - formats = getattr(av, "formats_available", set()) codecs = getattr(av, "codecs_available", set()) return { "available": True, "version": str(getattr(av, "__version__", "")), - "flv": FLV_FORMAT in formats, + "flv": True, "h264": H264_CODEC in codecs, "aac": AAC_CODEC in codecs, "error": "", @@ -108,29 +106,26 @@ def __init__( import av self._av = av + self._output = output self._fps = max(1, int(fps)) self._video_time_base = Fraction(1, self._fps) self._audio_time_base = Fraction(1, AUDIO_RATE) - self._container = av.open( - output, - mode="w", - format=FLV_FORMAT, - options={"flvflags": "no_duration_filesize", "flush_packets": "1"}, - ) - self._video_stream = self._container.add_stream(H264_CODEC, rate=self._fps) - self._video_stream.width = max(1, int(width)) - self._video_stream.height = max(1, int(height)) - self._video_stream.time_base = self._video_time_base - self._video_stream.codec_context.extradata = _avc_decoder_configuration(codec_header) - - self._audio_stream = self._container.add_stream(AAC_CODEC, rate=AUDIO_RATE) - self._audio_stream.bit_rate = AUDIO_BITRATE - self._audio_stream.layout = "stereo" - self._container.start_encoding() + self._video_config = _avc_decoder_configuration(codec_header) + self._audio_codec = av.CodecContext.create(AAC_CODEC, "w") + self._audio_codec.sample_rate = AUDIO_RATE + self._audio_codec.layout = "stereo" + self._audio_codec.format = "fltp" + self._audio_codec.bit_rate = AUDIO_BITRATE + self._audio_codec.time_base = self._audio_time_base + self._audio_codec.open() self._packet_index = 0 self._audio_pts = 0 self._closed = False self._lock = threading.RLock() + self._output.write(b"FLV\x01\x05\x00\x00\x00\x09\x00\x00\x00\x00") + self._write_tag(9, 0, b"\x17\x00\x00\x00\x00" + self._video_config) + audio_config = bytes(self._audio_codec.extradata or b"\x12\x10") + self._write_tag(8, 0, b"\xAF\x00" + audio_config) def mux(self, payload: bytes, *, keyframe: bool = False) -> None: with self._lock: @@ -142,18 +137,9 @@ def mux(self, payload: bytes, *, keyframe: bool = False) -> None: target_audio_pts = int(self._packet_index * AUDIO_RATE / self._fps) self._mux_silence_until(target_audio_pts) - packet = self._av.Packet(_annexb_to_avcc(payload)) - packet.stream = self._video_stream - packet.pts = self._packet_index - packet.dts = self._packet_index - packet.duration = 1 - packet.time_base = self._video_time_base - if keyframe: - try: - packet.is_keyframe = True - except Exception: - pass - self._container.mux(packet) + timestamp_ms = int(self._packet_index * 1000 / self._fps) + frame_header = b"\x17" if keyframe else b"\x27" + self._write_tag(9, timestamp_ms, frame_header + b"\x01\x00\x00\x00" + _annexb_to_avcc(payload)) self._packet_index += 1 def close(self) -> None: @@ -164,10 +150,10 @@ def close(self) -> None: try: target_audio_pts = int(self._packet_index * AUDIO_RATE / self._fps) self._mux_silence_until(target_audio_pts) - for packet in self._audio_stream.encode(None): - self._container.mux(packet) + for packet in self._audio_codec.encode(None): + self._write_audio_packet(packet) finally: - self._container.close() + self._output.flush() def _mux_silence_until(self, target_pts: int) -> None: while self._audio_pts <= target_pts: @@ -177,6 +163,23 @@ def _mux_silence_until(self, target_pts: int) -> None: frame.time_base = self._audio_time_base for plane in frame.planes: plane.update(bytes(plane.buffer_size)) - for packet in self._audio_stream.encode(frame): - self._container.mux(packet) + for packet in self._audio_codec.encode(frame): + self._write_audio_packet(packet) self._audio_pts += AUDIO_SAMPLES + + def _write_audio_packet(self, packet: Any) -> None: + packet_pts = packet.pts if packet.pts is not None else self._audio_pts + time_base = packet.time_base or self._audio_time_base + timestamp_ms = max(0, int(packet_pts * time_base * 1000)) + self._write_tag(8, timestamp_ms, b"\xAF\x01" + bytes(packet)) + + def _write_tag(self, tag_type: int, timestamp_ms: int, payload: bytes) -> None: + timestamp = max(0, int(timestamp_ms)) & 0xFFFFFFFF + header = ( + bytes((tag_type,)) + + len(payload).to_bytes(3, "big") + + (timestamp & 0xFFFFFF).to_bytes(3, "big") + + bytes(((timestamp >> 24) & 0xFF,)) + + b"\x00\x00\x00" + ) + self._output.write(header + payload + (len(payload) + 11).to_bytes(4, "big")) From 02f5b2115e51c85e8050527017f1a494067496be Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 15:40:12 +0900 Subject: [PATCH 50/61] f --- .../server/services/youtube_live_transport.py | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/selfdrive/carrot/server/services/youtube_live_transport.py b/selfdrive/carrot/server/services/youtube_live_transport.py index df40ba5594e..a52abf49db9 100644 --- a/selfdrive/carrot/server/services/youtube_live_transport.py +++ b/selfdrive/carrot/server/services/youtube_live_transport.py @@ -165,6 +165,10 @@ def connect(self) -> None: raise def write(self, data: bytes | bytearray | memoryview) -> int: + # Returns bytes CONSUMED by librtmp (may be < len). librtmp's RTMP_Write + # parses FLV tags and reads look-ahead beyond the current tag; feeding it a + # small single-tag buffer makes it read out of bounds and segfault, so the + # caller must batch and carry the unconsumed remainder forward. payload = bytes(data) if not payload: return 0 @@ -172,15 +176,12 @@ def write(self, data: bytes | bytearray | memoryview) -> int: handle = self._handle if handle is None or not self._library.RTMP_IsConnected(handle): raise LibrtmpError("YouTube RTMPS connection is closed") - offset = 0 - while offset < len(payload): - buffer = ctypes.create_string_buffer(payload[offset:]) - written = int(self._library.RTMP_Write(handle, buffer, len(payload) - offset)) - if written <= 0: - raise LibrtmpError("YouTube RTMPS write failed") - offset += written - self._bytes_written += offset - return offset + buffer = ctypes.create_string_buffer(payload) + written = int(self._library.RTMP_Write(handle, buffer, len(payload))) + if written < 0: + raise LibrtmpError("YouTube RTMPS write failed") + self._bytes_written += written + return written def is_connected(self) -> bool: with self._lock: @@ -262,10 +263,17 @@ def _configure_library(self) -> None: class RtmpSink(io.RawIOBase): + # Buffer muxer output and hand librtmp large, multi-tag chunks. Feeding + # librtmp one small FLV tag at a time makes RTMP_Write read past the buffer + # (look-ahead) and segfault. We keep whatever librtmp does not consume and + # prepend it to the next batch so it always has trailing context. + FLUSH_THRESHOLD = 16384 + def __init__(self, client: LibrtmpClient) -> None: super().__init__() self._client = client self._position = 0 + self._pending = bytearray() def writable(self) -> bool: return True @@ -274,12 +282,27 @@ def seekable(self) -> bool: return False def write(self, data: bytes | bytearray | memoryview) -> int: - written = self._client.write(data) - self._position += written - return written + chunk = bytes(data) + self._pending.extend(chunk) + self._position += len(chunk) + if len(self._pending) >= self.FLUSH_THRESHOLD: + self._drain(self.FLUSH_THRESHOLD) + return len(chunk) + + def _drain(self, floor: int) -> None: + # Keep at least `floor` bytes buffered so librtmp always has look-ahead. + while len(self._pending) >= floor: + written = self._client.write(bytes(self._pending)) + if written <= 0: + break + del self._pending[:written] def tell(self) -> int: return self._position def flush(self) -> None: - return None + while self._pending: + written = self._client.write(bytes(self._pending)) + if written <= 0: + break + del self._pending[:written] From 7892f21ed6a24b4273f8888da89f89307aa75768 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 15:55:25 +0900 Subject: [PATCH 51/61] f --- .../carrot/server/services/youtube_live.py | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index 6e256d61eda..18aef22ff2f 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -7,6 +7,7 @@ import socket import ssl import time +from collections import deque from pathlib import Path from typing import Any @@ -28,6 +29,8 @@ BACKOFF_BASE_SECONDS = 3.0 BACKOFF_MAX_SECONDS = 60.0 STREAM_STABLE_SECONDS = 10.0 +MIN_RECONNECT_INTERVAL_SECONDS = 3.0 +EVENT_LOG_MAX = 50 _PROCESS_MATCHES = { "carrot_cluster": "selfdrive.carrot.cluster_autorun", @@ -40,6 +43,12 @@ def _now() -> float: return time.time() +def _mono() -> float: + # Monotonic clock for durations/intervals — immune to wall-clock jumps + # (comma syncs system time from GPS/NTP after boot, which can step the clock). + return time.monotonic() + + def _read_json(path: Path) -> dict[str, Any]: try: raw = json.loads(path.read_text(encoding="utf-8")) @@ -139,6 +148,8 @@ def __init__( self._next_retry_at = 0.0 self._last_error = "" self._state = "disabled" + self._events: deque[dict[str, Any]] = deque(maxlen=EVENT_LOG_MAX) + self._last_start_mono = 0.0 self._muxer_capabilities = pyav_capabilities() self._transport_capabilities = librtmp_capabilities() persisted = _read_json(self.state_path) @@ -215,7 +226,7 @@ def status(self) -> dict[str, Any]: "frame_width": self._frame_width, "frame_height": self._frame_height, "frame_fps": self._frame_fps, - "log_tail": [self._last_error] if self._last_error else [], + "log_tail": [f"{event['level']}: {event['message']}" for event in list(self._events)[-12:]], "resource_status": resource_status, "warnings": warnings, } @@ -245,7 +256,7 @@ def test_config(self) -> dict[str, Any]: "quality": PHASE1_QUALITY, "resource_status": status["resource_status"], "warnings": status["warnings"], - "log_tail": [self._last_error] if self._last_error else [], + "log_tail": [f"{event['level']}: {event['message']}" for event in list(self._events)[-12:]], "message": "ready" if ok else "stream key, RTMPS network, librtmp, or PyAV FLV/AAC is unavailable", } @@ -298,7 +309,7 @@ def diagnostics(self) -> dict[str, Any]: "name": "librtmp-rtmps", **self._transport_capabilities, "connected": self._transport_connected, - "log_tail": [self._last_error] if self._last_error else [], + "log_tail": [f"{event['level']}: {event['message']}" for event in list(self._events)[-12:]], }, "muxer": { "name": "pyav-flv-aac", @@ -411,6 +422,11 @@ async def _tick(self) -> None: if not keyframe or not header: self._set_state("waiting_keyframe") return + if self._last_start_mono and (_mono() - self._last_start_mono) < MIN_RECONNECT_INTERVAL_SECONDS: + # Avoid hammering YouTube with rapid reconnects (it rejects them and it + # spins the state machine); hold briefly between start attempts. + self._set_state("backoff") + return try: await self._start_stream(stream_key, codec_header=header, width=width, height=height) except Exception as exc: @@ -436,6 +452,14 @@ def _write_status(self, *, force: bool = False) -> None: except Exception: pass + def _log(self, message: str, level: str = "info") -> None: + # Rolling event history that survives restarts (unlike _last_error, which is + # cleared on each start attempt). Surfaced via status()/diagnostics log_tail. + text = str(message or "").strip() + if not text: + return + self._events.append({"t": round(_now(), 3), "level": level, "message": text}) + def _param_enabled(self) -> bool: try: params = self._get_params() @@ -544,7 +568,9 @@ async def _start_stream(self, stream_key: str, *, codec_header: bytes, width: in await self._stop_stream() self._last_error = "" self._restart_count += 1 + self._last_start_mono = _mono() self._set_state("starting") + self._log("connecting to YouTube RTMPS") rtmp_url = f"{YOUTUBE_RTMPS_BASE}/{stream_key}" transport = LibrtmpClient(rtmp_url) try: @@ -566,6 +592,7 @@ async def _start_stream(self, stream_key: str, *, codec_header: bytes, width: in self._started_at = _now() self._session_started_bytes = self._bytes_sent self._next_retry_at = 0.0 + self._log(f"stream started ({width}x{height})") async def _write_frame(self, payload: bytes, *, keyframe: bool) -> bool: transport = self._transport @@ -620,6 +647,7 @@ def _schedule_backoff(self, reason: str = "") -> None: self._next_retry_at = _now() + delay if reason and not self._last_error: self._last_error = reason + self._log(f"retry in {delay:.0f}s: {reason or self._last_error or 'reconnect'}", "warn") def _process_status(self) -> dict[str, Any]: result = {} From 259c34d87c0a0a0f74b8af0b7319dbb5c2ac771b Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 16:03:36 +0900 Subject: [PATCH 52/61] f --- .../carrot/server/services/youtube_live.py | 68 ++++++++++++++----- .../server/services/youtube_live_muxer.py | 23 +++++-- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/selfdrive/carrot/server/services/youtube_live.py b/selfdrive/carrot/server/services/youtube_live.py index 18aef22ff2f..697aa17cf56 100644 --- a/selfdrive/carrot/server/services/youtube_live.py +++ b/selfdrive/carrot/server/services/youtube_live.py @@ -136,16 +136,21 @@ def __init__( self._params: Any | None = None self._last_status_write = 0.0 self._started_at = 0.0 + self._started_mono = 0.0 self._last_frame_at = 0.0 + self._last_frame_mono = 0.0 self._last_frame_id: int | None = None self._frame_width = 526 self._frame_height = 330 self._frame_fps = 20 + self._frame_ts_ns = 0 + self._stream_t0_ns = 0 + self._last_pts_ms = 0 self._bytes_sent = 0 self._session_started_bytes = 0 self._restart_count = 0 self._consecutive_failures = 0 - self._next_retry_at = 0.0 + self._next_retry_mono = 0.0 self._last_error = "" self._state = "disabled" self._events: deque[dict[str, Any]] = deque(maxlen=EVENT_LOG_MAX) @@ -182,10 +187,12 @@ def status(self) -> dict[str, Any]: stream_key = self.get_stream_key() running = self._transport_connected now = _now() - uptime = int(now - self._started_at) if self._started_at and running else 0 - elapsed = max(1.0, now - self._started_at) if self._started_at and running else 1.0 + mono = _mono() + uptime = int(mono - self._started_mono) if self._started_mono and running else 0 + elapsed = max(1.0, mono - self._started_mono) if self._started_mono and running else 1.0 session_bytes = max(0, self._bytes_sent - self._session_started_bytes) if running else 0 - last_frame_age_ms = int((now - self._last_frame_at) * 1000) if self._last_frame_at else None + last_frame_age_ms = int((mono - self._last_frame_mono) * 1000) if self._last_frame_mono else None + retry_in_sec = max(0, int(self._next_retry_mono - mono)) if self._next_retry_mono else 0 resource_status = self._resource_status() warnings = self._warnings(resource_status) return { @@ -217,8 +224,8 @@ def status(self) -> dict[str, Any]: "estimated_kbps": int((session_bytes * 8 / 1000) / elapsed) if running else 0, "restart_count": self._restart_count, "consecutive_failures": self._consecutive_failures, - "next_retry_at": self._next_retry_at, - "retry_in_sec": max(0, int(self._next_retry_at - now)) if self._next_retry_at else 0, + "next_retry_at": (now + retry_in_sec) if retry_in_sec else 0.0, + "retry_in_sec": retry_in_sec, "last_error": self._last_error, "last_frame_at": self._last_frame_at, "last_frame_age_ms": last_frame_age_ms, @@ -359,7 +366,7 @@ async def _tick(self) -> None: await self._stop_stream() self._last_error = "" self._consecutive_failures = 0 - self._next_retry_at = 0.0 + self._next_retry_mono = 0.0 self._set_state("disabled") return @@ -392,7 +399,7 @@ async def _tick(self) -> None: self._set_state("error") return - if self._next_retry_at and _now() < self._next_retry_at: + if self._next_retry_mono and _mono() < self._next_retry_mono: self._set_state("backoff") return @@ -406,7 +413,7 @@ async def _tick(self) -> None: header, data, frame_id, keyframe, width, height = self._recv_frame() if not data: - if self._transport is not None and self._last_frame_at and _now() - self._last_frame_at > NO_FRAME_STOP_SECONDS: + if self._transport is not None and self._last_frame_mono and _mono() - self._last_frame_mono > NO_FRAME_STOP_SECONDS: self._last_error = "no qRoadEncodeData frames" await self._stop_stream() self._schedule_backoff("frame timeout") @@ -414,6 +421,7 @@ async def _tick(self) -> None: return self._last_frame_at = _now() + self._last_frame_mono = _mono() self._last_frame_id = frame_id self._frame_width = width self._frame_height = height @@ -443,12 +451,12 @@ def _set_state(self, state: str) -> None: self._write_status() def _write_status(self, *, force: bool = False) -> None: - now = _now() - if not force and now - self._last_status_write < STATUS_WRITE_MIN_INTERVAL: + mono = _mono() + if not force and mono - self._last_status_write < STATUS_WRITE_MIN_INTERVAL: return - self._last_status_write = now + self._last_status_write = mono try: - _write_json_atomic(self.state_path, {"updated_at": now, **self.status()}) + _write_json_atomic(self.state_path, {"updated_at": _now(), **self.status()}) except Exception: pass @@ -559,11 +567,32 @@ def _recv_frame(self) -> tuple[bytes, bytes, int | None, bool, int, int]: width = int(getattr(frame, "width", 0) or 526) height = int(getattr(frame, "height", 0) or 330) keyframe = bool(header) or bool(flags & 0x8) + try: + # boottime ns from the encoder; monotonic and immune to wall-clock jumps + self._frame_ts_ns = int(getattr(frame, "timestampEof", 0) or getattr(frame, "timestampSof", 0) or 0) + except Exception: + self._frame_ts_ns = 0 return header, data, frame_id, keyframe, width, height except Exception as exc: self._last_error = f"{PHASE1_SOURCE_SERVICE} recv failed: {exc}" return b"", b"", None, False, 526, 330 + def _frame_pts_ms(self) -> int | None: + # Real presentation time (ms) from the encoder frame timestamp, relative to + # the first frame of this session. Keeps the stream aligned to wall time even + # when frames are dropped; returns None if no timestamp so the muxer falls + # back to frame-index timing. + ts_ns = self._frame_ts_ns + if ts_ns <= 0: + return None + if self._stream_t0_ns <= 0: + self._stream_t0_ns = ts_ns + pts_ms = int((ts_ns - self._stream_t0_ns) / 1_000_000) + if pts_ms < self._last_pts_ms: + pts_ms = self._last_pts_ms + self._last_pts_ms = pts_ms + return pts_ms + async def _start_stream(self, stream_key: str, *, codec_header: bytes, width: int, height: int) -> None: await self._stop_stream() self._last_error = "" @@ -590,8 +619,11 @@ async def _start_stream(self, stream_key: str, *, codec_header: bytes, width: in self._transport_connected = True self._muxer = muxer self._started_at = _now() + self._started_mono = _mono() self._session_started_bytes = self._bytes_sent - self._next_retry_at = 0.0 + self._next_retry_mono = 0.0 + self._stream_t0_ns = 0 + self._last_pts_ms = 0 self._log(f"stream started ({width}x{height})") async def _write_frame(self, payload: bytes, *, keyframe: bool) -> bool: @@ -606,9 +638,9 @@ async def _write_frame(self, payload: bytes, *, keyframe: bool) -> bool: await self._stop_stream() return False try: - await asyncio.to_thread(muxer.mux, payload, keyframe=keyframe) + await asyncio.to_thread(muxer.mux, payload, keyframe=keyframe, timestamp_ms=self._frame_pts_ms()) self._bytes_sent = self._session_started_bytes + transport.bytes_written - if self._started_at and _now() - self._started_at >= STREAM_STABLE_SECONDS: + if self._started_mono and _mono() - self._started_mono >= STREAM_STABLE_SECONDS: self._consecutive_failures = 0 return True except Exception as exc: @@ -626,6 +658,7 @@ async def _stop_stream(self) -> None: self._muxer = None if transport is None and muxer is None: self._started_at = 0.0 + self._started_mono = 0.0 return self._set_state("stopping") if muxer is not None: @@ -640,11 +673,12 @@ async def _stop_stream(self) -> None: except Exception: pass self._started_at = 0.0 + self._started_mono = 0.0 def _schedule_backoff(self, reason: str = "") -> None: self._consecutive_failures += 1 delay = min(BACKOFF_MAX_SECONDS, BACKOFF_BASE_SECONDS * (2 ** max(0, self._consecutive_failures - 1))) - self._next_retry_at = _now() + delay + self._next_retry_mono = _mono() + delay if reason and not self._last_error: self._last_error = reason self._log(f"retry in {delay:.0f}s: {reason or self._last_error or 'reconnect'}", "warn") diff --git a/selfdrive/carrot/server/services/youtube_live_muxer.py b/selfdrive/carrot/server/services/youtube_live_muxer.py index a11138f9766..f79c349b390 100644 --- a/selfdrive/carrot/server/services/youtube_live_muxer.py +++ b/selfdrive/carrot/server/services/youtube_live_muxer.py @@ -120,6 +120,7 @@ def __init__( self._audio_codec.open() self._packet_index = 0 self._audio_pts = 0 + self._last_video_ms = 0 self._closed = False self._lock = threading.RLock() self._output.write(b"FLV\x01\x05\x00\x00\x00\x09\x00\x00\x00\x00") @@ -127,19 +128,28 @@ def __init__( audio_config = bytes(self._audio_codec.extradata or b"\x12\x10") self._write_tag(8, 0, b"\xAF\x00" + audio_config) - def mux(self, payload: bytes, *, keyframe: bool = False) -> None: + def mux(self, payload: bytes, *, keyframe: bool = False, timestamp_ms: int | None = None) -> None: with self._lock: if self._closed: raise RuntimeError("FLV muxer is closed") if not payload: return - target_audio_pts = int(self._packet_index * AUDIO_RATE / self._fps) - self._mux_silence_until(target_audio_pts) + if timestamp_ms is None: + video_ms = int(self._packet_index * 1000 / self._fps) + else: + video_ms = max(0, int(timestamp_ms)) + # FLV/RTMP timestamps must be non-decreasing + if video_ms < self._last_video_ms: + video_ms = self._last_video_ms + self._last_video_ms = video_ms + + # keep the silent audio track filled up to the current video time so a + # dropped-frame gap stays A/V aligned + self._mux_silence_until(int(video_ms * AUDIO_RATE / 1000)) - timestamp_ms = int(self._packet_index * 1000 / self._fps) frame_header = b"\x17" if keyframe else b"\x27" - self._write_tag(9, timestamp_ms, frame_header + b"\x01\x00\x00\x00" + _annexb_to_avcc(payload)) + self._write_tag(9, video_ms, frame_header + b"\x01\x00\x00\x00" + _annexb_to_avcc(payload)) self._packet_index += 1 def close(self) -> None: @@ -148,8 +158,7 @@ def close(self) -> None: return self._closed = True try: - target_audio_pts = int(self._packet_index * AUDIO_RATE / self._fps) - self._mux_silence_until(target_audio_pts) + self._mux_silence_until(int(self._last_video_ms * AUDIO_RATE / 1000)) for packet in self._audio_codec.encode(None): self._write_audio_packet(packet) finally: From ea93601f16a9a63a2ebae698c36128f0eaa8d873 Mon Sep 17 00:00:00 2001 From: jominki354 Date: Wed, 1 Jul 2026 16:07:37 +0900 Subject: [PATCH 53/61] f --- .../web/css/components/nav_status_badges.css | 46 ++++++++++++ selfdrive/carrot/web/index.html | 6 ++ .../web/js/shared/ui/nav_status_badges.js | 70 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 selfdrive/carrot/web/css/components/nav_status_badges.css create mode 100644 selfdrive/carrot/web/js/shared/ui/nav_status_badges.js diff --git a/selfdrive/carrot/web/css/components/nav_status_badges.css b/selfdrive/carrot/web/css/components/nav_status_badges.css new file mode 100644 index 00000000000..5758e1f6f76 --- /dev/null +++ b/selfdrive/carrot/web/css/components/nav_status_badges.css @@ -0,0 +1,46 @@ +.topbar #btnHome .nav-status-badges { + position: absolute; + top: 5px; + right: 6px; + z-index: 1; + display: none; + max-width: calc(100% - 12px); + align-items: center; + justify-content: flex-end; + gap: 2px; + pointer-events: none; +} + +.topbar #btnHome .nav-status-badges.is-visible { + display: flex; +} + +.topbar #btnHome .nav-status-badge { + min-width: 24px; + padding: 2px 4px; + border-radius: 999px; + color: var(--md-on-error, #ffffff); + font-size: 8px; + font-weight: 900; + letter-spacing: 0; + line-height: 1; + text-align: center; + white-space: nowrap; + box-shadow: 0 0 0 1px color-mix(in srgb, var(--md-surface) 72%, transparent); +} + +.topbar #btnHome .nav-status-badge--record { + background: rgba(118, 18, 20, 0.92); + color: #ffd7d3; + animation: nav-rec-badge-blink 1s steps(1, end) infinite; +} + +.topbar #btnHome .nav-status-badge--live { + background: var(--md-error, #ba1a1a); +} + +/* The shared badge container replaces the legacy REC pseudo-element. */ +.topbar #btnHome.recording::before { + display: none; + animation: none; +} diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index f0a6369e39b..c44de9b63fd 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -95,6 +95,7 @@ + @@ -621,6 +622,10 @@

    Home