Merge branch 'upstream/master' into sync-20260517-new-new

This commit is contained in:
Jason Wen
2026-06-02 22:59:13 -04:00
648 changed files with 5113 additions and 90864 deletions
+44 -4
View File
@@ -29,7 +29,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
create_connection)
import cereal.messaging as messaging
from cereal import log
from cereal import car, log
from cereal.services import SERVICE_LIST
from openpilot.common.api import Api, get_key_pair
from openpilot.common.utils import CallbackReader, get_upload_stream
@@ -45,6 +45,7 @@ from openpilot.system.hardware.hw import Paths
ATHENA_HOST = os.getenv('ATHENA_HOST', 'wss://athena.comma.ai')
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
LOCAL_PORT_WHITELIST = {22, } # SSH
WEBRTCD_PORT = 5001
LOG_ATTR_NAME = 'user.upload'
LOG_ATTR_VALUE_MAX_UNIX_TIME = int.to_bytes(2147483647, 4, sys.byteorder)
@@ -163,7 +164,7 @@ class UploadQueueCache:
try:
queue: list[UploadItem | None] = list(upload_queue.queue)
items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)]
Params().put("AthenadUploadQueue", items)
Params().put("AthenadUploadQueue", items, block=True)
except Exception:
cloudlog.exception("athena.UploadQueueCache.cache.exception")
@@ -504,7 +505,7 @@ def setRouteViewed(route: str) -> dict[str, int | str]:
# remove duplicates
routes = list(dict.fromkeys(routes))
params.put("AthenadRecentlyViewedRoutes", ",".join(routes[-10:]))
params.put("AthenadRecentlyViewedRoutes", ",".join(routes[-10:]), block=True)
return {"success": 1}
@@ -567,6 +568,16 @@ def getSshAuthorizedKeys() -> str:
def getGithubUsername() -> str:
return cast(str, Params().get("GithubUsername") or "")
@dispatcher.add_method
def getNotCar() -> bool:
cp_bytes = Params().get("CarParamsPersistent")
if cp_bytes is not None:
with car.CarParams.from_bytes(cp_bytes) as CP:
return CP.notCar
return False
@dispatcher.add_method
def getSimInfo():
return HARDWARE.get_sim_info()
@@ -588,6 +599,35 @@ def getNetworks():
return HARDWARE.get_networks()
@dispatcher.add_method
def startStream(sdp: str) -> dict:
from openpilot.system.webrtc.webrtcd import StreamRequestBody
bridge_services_in = []
# get live car params to avoid stale notCar edge case
cp_bytes = Params().get("CarParams")
if cp_bytes is not None:
with car.CarParams.from_bytes(cp_bytes) as CP:
if CP.notCar:
bridge_services_in.append("testJoystick")
body = StreamRequestBody(sdp, "wideRoad", bridge_services_in, ["carState"])
try:
resp = requests.post(f"http://localhost:{WEBRTCD_PORT}/stream",
json=asdict(body), timeout=10)
if not resp.ok:
try:
error_body = resp.json()
raise Exception(error_body.get("message", f"webrtcd returned {resp.status_code}"))
except ValueError:
resp.raise_for_status()
return resp.json()
except requests.ConnectTimeout as e:
raise Exception("webrtc took too long to respond. is it on?") from e
except requests.ConnectionError as e:
raise Exception("webrtc is not running. turn on comma body ignition.") from e
@dispatcher.add_method
def takeSnapshot() -> str | dict[str, str] | None:
from openpilot.system.camerad.snapshot import jpeg_write, snapshot
@@ -843,7 +883,7 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None:
recv_queue.put_nowait(data)
elif opcode == ABNF.OPCODE_PING:
last_ping = int(time.monotonic() * 1e9)
Params().put("LastAthenaPingTime", last_ping)
Params().put("LastAthenaPingTime", last_ping, block=True)
except WebSocketTimeoutException:
ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping
if ns_since_last_ping > RECONNECT_TIMEOUT_S * 1e9:
+1 -1
View File
@@ -97,7 +97,7 @@ def register(show_spinner=False) -> str | None:
spinner.close()
if dongle_id:
params.put("DongleId", dongle_id)
params.put("DongleId", dongle_id, block=True)
set_offroad_alert("Offroad_UnregisteredHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC)
return dongle_id
+2 -2
View File
@@ -73,8 +73,8 @@ class TestAthenadMethods:
self.params = Params()
for k, v in self.default_params.items():
self.params.put(k, v)
self.params.put_bool("GsmMetered", True)
self.params.put(k, v, block=True)
self.params.put_bool("GsmMetered", True, block=True)
athenad.upload_queue = queue.PriorityQueue()
athenad.cur_upload_items.clear()
+1 -1
View File
@@ -37,7 +37,7 @@ class TestRegistration:
dongle = "DONGLE_ID_123"
m = mocker.patch("openpilot.system.athena.registration.api_get", autospec=True)
for persist, params in [(True, True), (True, False), (False, True)]:
self.params.put("DongleId", dongle if params else "")
self.params.put("DongleId", dongle if params else "", block=True)
with open(self.dongle_id, "w") as f:
f.write(dongle if persist else "")
assert register() == dongle
File diff suppressed because one or more lines are too long
-2
View File
@@ -12,8 +12,6 @@
#include <string>
#include <vector>
#include "media/cam_sensor_cmn_header.h"
#include "common/params.h"
#include "common/swaglog.h"
+2 -2
View File
@@ -1,9 +1,9 @@
#include "cdm.h"
#include "stddef.h"
int write_dmi(uint8_t *dst, uint64_t *addr, uint32_t length, uint32_t dmi_addr, uint8_t sel) {
int write_dmi(uint8_t *dst, uint64_t *addr, uint32_t length, uint32_t dmi_addr, uint8_t sel, uint8_t opcode) {
struct cdm_dmi_cmd *cmd = (struct cdm_dmi_cmd*)dst;
cmd->cmd = CAM_CDM_CMD_DMI_32;
cmd->cmd = opcode;
cmd->length = length - 1;
cmd->reserved = 0;
cmd->addr = 0; // gets patched in
+5 -5
View File
@@ -6,11 +6,6 @@
#include <vector>
#include <memory>
// our helpers
int write_random(uint8_t *dst, const std::vector<uint32_t> &vals);
int write_cont(uint8_t *dst, uint32_t reg, const std::vector<uint32_t> &vals);
int write_dmi(uint8_t *dst, uint64_t *addr, uint32_t length, uint32_t dmi_addr, uint8_t sel);
// from drivers/media/platform/msm/camera/cam_cdm/cam_cdm_util.{c,h}
enum cam_cdm_command {
@@ -32,6 +27,11 @@ enum cam_cdm_command {
CAM_CDM_CMD_PRIVATE_BASE_MAX = 0x7F
};
// our helpers
int write_random(uint8_t *dst, const std::vector<uint32_t> &vals);
int write_cont(uint8_t *dst, uint32_t reg, const std::vector<uint32_t> &vals);
int write_dmi(uint8_t *dst, uint64_t *addr, uint32_t length, uint32_t dmi_addr, uint8_t sel, uint8_t opcode = CAM_CDM_CMD_DMI_32);
/**
* struct cdm_regrandom_cmd - Definition for CDM random register command.
* @count: Number of register writes
+109 -1
View File
@@ -4,7 +4,115 @@
#include <cstdint>
#include <tuple>
#include "third_party/linux/include/msm_media_info.h"
// NV12 subset copied from media/msm_media_info.h.
#ifndef MSM_MEDIA_ALIGN
#define MSM_MEDIA_ALIGN(__sz, __align) (((__align) & ((__align) - 1)) ? \
((((__sz) + (__align) - 1) / (__align)) * (__align)) : \
(((__sz) + (__align) - 1) & (~((__align) - 1))))
#endif
#ifndef MSM_MEDIA_MAX
#define MSM_MEDIA_MAX(__a, __b) ((__a) > (__b) ? (__a) : (__b))
#endif
enum color_fmts {
COLOR_FMT_NV12,
};
static inline unsigned int VENUS_EXTRADATA_SIZE(int width, int height) {
(void)height;
(void)width;
return 16 * 1024;
}
static inline unsigned int VENUS_Y_STRIDE(int color_fmt, int width) {
unsigned int stride = 0;
if (!width) goto invalid_input;
switch (color_fmt) {
case COLOR_FMT_NV12:
stride = MSM_MEDIA_ALIGN(width, 128);
break;
default:
break;
}
invalid_input:
return stride;
}
static inline unsigned int VENUS_UV_STRIDE(int color_fmt, int width) {
unsigned int stride = 0;
if (!width) goto invalid_input;
switch (color_fmt) {
case COLOR_FMT_NV12:
stride = MSM_MEDIA_ALIGN(width, 128);
break;
default:
break;
}
invalid_input:
return stride;
}
static inline unsigned int VENUS_Y_SCANLINES(int color_fmt, int height) {
unsigned int sclines = 0;
if (!height) goto invalid_input;
switch (color_fmt) {
case COLOR_FMT_NV12:
sclines = MSM_MEDIA_ALIGN(height, 32);
break;
default:
break;
}
invalid_input:
return sclines;
}
static inline unsigned int VENUS_UV_SCANLINES(int color_fmt, int height) {
unsigned int sclines = 0;
if (!height) goto invalid_input;
switch (color_fmt) {
case COLOR_FMT_NV12:
sclines = MSM_MEDIA_ALIGN((height + 1) >> 1, 16);
break;
default:
break;
}
invalid_input:
return sclines;
}
static inline unsigned int VENUS_BUFFER_SIZE(int color_fmt, int width, int height) {
const unsigned int extra_size = VENUS_EXTRADATA_SIZE(width, height);
unsigned int size = 0;
unsigned int y_stride = 0, uv_stride = 0, y_sclines = 0, uv_sclines = 0;
if (!width || !height) goto invalid_input;
y_stride = VENUS_Y_STRIDE(color_fmt, width);
uv_stride = VENUS_UV_STRIDE(color_fmt, width);
y_sclines = VENUS_Y_SCANLINES(color_fmt, height);
uv_sclines = VENUS_UV_SCANLINES(color_fmt, height);
switch (color_fmt) {
case COLOR_FMT_NV12: {
const unsigned int y_plane = y_stride * y_sclines;
const unsigned int uv_plane = uv_stride * uv_sclines + 4096;
size = y_plane + uv_plane + MSM_MEDIA_MAX(extra_size, 8 * y_stride);
size = MSM_MEDIA_ALIGN(size, 4096);
size += MSM_MEDIA_ALIGN(width, 512) * 512;
size = MSM_MEDIA_ALIGN(size, 4096);
break;
}
default:
break;
}
invalid_input:
return size;
}
// Returns NV12 aligned (stride, y_height, uv_height, buffer_size) for the given frame dimensions.
inline std::tuple<uint32_t, uint32_t, uint32_t, uint32_t> get_nv12_info(int width, int height) {
+1 -1
View File
@@ -1,5 +1,5 @@
# Python version of system/camerad/cameras/nv12_info.h
# Calculations from third_party/linux/include/msm_media_info.h (VENUS_BUFFER_SIZE)
# Calculations from media/msm_media_info.h (VENUS_BUFFER_SIZE)
def align(val: int, alignment: int) -> int:
return ((val + alignment - 1) // alignment) * alignment
+102 -25
View File
@@ -10,7 +10,6 @@
#include "media/cam_isp.h"
#include "media/cam_icp.h"
#include "media/cam_isp_ife.h"
#include "media/cam_sensor_cmn_header.h"
#include "media/cam_sync.h"
#include "common/util.h"
@@ -466,8 +465,11 @@ void SpectraCamera::config_bps(int idx, int request_id) {
* BPS = Bayer Processing Segment
*/
int size = sizeof(struct cam_packet) + sizeof(struct cam_cmd_buf_desc)*2 + sizeof(struct cam_buf_io_cfg)*2;
size += sizeof(struct cam_patch_desc)*9;
bool needs_downscale = sensor->out_scale > 1;
int num_io_cfgs = needs_downscale ? 3 : 2;
int num_patches = needs_downscale ? 14 : 12;
int size = sizeof(struct cam_packet) + sizeof(struct cam_cmd_buf_desc)*2 + sizeof(struct cam_buf_io_cfg)*num_io_cfgs;
size += sizeof(struct cam_patch_desc)*num_patches;
uint32_t cam_packet_handle = 0;
auto pkt = m->mem_mgr.alloc<struct cam_packet>(size, &cam_packet_handle);
@@ -545,8 +547,15 @@ void SpectraCamera::config_bps(int idx, int request_id) {
int cdm_len = 0;
if (bps_lin_reg.size() == 0) {
// set first knee pt to do BLC
uint32_t new_knee[8];
new_knee[0] = sensor->black_level << (14 - sensor->bits_per_pixel);
for (int i = 0; i < 7; i++) {
uint32_t pts = sensor->linearization_pts[i / 2];
new_knee[i + 1] = (i % 2 == 0) ? (pts >> 16) : (pts & 0xffff);
}
for (int i = 0; i < 4; i++) {
bps_lin_reg.push_back(((sensor->linearization_pts[i] & 0xffff) << 0x10) | (sensor->linearization_pts[i] >> 0x10));
bps_lin_reg.push_back((new_knee[2*i + 1] << 16) | new_knee[2*i]);
}
}
@@ -569,20 +578,24 @@ void SpectraCamera::config_bps(int idx, int request_id) {
0x00000080,
0x00800066,
});
// linearization, EN=0
// linearization
cdm_len += write_cont((unsigned char *)bps_cdm_program_array.ptr + cdm_len, 0x1868, bps_lin_reg);
cdm_len += write_cont((unsigned char *)bps_cdm_program_array.ptr + cdm_len, 0x1878, bps_lin_reg);
cdm_len += write_cont((unsigned char *)bps_cdm_program_array.ptr + cdm_len, 0x1888, bps_lin_reg);
cdm_len += write_cont((unsigned char *)bps_cdm_program_array.ptr + cdm_len, 0x1898, bps_lin_reg);
/*
uint8_t *start = (unsigned char *)bps_cdm_program_array.ptr + cdm_len;
uint64_t addr;
cdm_len += write_dmi((unsigned char *)bps_cdm_program_array.ptr + cdm_len, &addr, sensor->linearization_lut.size()*sizeof(uint32_t), 0x1808, 1);
patches.push_back(addr - (uint64_t)start);
*/
cdm_len += write_dmi((unsigned char *)bps_cdm_program_array.ptr + cdm_len, &addr, sensor->linearization_lut.size()*sizeof(uint32_t), 0x1808, 1, CAM_CDM_CMD_DMI);
patches.push_back(addr - (uint64_t)bps_cdm_program_array.ptr);
// color correction
cdm_len += write_cont((unsigned char *)bps_cdm_program_array.ptr + cdm_len, 0x2e68, bps_ccm_reg);
// gamma
for (uint8_t ch = 1; ch <= 3; ch++) {
cdm_len += write_dmi((unsigned char *)bps_cdm_program_array.ptr + cdm_len, &addr, sensor->gamma_lut_rgb.size()*sizeof(uint32_t), 0x3208, ch, CAM_CDM_CMD_DMI);
patches.push_back(addr - (uint64_t)bps_cdm_program_array.ptr);
}
cdm_len += build_common_ife_bps((unsigned char *)bps_cdm_program_array.ptr + cdm_len, cc, sensor.get(), patches, false);
pa->length = cdm_len - 1;
@@ -596,7 +609,7 @@ void SpectraCamera::config_bps(int idx, int request_id) {
tmp.header = CAM_ICP_CMD_GENERIC_BLOB_CLK;
tmp.header |= (sizeof(cam_icp_clk_bw_request)) << 8;
tmp.clk.budget_ns = 0x1fca058;
tmp.clk.frame_cycles = 2329024; // comes from the striping lib
tmp.clk.frame_cycles = sensor->frame_width * sensor->frame_height; // matches striping lib pixelCount
tmp.clk.rt_flag = 0x0;
tmp.clk.uncompressed_bw = 0x38512180;
tmp.clk.compressed_bw = 0x38512180;
@@ -611,7 +624,7 @@ void SpectraCamera::config_bps(int idx, int request_id) {
}
// *** io config ***
pkt->num_io_configs = 2;
pkt->num_io_configs = num_io_cfgs;
pkt->io_configs_offset = sizeof(struct cam_cmd_buf_desc)*pkt->num_cmd_buf;
struct cam_buf_io_cfg *io_cfg = (struct cam_buf_io_cfg *)((char*)&pkt->payload + pkt->io_configs_offset);
{
@@ -655,28 +668,69 @@ void SpectraCamera::config_bps(int idx, int request_id) {
io_cfg[1].format = CAM_FORMAT_NV12; // TODO: why is this 21 in the dump? should be 12
io_cfg[1].color_space = CAM_COLOR_SPACE_BT601_FULL;
io_cfg[1].resource_type = CAM_ICP_BPS_OUTPUT_IMAGE_FULL;
io_cfg[1].resource_type = needs_downscale ? CAM_ICP_BPS_OUTPUT_IMAGE_REG1 : CAM_ICP_BPS_OUTPUT_IMAGE_FULL;
io_cfg[1].fence = sync_objs_bps[idx];
io_cfg[1].direction = CAM_BUF_OUTPUT;
io_cfg[1].subsample_pattern = 0x1;
io_cfg[1].framedrop_pattern = 0x1;
if (needs_downscale) {
// downscaling needs a full res placeholder
uint32_t full_stride, full_y_h, full_uv_h, full_yuv_size;
std::tie(full_stride, full_y_h, full_uv_h, full_yuv_size) = get_nv12_info(sensor->frame_width, sensor->frame_height);
io_cfg[2].mem_handle[0] = bps_fullres_dummy.handle;
io_cfg[2].mem_handle[1] = bps_fullres_dummy.handle;
io_cfg[2].planes[0] = (struct cam_plane_cfg){
.width = sensor->frame_width,
.height = sensor->frame_height,
.plane_stride = full_stride,
.slice_height = full_y_h,
};
io_cfg[2].planes[1] = (struct cam_plane_cfg){
.width = sensor->frame_width,
.height = sensor->frame_height / 2,
.plane_stride = full_stride,
.slice_height = full_uv_h,
};
io_cfg[2].offsets[1] = ALIGNED_SIZE(full_stride * full_y_h, 0x1000);
io_cfg[2].format = CAM_FORMAT_NV12;
io_cfg[2].color_space = CAM_COLOR_SPACE_BT601_FULL;
io_cfg[2].resource_type = CAM_ICP_BPS_OUTPUT_IMAGE_FULL;
io_cfg[2].fence = sync_objs_bps[idx];
io_cfg[2].direction = CAM_BUF_OUTPUT;
io_cfg[2].subsample_pattern = 0x1;
io_cfg[2].framedrop_pattern = 0x1;
}
}
// *** patches ***
{
assert(patches.size() == 0 | patches.size() == 1);
assert(patches.size() == 0 || patches.size() == 4);
pkt->patch_offset = sizeof(struct cam_cmd_buf_desc)*pkt->num_cmd_buf + sizeof(struct cam_buf_io_cfg)*pkt->num_io_configs;
if (patches.size() > 0) {
add_patch(pkt.get(), bps_cmd.handle, patches[0], bps_linearization_lut.handle, 0);
// linearization LUT
add_patch(pkt.get(), bps_cdm_program_array.handle, patches[0], bps_linearization_lut.handle, 0);
// gamma LUTs
for (int i = 0; i < 3; i++) {
add_patch(pkt.get(), bps_cdm_program_array.handle, patches[i+1], bps_gamma_lut.handle, 0);
}
}
// input frame
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[0].ptr[0]), buf_handle_raw[idx], 0);
// output frame
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[1].ptr[0]), buf_handle_yuv[idx], 0);
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[1].ptr[1]), buf_handle_yuv[idx], io_cfg[1].offsets[1]);
if (needs_downscale) {
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[1].ptr[0]), bps_fullres_dummy.handle, 0);
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[1].ptr[1]), bps_fullres_dummy.handle, io_cfg[2].offsets[1]);
// output frame at REG1
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[7].ptr[0]), buf_handle_yuv[idx], 0);
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[7].ptr[1]), buf_handle_yuv[idx], io_cfg[1].offsets[1]);
} else {
// output frame at FULL
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[1].ptr[0]), buf_handle_yuv[idx], 0);
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, frames[1].ptr[1]), buf_handle_yuv[idx], io_cfg[1].offsets[1]);
}
// rest of buffers
add_patch(pkt.get(), bps_cmd.handle, buf_desc[0].offset + offsetof(bps_tmp, settings_addr), bps_iq.handle, 0);
@@ -987,9 +1041,6 @@ bool SpectraCamera::openSensor() {
LOGD("-- Probing sensor %d", cc.camera_num);
auto init_sensor_lambda = [this](SensorInfo *s) {
if (s->image_sensor == cereal::FrameData::ImageSensor::OS04C10 && cc.output_type == ISP_IFE_PROCESSED) {
((OS04C10*)s)->ife_downscale_configure();
}
sensor.reset(s);
return (sensors_init() == 0);
};
@@ -1167,13 +1218,39 @@ void SpectraCamera::configICP() {
// used internally by the BPS, we just allocate it.
// size comes from the BPSStripingLib
bps_cdm_striping_bl.init(m, 0xa100, 0x20, true, m->icp_device_iommu);
bps_cdm_striping_bl.init(m, 0xcfe0, 0x20, true, m->icp_device_iommu);
if (sensor->out_scale > 1) {
uint32_t full_stride, full_y_h, full_uv_h, full_yuv_size;
std::tie(full_stride, full_y_h, full_uv_h, full_yuv_size) = get_nv12_info(sensor->frame_width, sensor->frame_height);
bps_fullres_dummy.init(m, full_yuv_size, 0x1000, true, m->icp_device_iommu);
}
// LUTs
/*
assert(sensor->linearization_lut.size() == 36);
bps_linearization_lut.init(m, sensor->linearization_lut.size()*sizeof(uint32_t), 0x20, true, m->icp_device_iommu);
memcpy(bps_linearization_lut.ptr, sensor->linearization_lut.data(), bps_linearization_lut.size);
*/
// bit shift linearization_lut to bps specs, also compensate for black level here
uint32_t bl = sensor->black_level << (14 - sensor->bits_per_pixel);
uint32_t* bps_lut = (uint32_t*)bps_linearization_lut.ptr;
for (size_t i = 0; i < sensor->linearization_lut.size(); i++) {
size_t seg = i / 4;
size_t ch = i % 4;
if (seg == 0) {
bps_lut[i] = 0;
continue;
}
uint32_t e = sensor->linearization_lut[(seg - 1) * 4 + ch];
uint32_t base = e & 0x3fff;
uint32_t slope_q11 = (e >> 14) & 0x3fff;
uint32_t slope_q12 = std::min<uint32_t>(slope_q11 << 1, 0x3fff);
base = (base > bl) ? (base - bl) : 0;
bps_lut[i] = base | (slope_q12 << 14);
}
assert(sensor->gamma_lut_rgb.size() == 64);
bps_gamma_lut.init(m, sensor->gamma_lut_rgb.size()*sizeof(uint32_t), 0x20, true, m->icp_device_iommu);
memcpy(bps_gamma_lut.ptr, sensor->gamma_lut_rgb.data(), bps_gamma_lut.size);
}
void SpectraCamera::configCSIPHY() {
+25
View File
@@ -30,6 +30,29 @@ const int MIPI_SETTLE_CNT = 33; // Calculated by camera_freqs.py
#define OpcodesIFEInitialConfig 0x0
#define OpcodesIFEUpdate 0x1
// Sensor command values from the SDM845 kernel's private camera header:
// drivers/media/platform/msm/camera/cam_sensor_module/cam_sensor_utils/cam_sensor_cmn_header.h
// These are userspace-visible ioctl payload values, but Qualcomm did not export them through UAPI.
enum {
CAMERA_SENSOR_CMD_TYPE_PROBE = 1,
CAMERA_SENSOR_CMD_TYPE_PWR_UP = 2,
CAMERA_SENSOR_CMD_TYPE_PWR_DOWN = 3,
CAMERA_SENSOR_CMD_TYPE_I2C_INFO = 4,
CAMERA_SENSOR_CMD_TYPE_I2C_RNDM_WR = 5,
CAMERA_SENSOR_CMD_TYPE_WAIT = 9,
CAMERA_SENSOR_WAIT_OP_SW_UCND = 3,
CAMERA_SENSOR_I2C_TYPE_BYTE = 1,
CAMERA_SENSOR_I2C_TYPE_WORD = 2,
I2C_FAST_MODE = 1,
CAM_SENSOR_PACKET_OPCODE_SENSOR_PROBE = 3,
CAM_SENSOR_PACKET_OPCODE_SENSOR_CONFIG = 4,
CAM_SENSOR_PACKET_OPCODE_SENSOR_NOP = 127,
};
std::optional<int32_t> device_acquire(int fd, int32_t session_handle, void *data, uint32_t num_resources=1);
int device_config(int fd, int32_t session_handle, int32_t dev_handle, uint64_t packet_handle);
int device_control(int fd, int op_code, int session_handle, int dev_handle);
@@ -173,6 +196,8 @@ public:
SpectraBuf bps_iq;
SpectraBuf bps_striping;
SpectraBuf bps_linearization_lut;
SpectraBuf bps_gamma_lut;
SpectraBuf bps_fullres_dummy;
std::vector<uint32_t> bps_lin_reg;
std::vector<uint32_t> bps_ccm_reg;
+7 -18
View File
@@ -1,7 +1,7 @@
#include <cmath>
#include "system/camerad/sensors/sensor.h"
#include "third_party/linux/include/msm_camsensor_sdk.h"
#include <media/msm_camsensor_sdk.h>
namespace {
@@ -21,27 +21,16 @@ const uint32_t os04c10_analog_gains_reg[] = {
} // namespace
void OS04C10::ife_downscale_configure() {
out_scale = 2;
pixel_size_mm = 0.002;
frame_width = 2688;
frame_height = 1520;
exposure_time_max = 2352;
init_reg_array.insert(init_reg_array.end(), std::begin(ife_downscale_override_array_os04c10), std::end(ife_downscale_override_array_os04c10));
}
OS04C10::OS04C10() {
image_sensor = cereal::FrameData::ImageSensor::OS04C10;
bayer_pattern = CAM_ISP_PATTERN_BAYER_BGBGBG;
pixel_size_mm = 0.004;
pixel_size_mm = 0.002;
data_word = false;
// hdr_offset = 64 * 2 + 8; // stagger
frame_width = 1344;
frame_height = 760; //760 * 2 + hdr_offset;
frame_stride = (frame_width * 12 / 8); // no alignment
out_scale = 2;
frame_width = 2688;
frame_height = 1520;
frame_stride = frame_width * 12 / 8;
extra_height = 0;
frame_offset = 0;
@@ -65,7 +54,7 @@ OS04C10::OS04C10() {
dc_gain_on_grey = 0.9;
dc_gain_off_grey = 1.0;
exposure_time_min = 2;
exposure_time_max = 1684;
exposure_time_max = 2352;
analog_gain_min_idx = 0x0;
analog_gain_rec_idx = 0x0; // 1x
analog_gain_max_idx = 0x28;
+17 -37
View File
@@ -4,7 +4,7 @@ const struct i2c_random_wr_payload start_reg_array_os04c10[] = {{0x100, 1}};
const struct i2c_random_wr_payload stop_reg_array_os04c10[] = {{0x100, 0}};
const struct i2c_random_wr_payload init_array_os04c10[] = {
// baseed on DP_2688X1520_NEWSTG_MIPI0776Mbps_30FPS_10BIT_FOURLANE
// OS04C10_AA_00_02_17_wAO_2688x1524_MIPI728Mbps_Linear12bit_20FPS_4Lane_MCLK24MHz
{0x0103, 0x01}, // software reset
// PLL + clocks
@@ -93,7 +93,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x388b, 0x00},
{0x3c80, 0x10},
{0x3c86, 0x00},
{0x3c8c, 0x20},
{0x3c8c, 0x40},
{0x3c9f, 0x01},
{0x3d85, 0x1b},
{0x3d8c, 0x71},
@@ -197,7 +197,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x370b, 0x48},
{0x370c, 0x01},
{0x370f, 0x00},
{0x3714, 0x28},
{0x3714, 0x24},
{0x3716, 0x04},
{0x3719, 0x11},
{0x371a, 0x1e},
@@ -229,7 +229,7 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x37bd, 0x01},
{0x37bf, 0x26},
{0x37c0, 0x11},
{0x37c2, 0x14},
{0x37c2, 0x04},
{0x37cd, 0x19},
{0x37e0, 0x08},
{0x37e6, 0x04},
@@ -239,14 +239,14 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x37d8, 0x02},
{0x37e2, 0x10},
{0x3739, 0x10},
{0x3662, 0x08},
{0x3662, 0x10},
{0x37e4, 0x20},
{0x37e3, 0x08},
{0x37d9, 0x04},
{0x37d9, 0x08},
{0x4040, 0x00},
{0x4041, 0x03},
{0x4008, 0x01},
{0x4009, 0x06},
{0x4041, 0x07},
{0x4008, 0x02},
{0x4009, 0x0d},
// FSIN - frame sync
{0x3002, 0x22},
@@ -267,20 +267,20 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x3802, 0x00}, {0x3803, 0x00},
{0x3804, 0x0a}, {0x3805, 0x8f},
{0x3806, 0x05}, {0x3807, 0xff},
{0x3808, 0x05}, {0x3809, 0x40},
{0x380a, 0x02}, {0x380b, 0xf8},
{0x3808, 0x0a}, {0x3809, 0x80},
{0x380a, 0x05}, {0x380b, 0xf0},
{0x3811, 0x08},
{0x3813, 0x08},
{0x3814, 0x03},
{0x3814, 0x01},
{0x3815, 0x01},
{0x3816, 0x03},
{0x3816, 0x01},
{0x3817, 0x01},
{0x380c, 0x0b}, {0x380d, 0xac}, // HTS (line length)
{0x380e, 0x06}, {0x380f, 0x9c}, // VTS (frame length)
{0x380c, 0x08}, {0x380d, 0x5c}, // HTS (line length)
{0x380e, 0x09}, {0x380f, 0x38}, // VTS (frame length)
{0x3820, 0xb3},
{0x3821, 0x01},
{0x3820, 0xb0},
{0x3821, 0x00},
{0x3880, 0x00},
{0x3882, 0x20},
{0x3c91, 0x0b},
@@ -330,23 +330,3 @@ const struct i2c_random_wr_payload init_array_os04c10[] = {
{0x5104, 0x08}, {0x5105, 0xd6},
{0x5144, 0x08}, {0x5145, 0xd6},
};
const struct i2c_random_wr_payload ife_downscale_override_array_os04c10[] = {
// based on OS04C10_AA_00_02_17_wAO_2688x1524_MIPI728Mbps_Linear12bit_20FPS_4Lane_MCLK24MHz
{0x3c8c, 0x40},
{0x3714, 0x24},
{0x37c2, 0x04},
{0x3662, 0x10},
{0x37d9, 0x08},
{0x4041, 0x07},
{0x4008, 0x02},
{0x4009, 0x0d},
{0x3808, 0x0a}, {0x3809, 0x80},
{0x380a, 0x05}, {0x380b, 0xf0},
{0x3814, 0x01},
{0x3816, 0x01},
{0x380c, 0x08}, {0x380d, 0x5c}, // HTS
{0x380e, 0x09}, {0x380f, 0x38}, // VTS
{0x3820, 0xb0},
{0x3821, 0x00},
};
+1 -1
View File
@@ -1,7 +1,7 @@
#include <cmath>
#include "system/camerad/sensors/sensor.h"
#include "third_party/linux/include/msm_camsensor_sdk.h"
#include <media/msm_camsensor_sdk.h>
namespace {
-1
View File
@@ -98,7 +98,6 @@ public:
class OS04C10 : public SensorInfo {
public:
OS04C10();
void ife_downscale_configure();
std::vector<i2c_random_wr_payload> 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;
+3 -3
View File
@@ -87,7 +87,7 @@ def snapshot():
return None, None
front_camera_allowed = params.get_bool("RecordFront")
params.put_bool("IsTakingSnapshot", True)
params.put_bool("IsTakingSnapshot", True, block=True)
set_offroad_alert("Offroad_IsTakingSnapshot", True)
time.sleep(2.0) # Give hardwared time to read the param, or if just started give camerad time to start
@@ -95,7 +95,7 @@ def snapshot():
try:
subprocess.check_call(["pgrep", "camerad"])
print("Camerad already running")
params.put_bool("IsTakingSnapshot", False)
params.put_bool("IsTakingSnapshot", False, block=True)
params.remove("Offroad_IsTakingSnapshot")
return None, None
except subprocess.CalledProcessError:
@@ -111,7 +111,7 @@ def snapshot():
rear, front = get_snapshots(frame, front_frame)
finally:
managed_processes['camerad'].stop()
params.put_bool("IsTakingSnapshot", False)
params.put_bool("IsTakingSnapshot", False, block=True)
set_offroad_alert("Offroad_IsTakingSnapshot", False)
if not front_camera_allowed:
+1 -1
View File
@@ -8,7 +8,7 @@ echo 0xfffdbfff | sudo tee /sys/module/cam_debug_util/parameters/debug_mdl
#echo 0 | sudo tee /sys/module/cam_debug_util/parameters/debug_mdl
sudo dmesg -C
scons -u -j8 --minimal .
scons -u --minimal .
export DEBUG_FRAMES=1
export DISABLE_ROAD=1 DISABLE_WIDE_ROAD=1
#export DISABLE_DRIVER=1
+7 -8
View File
@@ -20,6 +20,10 @@ class Profile:
enabled: bool
provider: str
@property
def is_comma(self) -> bool:
return self.provider == 'Webbing' and self.iccid.startswith('8985235')
@dataclass
class ThermalZone:
# a zone from /sys/class/thermal/thermal_zone*
@@ -94,8 +98,9 @@ class LPABase(ABC):
def process_notifications(self) -> None:
pass
def is_comma_profile(self, iccid: str) -> bool:
return any(iccid.startswith(prefix) for prefix in ('8985235',))
@abstractmethod
def is_euicc(self) -> bool:
pass
class HardwareBase(ABC):
@staticmethod
@@ -194,12 +199,6 @@ class HardwareBase(ABC):
def initialize_hardware(self):
pass
def configure_modem(self):
pass
def reboot_modem(self):
pass
def get_networks(self):
return None
+54 -19
View File
@@ -2,34 +2,69 @@
import argparse
from openpilot.system.hardware import HARDWARE
from openpilot.system.hardware.base import LPABase, Profile
def sorted_profiles(lpa: LPABase) -> list[Profile]:
return sorted(lpa.list_profiles(), key=lambda p: p.iccid)
def resolve_iccid(lpa: LPABase, ref: str) -> str:
# ref is either a 1-based index into the sorted list, or a literal iccid
if ref.isdigit():
profiles = sorted_profiles(lpa)
idx = int(ref) - 1
if not 0 <= idx < len(profiles):
raise SystemExit(f'no profile at index {ref} (have {len(profiles)})')
return profiles[idx].iccid
return ref
def print_profiles(lpa: LPABase) -> None:
profiles = sorted_profiles(lpa)
print(f'\n{len(profiles)} profile{"s" if len(profiles) != 1 else ""}:')
for i, p in enumerate(profiles, start=1):
print(f'{i}. {p.iccid} (nickname: {p.nickname or "<none provided>"}) (provider: {p.provider}) - {"enabled" if p.enabled else "disabled"}')
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai')
parser.add_argument('--switch', metavar='iccid', help='switch to profile')
parser.add_argument('--delete', metavar='iccid', help='delete profile (warning: this cannot be undone)')
parser.add_argument('--download', nargs=2, metavar=('qr', 'name'), help='download a profile using QR code (format: LPA:1$rsp.truphone.com$QRF-SPEEDTEST)')
parser.add_argument('--nickname', nargs=2, metavar=('iccid', 'name'), help='update the nickname for a profile')
sub = parser.add_subparsers(dest='cmd')
sub.add_parser('list', help='list profiles')
p_switch = sub.add_parser('switch', help='switch to profile')
p_switch.add_argument('profile', help='iccid or 1-based index from `list`')
p_delete = sub.add_parser('delete', help='delete profile (warning: this cannot be undone)')
p_delete.add_argument('profile', help='iccid or 1-based index from `list`')
p_download = sub.add_parser('download', help='download a profile using QR code (format: LPA:1$rsp.truphone.com$QRF-SPEEDTEST)')
p_download.add_argument('qr')
p_download.add_argument('name')
p_nickname = sub.add_parser('nickname', help='update the nickname for a profile')
p_nickname.add_argument('profile', help='iccid or 1-based index from `list`')
p_nickname.add_argument('name')
args = parser.parse_args()
lpa = HARDWARE.get_sim_lpa()
if args.switch:
lpa.switch_profile(args.switch)
elif args.delete:
confirm = input('are you sure you want to delete this profile? (y/N) ')
if args.cmd == 'switch':
lpa.switch_profile(resolve_iccid(lpa, args.profile))
elif args.cmd == 'delete':
iccid = resolve_iccid(lpa, args.profile)
confirm = input(f'are you sure you want to delete profile {iccid}? (y/N) ')
if confirm == 'y':
lpa.delete_profile(args.delete)
lpa.delete_profile(iccid)
else:
print('cancelled')
exit(0)
elif args.download:
lpa.download_profile(args.download[0], args.download[1])
elif args.nickname:
lpa.nickname_profile(args.nickname[0], args.nickname[1])
elif args.cmd == 'download':
lpa.download_profile(args.qr, args.name)
elif args.cmd == 'nickname':
lpa.nickname_profile(resolve_iccid(lpa, args.profile), args.name)
else:
parser.print_help()
profiles = lpa.list_profiles()
print(f'\n{len(profiles)} profile{"s" if len(profiles) > 1 else ""}:')
for p in profiles:
print(f'- {p.iccid} (nickname: {p.nickname or "<none provided>"}) (provider: {p.provider}) - {"enabled" if p.enabled else "disabled"}')
if args.cmd is None:
parser.print_help()
print_profiles(lpa)
+7 -2
View File
@@ -2,6 +2,11 @@
import numpy as np
from openpilot.common.pid import PIDController
from openpilot.system.hardware import HARDWARE
# raise fan setpoint on tici/tizi to reduce noise
# after raising LMH threshold in AGNOS 18.1 to prevent CPU throttling
OFFSET = 0 if HARDWARE.get_device_type() == "mici" else 5
class FanController:
@@ -18,6 +23,6 @@ class FanController:
self.last_ignition = ignition
return int(self.controller.update(
error=(cur_temp - 75), # temperature setpoint in C
feedforward=np.interp(cur_temp, [60.0, 100.0], [0, 100])
error=(cur_temp - (75 + OFFSET)), # temperature setpoint in C
feedforward=np.interp(cur_temp, [60.0 + OFFSET, 100.0 + OFFSET], [0, 100])
))
+26 -40
View File
@@ -40,15 +40,21 @@ HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'ne
# List of thermal bands. We will stay within this region as long as we are within the bounds.
# When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict.
THERMAL_BANDS = OrderedDict({
ThermalStatus.green: ThermalBand(None, 80.0),
ThermalStatus.yellow: ThermalBand(75.0, 96.0),
ThermalStatus.red: ThermalBand(88.0, 107.),
ThermalStatus.danger: ThermalBand(94.0, None),
})
if HARDWARE.get_device_type() == "mici":
THERMAL_BANDS = OrderedDict({
ThermalStatus.ok: ThermalBand(None, 100.0),
ThermalStatus.overheated: ThermalBand(92.0, 107.),
ThermalStatus.critical: ThermalBand(98.0, None),
})
else:
THERMAL_BANDS = OrderedDict({
ThermalStatus.ok: ThermalBand(None, 96.0),
ThermalStatus.overheated: ThermalBand(88.0, 107.),
ThermalStatus.critical: ThermalBand(94.0, None),
})
# Override to highest thermal band when offroad and above this temp
OFFROAD_DANGER_TEMP = 75
OFFROAD_DANGER_TEMP = 85 if HARDWARE.get_device_type() == "mici" else 75
prev_offroad_states: dict[str, tuple[bool, str | None]] = {}
@@ -101,9 +107,6 @@ def hw_state_thread(end_event, hw_queue):
prev_hw_state = None
modem_version = None
modem_configured = False
modem_missing_count = 0
modem_restart_count = 0
while not end_event.is_set():
# these are expensive calls. update every 10s
@@ -121,18 +124,6 @@ def hw_state_thread(end_event, hw_queue):
if modem_version is not None:
cloudlog.event("modem version", version=modem_version)
if AGNOS and modem_restart_count < 3 and HARDWARE.get_modem_version() is None:
# TODO: we may be able to remove this with a MM update
# ModemManager's probing on startup can fail
# rarely, restart the service to probe again.
# Also, AT commands sometimes timeout resulting in ModemManager not
# trying to use this modem anymore.
modem_missing_count += 1
if (modem_missing_count % 4) == 0:
modem_restart_count += 1
cloudlog.event("restarting ModemManager")
os.system("sudo systemctl restart --no-block ModemManager")
tx, rx = HARDWARE.get_modem_data_usage()
hw_state = HardwareState(
@@ -149,11 +140,6 @@ def hw_state_thread(end_event, hw_queue):
except queue.Full:
pass
if not modem_configured and HARDWARE.get_modem_version() is not None:
cloudlog.warning("configuring modem")
HARDWARE.configure_modem()
modem_configured = True
prev_hw_state = hw_state
except Exception:
cloudlog.exception("Error getting hardware state")
@@ -180,7 +166,7 @@ def hardware_thread(end_event, hw_queue) -> None:
started_ts: float | None = None
started_seen = False
startup_blocked_ts: float | None = None
thermal_status = ThermalStatus.yellow
thermal_status = ThermalStatus.ok
last_hw_state = HardwareState(
network_type=NetworkType.none,
@@ -219,7 +205,7 @@ def hardware_thread(end_event, hw_queue) -> None:
# handle requests to cycle system started state
if params.get_bool("OnroadCycleRequested"):
params.put_bool("OnroadCycleRequested", False)
params.put_bool("OnroadCycleRequested", False, block=True)
offroad_cycle_count = sm.frame
onroad_conditions["not_onroad_cycle"] = (sm.frame - offroad_cycle_count) >= ONROAD_CYCLE_TIME * SERVICE_LIST['pandaStates'].frequency
@@ -288,7 +274,7 @@ def hardware_thread(end_event, hw_queue) -> None:
if is_offroad_for_5_min and offroad_comp_temp > OFFROAD_DANGER_TEMP:
# if device is offroad and already hot without the extra onroad load,
# we want to cool down first before increasing load
thermal_status = ThermalStatus.danger
thermal_status = ThermalStatus.critical
else:
current_band = THERMAL_BANDS[thermal_status]
band_idx = list(THERMAL_BANDS.keys()).index(thermal_status)
@@ -312,7 +298,7 @@ def hardware_thread(end_event, hw_queue) -> None:
startup_conditions["not_taking_snapshot"] = not params.get_bool("IsTakingSnapshot")
# must be at an engageable thermal band to go onroad
startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.red
startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.overheated
# ensure device is fully booted
startup_conditions["device_booted"] = startup_conditions.get("device_booted", False) or HARDWARE.booted()
@@ -333,7 +319,7 @@ def hardware_thread(end_event, hw_queue) -> None:
set_offroad_alert("Offroad_TiciSupport", is_unsupported_combo, extra_text=build_metadata.channel)
# if the temperature enters the danger zone, go offroad to cool down
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.critical
extra_text = f"{offroad_comp_temp:.1f}C"
show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"]
set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text)
@@ -347,13 +333,13 @@ def hardware_thread(end_event, hw_queue) -> None:
should_start = should_start and all(startup_conditions.values())
if should_start != should_start_prev or (count == 0):
params.put_bool("IsEngaged", False)
params.put_bool("IsEngaged", False, block=True)
engaged_prev = False
if sm.updated['selfdriveState']:
engaged = sm['selfdriveState'].enabled
if engaged != engaged_prev:
params.put_bool("IsEngaged", engaged)
params.put_bool("IsEngaged", engaged, block=True)
engaged_prev = engaged
try:
@@ -392,7 +378,7 @@ def hardware_thread(end_event, hw_queue) -> None:
# GitHub runner auto off: 9V is used as the threshold because most desktop runners
# will rarely exceed 5V so 9V is set as our buffer between desk use and car use.
params.put_bool_nonblocking("GithubRunnerSufficientVoltage", ((voltage or 0) and voltage > 9000))
params.put_bool("GithubRunnerSufficientVoltage", ((voltage or 0) and voltage > 9000))
power_monitor.calculate(voltage, onroad_conditions["ignition"])
msg.deviceState.offroadPowerUsageUwh = power_monitor.get_power_used()
@@ -408,7 +394,7 @@ def hardware_thread(end_event, hw_queue) -> None:
# Check if we need to shut down
if power_monitor.should_shutdown(onroad_conditions["ignition"], in_car, off_ts, started_seen):
cloudlog.warning(f"shutting device down, offroad since {off_ts}")
params.put_bool("DoShutdown", True)
params.put_bool("DoShutdown", True, block=True)
msg.deviceState.started = started_ts is not None and not offroad_mode
msg.deviceState.startedMonoTime = int(1e9*(started_ts or 0))
@@ -454,11 +440,11 @@ def hardware_thread(end_event, hw_queue) -> None:
# save last one before going onroad
if rising_edge_started:
try:
params.put("LastOffroadStatusPacket", dat)
params.put("LastOffroadStatusPacket", dat, block=True)
except Exception:
cloudlog.exception("failed to save offroad status")
params.put_bool_nonblocking("NetworkMetered", msg.deviceState.networkMetered)
params.put_bool("NetworkMetered", msg.deviceState.networkMetered)
now_ts = time.monotonic()
if off_ts:
@@ -468,8 +454,8 @@ def hardware_thread(end_event, hw_queue) -> None:
last_uptime_ts = now_ts
if (count % int(60. / DT_HW)) == 0:
params.put("UptimeOffroad", uptime_offroad)
params.put("UptimeOnroad", uptime_onroad)
params.put("UptimeOffroad", uptime_offroad, block=True)
params.put("UptimeOnroad", uptime_onroad, block=True)
count += 1
should_start_prev = should_start
+1 -1
View File
@@ -56,7 +56,7 @@ class PowerMonitoring:
self.car_battery_capacity_uWh = max(self.car_battery_capacity_uWh, 0)
self.car_battery_capacity_uWh = min(self.car_battery_capacity_uWh, CAR_BATTERY_CAPACITY_uWh)
if now - self.last_save_time >= 10:
self.params.put_nonblocking("CarBatteryCapacity", int(self.car_battery_capacity_uWh))
self.params.put("CarBatteryCapacity", int(self.car_battery_capacity_uWh))
self.last_save_time = now
# First measurement, set integration time
@@ -139,7 +139,7 @@ class TestPowerMonitoring:
def test_disable_power_down(self, mocker):
POWER_DRAW = 0 # To stop shutting down for other reasons
TEST_TIME = 100
self.params.put_bool("DisablePowerDown", True)
self.params.put_bool("DisablePowerDown", True, block=True)
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
@@ -223,7 +223,7 @@ class TestPowerMonitoring:
def test_max_time_offroad_exceeded(self, max_time_offroad, offroad_time_min, expected_result):
# Set the parameter if provided
if max_time_offroad is not None:
self.params.put("MaxTimeOffroad", max_time_offroad)
self.params.put("MaxTimeOffroad", max_time_offroad, block=True)
# Convert offroad time from minutes to seconds
offroad_time_s = offroad_time_min * 60
+31 -31
View File
@@ -1,83 +1,83 @@
[
{
"name": "xbl",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz",
"hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz",
"hash": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb",
"hash_raw": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb",
"size": 3282256,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e"
"ondevice_hash": "bea7f1a24428c3ededf672fa4fc78baf180cfbd8aafb77c974655b38517283e3"
},
{
"name": "xbl_config",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz",
"hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz",
"hash": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85",
"hash_raw": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85",
"size": 98124,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c"
"ondevice_hash": "fb18cde08a98a168961ecd357e92474823046752b94e112f59fe51a6acd7197d"
},
{
"name": "abl",
"url": "https://commadist.azureedge.net/agnosupdate/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz",
"hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
"hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
"url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz",
"hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c",
"hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c",
"size": 274432,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee"
"ondevice_hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c"
},
{
"name": "aop",
"url": "https://commadist.azureedge.net/agnosupdate/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz",
"hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
"url": "https://commadist.azureedge.net/agnosupdate/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz",
"hash": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1",
"hash_raw": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1",
"size": 184364,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5"
"ondevice_hash": "6c9135446bd3fc075fcee59b887a12e49029ab1f98ed8d6d1e32c73569d47de3"
},
{
"name": "devcfg",
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz",
"hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz",
"hash": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc",
"hash_raw": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc",
"size": 40336,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8"
"ondevice_hash": "2a67971602012c1b43544964709da13c322786b456a8e78568b117e8b1540ce3"
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-d726315cf98a43e1090e5b49297404cf3d084cfbd42ad8bb7d8afb68136b9f51.img.xz",
"hash": "d726315cf98a43e1090e5b49297404cf3d084cfbd42ad8bb7d8afb68136b9f51",
"hash_raw": "d726315cf98a43e1090e5b49297404cf3d084cfbd42ad8bb7d8afb68136b9f51",
"size": 17500160,
"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": "2454108de1161289bc4a75449ad6421f1772b13b3e5cba68a84fca7530557699"
"ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-dcdea6bd675d0276a63c25151727829620794baf42ada2e5e19a3f77b3f583a5.img.xz",
"hash": "5f319030ad05942267b77f1a4686c4ca24cc09b2c2a4688e57342ffc9720fd49",
"hash_raw": "dcdea6bd675d0276a63c25151727829620794baf42ada2e5e19a3f77b3f583a5",
"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": "c12f1b7d790a418aea17424accf4cd59c575e5745cad82bdc9452f384483648c",
"ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342",
"alt": {
"hash": "dcdea6bd675d0276a63c25151727829620794baf42ada2e5e19a3f77b3f583a5",
"url": "https://commadist.azureedge.net/agnosupdate/system-dcdea6bd675d0276a63c25151727829620794baf42ada2e5e19a3f77b3f583a5.img",
"hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img",
"size": 4718592000
}
}
+39 -39
View File
@@ -130,47 +130,47 @@
},
{
"name": "xbl",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz",
"hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz",
"hash": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb",
"hash_raw": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb",
"size": 3282256,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e"
"ondevice_hash": "bea7f1a24428c3ededf672fa4fc78baf180cfbd8aafb77c974655b38517283e3"
},
{
"name": "xbl_config",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz",
"hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz",
"hash": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85",
"hash_raw": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85",
"size": 98124,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c"
"ondevice_hash": "fb18cde08a98a168961ecd357e92474823046752b94e112f59fe51a6acd7197d"
},
{
"name": "abl",
"url": "https://commadist.azureedge.net/agnosupdate/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz",
"hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
"hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
"url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz",
"hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c",
"hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c",
"size": 274432,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee"
"ondevice_hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c"
},
{
"name": "aop",
"url": "https://commadist.azureedge.net/agnosupdate/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz",
"hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
"url": "https://commadist.azureedge.net/agnosupdate/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz",
"hash": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1",
"hash_raw": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1",
"size": 184364,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5"
"ondevice_hash": "6c9135446bd3fc075fcee59b887a12e49029ab1f98ed8d6d1e32c73569d47de3"
},
{
"name": "bluetooth",
@@ -207,14 +207,14 @@
},
{
"name": "devcfg",
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz",
"hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz",
"hash": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc",
"hash_raw": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc",
"size": 40336,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8"
"ondevice_hash": "2a67971602012c1b43544964709da13c322786b456a8e78568b117e8b1540ce3"
},
{
"name": "devinfo",
@@ -339,51 +339,51 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-d726315cf98a43e1090e5b49297404cf3d084cfbd42ad8bb7d8afb68136b9f51.img.xz",
"hash": "d726315cf98a43e1090e5b49297404cf3d084cfbd42ad8bb7d8afb68136b9f51",
"hash_raw": "d726315cf98a43e1090e5b49297404cf3d084cfbd42ad8bb7d8afb68136b9f51",
"size": 17500160,
"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": "2454108de1161289bc4a75449ad6421f1772b13b3e5cba68a84fca7530557699"
"ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-dcdea6bd675d0276a63c25151727829620794baf42ada2e5e19a3f77b3f583a5.img.xz",
"hash": "5f319030ad05942267b77f1a4686c4ca24cc09b2c2a4688e57342ffc9720fd49",
"hash_raw": "dcdea6bd675d0276a63c25151727829620794baf42ada2e5e19a3f77b3f583a5",
"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": "c12f1b7d790a418aea17424accf4cd59c575e5745cad82bdc9452f384483648c",
"ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342",
"alt": {
"hash": "dcdea6bd675d0276a63c25151727829620794baf42ada2e5e19a3f77b3f583a5",
"url": "https://commadist.azureedge.net/agnosupdate/system-dcdea6bd675d0276a63c25151727829620794baf42ada2e5e19a3f77b3f583a5.img",
"hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img",
"size": 4718592000
}
},
{
"name": "userdata_90",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-a7b25ea29255f4fd3a2da99e037f40b4ca10bd4afd57dd96563353b8dfb0f634.img.xz",
"hash": "7ea9d7d4685ec36bbfdf06afe0b51650d567416c3092fef96bd97158ed322742",
"hash_raw": "a7b25ea29255f4fd3a2da99e037f40b4ca10bd4afd57dd96563353b8dfb0f634",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-14a3fc6e9bd148b9deebf6ae9df2f1b3b759e629b337e41b6895864cdd51f630.img.xz",
"hash": "52160dd01b30b3dc572226e8d549a034b03bc328e80f1f4cd6a857b6dd447687",
"hash_raw": "14a3fc6e9bd148b9deebf6ae9df2f1b3b759e629b337e41b6895864cdd51f630",
"size": 96636764160,
"sparse": true,
"full_check": true,
"has_ab": false,
"ondevice_hash": "79ed653c1679d84b13ee23083a511b0e668454e4af9b0db99a3279072ed041c1"
"ondevice_hash": "3bbc052c7793087946b0cd668c1778f930084f6b00896aeebd193dfce53fa518"
},
{
"name": "userdata_89",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-8e428632c967aa609cac184bff938a90240e53ffd3b4fca40bc94c33c81202ba.img.xz",
"hash": "7104cdb0384e4ecb1ebfa6136a2330251bc8aa829b9ec48c4b740f656252d382",
"hash_raw": "8e428632c967aa609cac184bff938a90240e53ffd3b4fca40bc94c33c81202ba",
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-425c69d021f4ee2f767963bf7b991d1a492485fa465c5f5d001e1cf7de3d62a0.img.xz",
"hash": "02a8c5512754d7781d930d242be3fad01fbce652297c69dfe8722dc92b18dc09",
"hash_raw": "425c69d021f4ee2f767963bf7b991d1a492485fa465c5f5d001e1cf7de3d62a0",
"size": 95563022336,
"sparse": true,
"full_check": true,
"has_ab": false,
"ondevice_hash": "fbede3b0831dbc4a4edd336e5f547f4978902b9421fb1484e86c416192c59165"
"ondevice_hash": "3667d500f91b08a4671d7c09623eb6ae8fc905d9273893b50689e214005c7fa6"
}
]
-30
View File
@@ -1,30 +0,0 @@
[connection]
id=esim
uuid=fff6553c-3284-4707-a6b1-acc021caaafb
type=gsm
permissions=
autoconnect=true
autoconnect-retries=100
autoconnect-priority=2
metered=1
[gsm]
apn=
home-only=false
auto-config=true
sim-id=
[ipv4]
route-metric=1000
dns-priority=1000
dns-search=
method=auto
[ipv6]
ddr-gen-mode=stable-privacy
dns-search=
route-metric=1000
dns-priority=1000
method=auto
[proxy]
+6 -3
View File
@@ -13,8 +13,11 @@
class HardwareTici : public HardwareNone {
public:
static std::string get_name() {
std::string model = util::read_file("/sys/firmware/devicetree/base/model");
return util::strip(model.substr(std::string("comma ").size()));
static const std::string name = []() {
std::string model = util::read_file("/sys/firmware/devicetree/base/model");
return util::strip(model.substr(std::string("comma ").size()));
}();
return name;
}
static cereal::InitData::DeviceType get_device_type() {
@@ -23,7 +26,7 @@ public:
{"tizi", cereal::InitData::DeviceType::TIZI},
{"mici", cereal::InitData::DeviceType::MICI}
};
auto it = device_map.find(get_name());
static const auto it = device_map.find(get_name());
assert(it != device_map.end());
return it->second;
}
+109 -256
View File
@@ -1,9 +1,9 @@
import math
import configparser
import json
import os
import socket
import subprocess
import time
import tempfile
from enum import IntEnum
from functools import cached_property, lru_cache
from pathlib import Path
@@ -16,51 +16,11 @@ from openpilot.system.hardware.tici.lpa import TiciLPA
from openpilot.system.hardware.tici.pins import GPIO
from openpilot.system.hardware.tici.amplifier import Amplifier
NM = 'org.freedesktop.NetworkManager'
NM_CON_ACT = NM + '.Connection.Active'
NM_DEV = NM + '.Device'
NM_DEV_WL = NM + '.Device.Wireless'
NM_DEV_STATS = NM + '.Device.Statistics'
NM_AP = NM + '.AccessPoint'
DBUS_PROPS = 'org.freedesktop.DBus.Properties'
MM = 'org.freedesktop.ModemManager1'
MM_MODEM = MM + ".Modem"
MM_MODEM_SIMPLE = MM + ".Modem.Simple"
MM_SIM = MM + ".Sim"
class MM_MODEM_STATE(IntEnum):
FAILED = -1
UNKNOWN = 0
INITIALIZING = 1
LOCKED = 2
DISABLED = 3
DISABLING = 4
ENABLING = 5
ENABLED = 6
SEARCHING = 7
REGISTERED = 8
DISCONNECTING = 9
CONNECTING = 10
CONNECTED = 11
class NMMetered(IntEnum):
NM_METERED_UNKNOWN = 0
NM_METERED_YES = 1
NM_METERED_NO = 2
NM_METERED_GUESS_YES = 3
NM_METERED_GUESS_NO = 4
TIMEOUT = 0.1
REFRESH_RATE_MS = 1000
MODEM_STATE_PATH = "/dev/shm/modem"
NetworkType = log.DeviceState.NetworkType
NetworkStrength = log.DeviceState.NetworkStrength
# https://developer.gnome.org/ModemManager/unstable/ModemManager-Flags-and-Enumerations.html#MMModemAccessTechnology
MM_MODEM_ACCESS_TECHNOLOGY_UMTS = 1 << 5
MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14
def affine_irq(val, action):
irqs = get_irqs_for_action(action)
@@ -78,26 +38,40 @@ def get_device_type():
model = f.read().strip('\x00')
return model.split('comma ')[-1]
def wpa_supplicant_cmd(cmd: str, timeout: float = 0.2) -> dict[str, str]:
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
sock.settimeout(timeout)
sock.bind(f"\0openpilot-wpa-{os.getpid()}-{time.monotonic_ns()}")
sock.connect("/run/wpa_supplicant/wlan0")
sock.send(cmd.encode())
while True:
out = sock.recv(8192).decode("utf-8", "replace")
if out.startswith("<"):
continue
if out.startswith("FAIL"):
return {}
return dict(l.split("=", 1) for l in out.splitlines() if "=" in l)
def get_default_route_iface():
with open("/proc/net/route") as f:
routes = [(int(route[6]), route[0]) for line in f.readlines()[1:] if (route := line.split())[1] == "00000000" and int(route[3], 16) & 0x1]
return min(routes)[1] if routes else None
class Tici(HardwareBase):
@cached_property
def bus(self):
import dbus
return dbus.SystemBus()
@cached_property
def nm(self):
return self.bus.get_object(NM, '/org/freedesktop/NetworkManager')
@property # this should not be cached, in case the modemmanager restarts
def mm(self):
return self.bus.get_object(MM, '/org/freedesktop/ModemManager1')
@cached_property
def amplifier(self):
if self.get_device_type() == "mici":
return None
return Amplifier()
def get_modem_state(self) -> dict:
try:
with open(MODEM_STATE_PATH) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def get_os_version(self):
with open("/VERSION") as f:
return f.read().strip()
@@ -138,67 +112,37 @@ class Tici(HardwareBase):
def get_network_type(self):
try:
primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
primary_connection = self.bus.get_object(NM, primary_connection)
primary_type = primary_connection.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if primary_type == '802-3-ethernet':
return NetworkType.ethernet
elif primary_type == '802-11-wireless':
return NetworkType.wifi
else:
active_connections = self.nm.Get(NM, 'ActiveConnections', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
for conn in active_connections:
c = self.bus.get_object(NM, conn)
tp = c.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if tp == 'gsm':
modem = self.get_modem()
access_t = modem.Get(MM_MODEM, 'AccessTechnologies', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if access_t >= MM_MODEM_ACCESS_TECHNOLOGY_LTE:
return NetworkType.cell4G
elif access_t >= MM_MODEM_ACCESS_TECHNOLOGY_UMTS:
return NetworkType.cell3G
else:
return NetworkType.cell2G
if (iface := get_default_route_iface()):
if iface.startswith('wlan'):
return NetworkType.wifi
if iface.startswith('eth'):
return NetworkType.ethernet
except Exception:
pass
ms = self.get_modem_state()
if ms.get('connected'):
nt = ms.get('network_type', '')
if nt == 'nr':
return NetworkType.cell5G
elif nt == 'lte':
return NetworkType.cell4G
elif nt in ('utran', 'umts'):
return NetworkType.cell3G
elif nt == 'gsm':
return NetworkType.cell2G
return NetworkType.none
def get_modem(self):
objects = self.mm.GetManagedObjects(dbus_interface="org.freedesktop.DBus.ObjectManager", timeout=TIMEOUT)
modem_path = list(objects.keys())[0]
return self.bus.get_object(MM, modem_path)
def get_wlan(self):
wlan_path = self.nm.GetDeviceByIpIface('wlan0', dbus_interface=NM, timeout=TIMEOUT)
return self.bus.get_object(NM, wlan_path)
def get_wwan(self):
wwan_path = self.nm.GetDeviceByIpIface('wwan0', dbus_interface=NM, timeout=TIMEOUT)
return self.bus.get_object(NM, wwan_path)
def get_sim_info(self):
modem = self.get_modem()
sim_path = modem.Get(MM_MODEM, 'Sim', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if sim_path == "/":
return {
'sim_id': '',
'mcc_mnc': None,
'network_type': ["Unknown"],
'sim_state': ["ABSENT"],
'data_connected': False
}
else:
sim = self.bus.get_object(MM, sim_path)
return {
'sim_id': str(sim.Get(MM_SIM, 'SimIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)),
'mcc_mnc': str(sim.Get(MM_SIM, 'OperatorIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)),
'network_type': ["Unknown"],
'sim_state': ["READY"],
'data_connected': modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) == MM_MODEM_STATE.CONNECTED,
}
ms = self.get_modem_state()
sim_id = ms.get('iccid', '')
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"],
'data_connected': ms.get('connected', False),
}
def get_sim_lpa(self) -> LPABase:
return TiciLPA()
@@ -206,40 +150,21 @@ class Tici(HardwareBase):
def get_imei(self, slot):
if slot != 0:
return ""
return str(self.get_modem().Get(MM_MODEM, 'EquipmentIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT))
return self.get_modem_state().get('imei', '')
def get_network_info(self):
if self.get_device_type() == "mici":
return None
try:
modem = self.get_modem()
info = modem.Command("AT+QNWINFO", math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
extra = modem.Command('AT+QENG="servingcell"', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
except Exception:
return None
if info and info.startswith('+QNWINFO: '):
info = info.replace('+QNWINFO: ', '').replace('"', '').split(',')
extra = "" if extra is None else extra.replace('+QENG: "servingcell",', '').replace('"', '')
state = "" if state is None else MM_MODEM_STATE(state).name
if len(info) != 4:
return None
technology, operator, band, channel = info
return({
'technology': technology,
'operator': operator,
'band': band,
'channel': int(channel),
'extra': extra,
'state': state,
})
else:
return None
ms = self.get_modem_state()
return {
'technology': ms.get('network_type', '').upper() if ms.get('network_type') else '',
'operator': ms.get('operator', ''),
'band': ms.get('band', ''),
'channel': ms.get('channel', 0),
'extra': ms.get('extra', ''),
'state': ms.get('state', 'UNKNOWN'),
}
def parse_strength(self, percentage):
if percentage < 25:
@@ -257,59 +182,62 @@ class Tici(HardwareBase):
try:
if network_type == NetworkType.none:
pass
elif network_type == NetworkType.ethernet:
network_strength = NetworkStrength.great
elif network_type == NetworkType.wifi:
wlan = self.get_wlan()
active_ap_path = wlan.Get(NM_DEV_WL, 'ActiveAccessPoint', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if active_ap_path != "/":
active_ap = self.bus.get_object(NM, active_ap_path)
strength = int(active_ap.Get(NM_AP, 'Strength', dbus_interface=DBUS_PROPS, timeout=TIMEOUT))
network_strength = self.parse_strength(strength)
rssi = wpa_supplicant_cmd("SIGNAL_POLL").get("RSSI")
if rssi is not None:
dbm = int(rssi)
if -100 < dbm <= 0:
network_strength = self.parse_strength(120 + max(-100, min(-20, dbm)))
else: # Cellular
modem = self.get_modem()
strength = int(modem.Get(MM_MODEM, 'SignalQuality', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)[0])
network_strength = self.parse_strength(strength)
network_strength = self.parse_strength(self.get_modem_state().get('signal_quality', 0))
except Exception:
pass
return network_strength
def get_network_metered(self, network_type) -> bool:
if network_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G):
from openpilot.common.params import Params
return Params().get_bool("GsmMetered")
try:
primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
primary_connection = self.bus.get_object(NM, primary_connection)
primary_devices = primary_connection.Get(NM_CON_ACT, 'Devices', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if network_type == NetworkType.wifi:
ssid = wpa_supplicant_cmd("STATUS").get("ssid", "")
if ssid:
# wpa_supplicant escapes non-printable bytes as \xNN; NM keyfile stores ASCII SSIDs as a literal and others as a byte;byte; list
ssid_bytes = ssid.encode().decode('unicode_escape').encode('latin-1')
ssid_keyfile_list = ';'.join(str(b) for b in ssid_bytes) + ';'
for dev in primary_devices:
dev_obj = self.bus.get_object(NM, str(dev))
metered_prop = dev_obj.Get(NM_DEV, 'Metered', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if network_type == NetworkType.wifi:
if metered_prop in [NMMetered.NM_METERED_YES, NMMetered.NM_METERED_GUESS_YES]:
return True
elif network_type in [NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G]:
if metered_prop == NMMetered.NM_METERED_NO:
return False
nm_dirs = ("/run/NetworkManager/system-connections", "/data/etc/NetworkManager/system-connections")
for fpath in (p for d in nm_dirs for p in Path(d).glob("*.nmconnection")):
raw = sudo_read(str(fpath))
if not raw:
continue
cp = configparser.ConfigParser(interpolation=None)
try:
cp.read_string(raw)
keyfile_ssid = cp.get("wifi", "ssid", fallback="")
if keyfile_ssid != ssid and keyfile_ssid != ssid_keyfile_list:
continue
metered = cp.getint("connection", "metered", fallback=0)
except (configparser.Error, ValueError):
continue
if metered == 1: # NM_METERED_YES
return True
if metered == 2: # NM_METERED_NO
return False
break
except Exception:
pass
return super().get_network_metered(network_type)
def get_modem_version(self):
try:
modem = self.get_modem()
return modem.Get(MM_MODEM, 'Revision', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
except Exception:
return None
return self.get_modem_state().get('modem_version') or None
def get_modem_temperatures(self):
timeout = 0.2 # Default timeout is too short
try:
modem = self.get_modem()
temps = modem.Command("AT+QTEMP", math.ceil(timeout), dbus_interface=MM_MODEM, timeout=timeout)
return list(filter(lambda t: t != 255, map(int, temps.split(' ')[1].split(','))))
except Exception:
return []
return self.get_modem_state().get('temperatures', [])
def get_current_power_draw(self):
return (self.read_param_file("/sys/class/hwmon/hwmon1/power1_input", int) / 1e6)
@@ -455,68 +383,6 @@ class Tici(HardwareBase):
except subprocess.CalledProcessException as e:
print(str(e))
def configure_modem(self):
sim_id = self.get_sim_info().get('sim_id', '')
cmds = []
modem = self.get_modem()
# Quectel EG25
if self.get_device_type() in ("tizi", ):
# clear out old blue prime initial APN
os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn="')
cmds += [
# SIM hot swap
'AT+QSIMDET=1,0',
'AT+QSIMSTAT=1',
# configure modem as data-centric
'AT+QNVW=5280,0,"0102000000000000"',
'AT+QNVFW="/nv/item_files/ims/IMS_enable",00',
'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01',
]
# Quectel EG916
else:
# this modem gets upset with too many AT commands
if sim_id is None or len(sim_id) == 0:
cmds += [
# SIM sleep disable
'AT$QCSIMSLEEP=0',
'AT$QCSIMCFG=SimPowerSave,0',
# ethernet config
'AT$QCPCFG=usbNet,1',
]
for cmd in cmds:
try:
modem.Command(cmd, math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
except Exception:
pass
# eSIM prime
dest = "/etc/NetworkManager/system-connections/esim.nmconnection"
if self.get_sim_lpa().is_comma_profile(sim_id) and not os.path.exists(dest):
with open(Path(__file__).parent/'esim.nmconnection') as f, tempfile.NamedTemporaryFile(mode='w') as tf:
dat = f.read()
dat = dat.replace("sim-id=", f"sim-id={sim_id}")
tf.write(dat)
tf.flush()
# needs to be root
os.system(f"sudo cp {tf.name} {dest}")
os.system(f"sudo nmcli con load {dest}")
def reboot_modem(self):
modem = self.get_modem()
for state in (0, 1):
try:
modem.Command(f'AT+CFUN={state}', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
except Exception:
pass
def get_networks(self):
r = {}
@@ -545,20 +411,8 @@ class Tici(HardwareBase):
return r
def get_modem_data_usage(self):
try:
wwan = self.get_wwan()
# Ensure refresh rate is set so values don't go stale
refresh_rate = wwan.Get(NM_DEV_STATS, 'RefreshRateMs', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if refresh_rate != REFRESH_RATE_MS:
u = type(refresh_rate)
wwan.Set(NM_DEV_STATS, 'RefreshRateMs', u(REFRESH_RATE_MS), dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
tx = wwan.Get(NM_DEV_STATS, 'TxBytes', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
rx = wwan.Get(NM_DEV_STATS, 'RxBytes', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
return int(tx), int(rx)
except Exception:
return -1, -1
ms = self.get_modem_state()
return ms.get('tx_bytes', -1), ms.get('rx_bytes', -1)
def has_internal_panda(self):
return True
@@ -569,7 +423,7 @@ class Tici(HardwareBase):
gpio_set(GPIO.STM_RST_N, 1)
gpio_set(GPIO.STM_BOOT0, 0)
time.sleep(1)
time.sleep(0.01)
gpio_set(GPIO.STM_RST_N, 0)
def recover_internal_panda(self):
@@ -578,9 +432,9 @@ class Tici(HardwareBase):
gpio_set(GPIO.STM_RST_N, 1)
gpio_set(GPIO.STM_BOOT0, 1)
time.sleep(0.5)
time.sleep(0.01)
gpio_set(GPIO.STM_RST_N, 0)
time.sleep(0.5)
time.sleep(0.01)
gpio_set(GPIO.STM_BOOT0, 0)
def booted(self):
@@ -592,7 +446,6 @@ class Tici(HardwareBase):
if __name__ == "__main__":
t = Tici()
t.configure_modem()
t.initialize_hardware()
t.set_power_save(False)
print(t.get_sim_info())
+43 -46
View File
@@ -4,7 +4,6 @@ import atexit
import base64
import fcntl
import hashlib
import math
import os
import requests
import serial
@@ -20,7 +19,7 @@ from typing import Any
from pathlib import Path
from openpilot.common.time_helpers import system_time_valid
from openpilot.system.hardware.base import LPABase, LPAError, Profile
from openpilot.system.hardware.base import LPABase, LPAError, LPAProfileNotFoundError, Profile
GSMA_CI_BUNDLE = str(Path(__file__).parent / "gsma_ci_bundle.pem")
@@ -29,15 +28,13 @@ DEFAULT_BAUD = 9600
DEFAULT_TIMEOUT = 5.0
# https://euicc-manual.osmocom.org/docs/lpa/applet-id/
ISDR_AID = "A0000005591010FFFFFFFF8900000100"
MM = "org.freedesktop.ModemManager1"
MM_MODEM = MM + ".Modem"
ES10X_MSS = 120
HTTP_TIMEOUT = 30
OPEN_ISDR_RETRIES = 10
OPEN_ISDR_RETRY_DELAY_S = 0.25
OPEN_ISDR_RESET_ATTEMPT = 5
SEND_APDU_RETRIES = 3
LOCK_FILE = '/dev/shm/modem_lpa.lock'
LOCK_FILE = '/dev/shm/modem.lock'
DEBUG = os.environ.get("DEBUG") == "1"
@@ -97,7 +94,7 @@ BPP_ERROR_MESSAGES = {
12: "Profile installation failed. The QR code may have already been used.",
}
# SGP.22 §5.2.6 — SM-DP+ reason/subject codes mapped to user-friendly messages
# SGP.22 §5.2.6 SM-DP+ reason/subject codes mapped to user-friendly messages
ES9P_ERROR_MESSAGES: dict[tuple[str, str], str] = {
('3.8', '8.2.6'): "This eSIM profile is already installed on another device. Please use a new QR code.",
('3.8', '8.2.1'): "This eSIM profile has expired. Please request a new QR code.",
@@ -137,7 +134,6 @@ class AtClient:
self._baud = baud
self._timeout = timeout
self._serial: serial.Serial | None = None
self._use_dbus = not os.path.exists(device)
def send_raw(self, data: bytes) -> None:
self._ensure_serial()
@@ -191,30 +187,7 @@ class AtClient:
if self._serial is None:
self._serial = serial.Serial(self._device, baudrate=self._baud, timeout=self._timeout)
def _get_modem(self):
import dbus
bus = dbus.SystemBus()
mm = bus.get_object(MM, '/org/freedesktop/ModemManager1')
objects = mm.GetManagedObjects(dbus_interface="org.freedesktop.DBus.ObjectManager", timeout=self._timeout)
modem_path = list(objects.keys())[0]
return bus.get_object(MM, modem_path)
def _dbus_query(self, cmd: str) -> list[str]:
if DEBUG:
print(f"DBUS >> {cmd}", file=sys.stderr)
try:
result = str(self._get_modem().Command(cmd, math.ceil(self._timeout), dbus_interface=MM_MODEM, timeout=self._timeout))
except Exception as e:
raise RuntimeError(f"AT command failed: {e}") from e
lines = [line.strip() for line in result.splitlines() if line.strip()]
if DEBUG:
for line in lines:
print(f"DBUS << {line}", file=sys.stderr)
return lines
def query(self, cmd: str) -> list[str]:
if self._use_dbus:
return self._dbus_query(cmd)
self._ensure_serial()
try:
self._send(cmd)
@@ -232,7 +205,7 @@ class AtClient:
pass
self.channel = None
# drain any unsolicited responses before opening
if self._serial and not self._use_dbus:
if self._serial:
try:
self._serial.reset_input_buffer()
except (OSError, serial.SerialException, termios.error):
@@ -604,8 +577,9 @@ def load_bpp(client: AtClient, b64_bpp: str) -> dict:
result = None
for chunk in _split_bpp(bpp):
response = es10x_command(client, chunk)
if response:
result = _parse_install_result(response) or result
if response and (parsed := _parse_install_result(response)):
result = parsed
break
if result is None:
raise RuntimeError("Profile installation failed: no result from eUICC")
@@ -716,22 +690,29 @@ class TiciLPA(LPABase):
atexit.register(self._client.close)
@contextmanager
def _acquire_channel(self):
def _acquire_lock(self):
fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
self._client.open_isdr()
yield
finally:
if self._client.channel:
try:
self._client.query(f"AT+CCHC={self._client.channel}")
except (RuntimeError, TimeoutError):
pass
self._client.channel = None
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
@contextmanager
def _acquire_channel(self):
with self._acquire_lock():
try:
self._client.open_isdr()
yield
finally:
if self._client.channel:
try:
self._client.query(f"AT+CCHC={self._client.channel}")
except (RuntimeError, TimeoutError):
pass
self._client.channel = None
def list_profiles(self) -> list[Profile]:
with self._acquire_channel():
return [
@@ -754,7 +735,10 @@ class TiciLPA(LPABase):
process_notifications(self._client)
def delete_profile(self, iccid: str) -> None:
if self.is_comma_profile(iccid):
profile = next((p for p in self.list_profiles() if p.iccid == iccid), None)
if profile is None:
raise LPAProfileNotFoundError(f"profile not found: {iccid}")
if profile.is_comma:
raise LPAError("refusing to delete a comma profile")
with self._acquire_channel():
request = encode_tlv(TAG_DELETE_PROFILE, encode_tlv(TAG_ICCID, string_to_tbcd(iccid)))
@@ -788,7 +772,20 @@ class TiciLPA(LPABase):
code = self._enable_profile(iccid)
if code not in (PROFILE_OK, PROFILE_NOT_IN_DISABLED_STATE):
raise LPAError(f"EnableProfile failed: {PROFILE_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
from openpilot.system.hardware import HARDWARE
if HARDWARE.get_device_type() == "mici":
self._client.send_raw(b'AT+CFUN=0\rAT+CFUN=1\r') # mici has no SIM presence pin; raw because CFUN=0 drops serial
self._client._ensure_serial(reconnect=True)
def is_euicc(self) -> bool:
# +CCHO:<n> -> ISD-R applet present, eUICC. Any error -> non-eUICC.
with self._acquire_lock():
try:
lines = self._client.query(f'AT+CCHO="{ISDR_AID}"')
except RuntimeError:
return False
for line in lines:
if line.startswith("+CCHO:") and (ch := line.split(":", 1)[1].strip()):
try:
self._client.query(f"AT+CCHC={ch}")
except (RuntimeError, TimeoutError):
pass
self._client.channel = None
return True
return False
+587
View File
@@ -0,0 +1,587 @@
#!/usr/bin/env python3
import fcntl
import json
import logging
import os
import serial
import signal
import subprocess
import tempfile
import time
from ipaddress import IPv4Address, AddressValueError
from enum import Enum
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s",
datefmt="%H:%M:%S",
)
AT_PORT = "/dev/modem_at0"
PPP_PORT = "/dev/modem_at1"
STATE_PATH = "/dev/shm/modem"
AT_LOCK = "/dev/shm/modem.lock" # shared with LPA
AT_INIT = [
"ATE0", # disable command echo
"ATV1", # verbose result codes (CONNECT/BUSY/NO CARRIER, not numeric)
"AT+CMEE=1", # numeric +CME ERROR codes on failures (per 3GPP 27.007)
"ATX4", # extended result codes: busy + dial tone detection, line speed in CONNECT
"AT&C1", # DCD pin follows carrier state (V.250 default)
"AT+CREG=2", # registration URCs include location info
"AT+CGREG=2", # GPRS registration URCs include location info
]
CREG = {0: "not_registered", 1: "home", 2: "searching", 3: "denied", 4: "unknown", 5: "roaming"}
# 3GPP TS 27.007 +COPS <AcT> -> network type
NETWORK_TYPE = {0: "gsm", 1: "gsm", 3: "gsm", 8: "gsm",
2: "utran", 4: "utran", 5: "utran", 6: "utran",
7: "lte", 9: "lte", 10: "lte",
11: "nr", 12: "nr", 13: "nr"}
DIAL_CID = 1
WEBBING_ICCID_PREFIX = "8985235"
PPPD_CMD = [
"sudo", "pppd", PPP_PORT, "460800", "noauth", "nodetach", "noipdefault", "usepeerdns",
"nodefaultroute", "connect",
"/usr/sbin/chat -v ABORT 'NO CARRIER' ABORT 'NO DIALTONE' ABORT 'BUSY' " +
f"ABORT 'NO ANSWER' ABORT 'ERROR' TIMEOUT 5 '' AT OK ATD*99***{DIAL_CID}# CONNECT ''",
"lcp-echo-interval", "30", "lcp-echo-failure", "4", "mtu", "1500", "mru", "1500",
"novj", "novjccomp", "ipcp-accept-local", "ipcp-accept-remote", "nomagic",
"user", '""', "password", '""',
]
INITIAL_STATE = {
"seconds_since_boot": 0,
"state": "INITIALIZING",
"connected": False, "ip_address": "",
"iccid": "", "mcc_mnc": "", "imei": "", "modem_version": "",
"signal_strength": 0, "signal_quality": 0,
"network_type": "unknown", "operator": "", "band": "", "channel": 0,
"registration": "unknown", "temperatures": [], "extra": "",
"tx_bytes": 0, "rx_bytes": 0,
}
class State(Enum):
INITIALIZING = "INITIALIZING"
SEARCHING = "SEARCHING"
CONNECTING = "CONNECTING"
CONNECTED = "CONNECTED"
DISCONNECTING = "DISCONNECTING"
STATE_WAIT = 1.0 # seconds to wait after each state handler returns
class PPPSession:
"""Owns pppd lifecycle, fail tracking, and PPP routing."""
MAX_FAILS = 3
def __init__(self):
self._proc: subprocess.Popen | None = None
self._fails = 0
self._peer = ""
def start(self):
self._proc = subprocess.Popen(PPPD_CMD, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self._peer = ""
logging.info(f"PPP dialing CID {DIAL_CID}")
def kill(self):
subprocess.run(["sudo", "killall", "-9", "pppd"], capture_output=True)
self._peer = ""
@staticmethod
def reset_data_port():
"""Drop DTR on PPP_PORT so the modem terminates any stuck PPP session."""
try:
with serial.Serial(PPP_PORT, 460800, timeout=1) as s:
s.dtr = False
time.sleep(0.2)
s.dtr = True
except Exception as e:
logging.warning(f"data port reset failed: {e}")
def has_exited(self) -> bool:
return self._proc is not None and self._proc.poll() is not None
def reset_fail_counter(self):
self._fails = 0
def record_fail(self) -> bool:
"""Bump fail counter; return True if at the give-up limit."""
self._fails += 1
return self._fails >= self.MAX_FAILS
@property
def fails(self) -> int:
return self._fails
def maybe_install_routes(self, ip: str, peer: str) -> bool:
"""Install routes if peer changed; kill the session on failure so the state machine reconnects."""
if not peer or peer == self._peer:
return False
try:
IPv4Address(ip)
IPv4Address(peer)
except AddressValueError:
logging.warning(f"refusing route install with non-IPv4 ip={ip!r} peer={peer!r}")
self.kill()
return False
self.cleanup_routes()
cmds = [
["sudo", "ip", "route", "add", "default", "via", peer, "dev", "ppp0", "metric", "1000"],
["sudo", "ip", "route", "add", "default", "via", peer, "dev", "ppp0", "table", "1000"],
["sudo", "ip", "rule", "add", "from", ip, "table", "1000"],
]
for cmd in cmds:
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
logging.warning(f"route install failed ({' '.join(cmd[1:])}): {r.stderr.strip()}")
self.cleanup_routes()
self.kill()
return False
logging.info(f"route set up for {ip} via {peer}")
self._peer = peer
return True
def maybe_install_dns(self, dns_servers: list[str]) -> bool:
"""Register DNS servers with systemd-resolved; kill the session on failure to force a retry."""
if not dns_servers:
return False
for cmd in (["sudo", "resolvectl", "dns", "ppp0", *dns_servers],
["sudo", "resolvectl", "default-route", "ppp0", "yes"]):
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
logging.warning(f"resolvectl failed ({' '.join(cmd[1:])}): {r.stderr.strip()}")
self.kill()
return False
logging.info(f"resolvectl: ppp0 DNS = {dns_servers}")
return True
@staticmethod
def cleanup_routes():
subprocess.run(["sudo", "ip", "route", "del", "default", "dev", "ppp0"], capture_output=True)
subprocess.run(["sudo", "ip", "route", "flush", "table", "1000"], capture_output=True)
# rules don't have a flush; delete until none remain
while subprocess.run(["sudo", "ip", "rule", "del", "table", "1000"], capture_output=True).returncode == 0:
pass
subprocess.run(["sudo", "resolvectl", "revert", "ppp0"], capture_output=True)
class Modem:
def __init__(self):
self._ppp = PPPSession()
self._sim_change = False
self._apn = "" # blank = network-provided via PCO
self._roaming_allowed = True
self.running = True
self.S = INITIAL_STATE.copy()
@staticmethod
def _read_param(key):
try:
with open(f"/data/params/d/{key}") as f:
return f.read().strip()
except FileNotFoundError:
return ""
@staticmethod
def _parse_reg(v: str) -> str:
try:
return CREG.get(int(v.split(",")[1].strip('"')), "unknown")
except (ValueError, IndexError):
return "unknown"
@staticmethod
def _has_modem_manager() -> bool:
return os.path.isfile("/lib/systemd/system/ModemManager.service")
def _is_roaming_allowed(self) -> bool:
if self.S["iccid"].startswith(WEBBING_ICCID_PREFIX):
return True
return self._read_param("GsmRoaming") == "1"
def _publish_state(self, **kwargs):
self.S.update(kwargs)
self.S["seconds_since_boot"] = time.monotonic()
with tempfile.NamedTemporaryFile(mode="w", dir="/dev/shm", delete=False) as f:
json.dump(self.S, f, indent=2)
os.chmod(f.name, 0o644)
os.replace(f.name, STATE_PATH)
def _at(self, cmd):
"""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:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
os.close(fd)
return []
try:
with serial.Serial(AT_PORT, 9600, timeout=5) as ser:
ser.reset_input_buffer()
ser.write((cmd + "\r").encode())
lines = []
while True:
raw = ser.readline()
if not raw:
raise TimeoutError("AT timeout")
line = raw.decode(errors="ignore").strip()
if not line:
continue
if line == "OK":
break
if line == "ERROR" or line.startswith("+CME ERROR"):
raise RuntimeError(line)
lines.append(line)
return lines
except (RuntimeError, TimeoutError, OSError) as e:
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):
if pfx in line and ":" in line:
return line.split(":", 1)[1].strip()
return None
def _init_at_channel(self) -> bool:
"""Run AT_INIT and confirm ATE0 took effect. Returns False if echo is still on."""
for c in AT_INIT:
self._at(c)
r = self._at("AT+CGMI")
return bool(r) and not r[0].startswith("AT")
def _configure_modem(self, modem_version: str):
if not modem_version.startswith("EG25"):
return
cmds = [
# clear initial EPS bearer APN (some carriers reject the default)
'AT+CGDCONT=0,"IP",""',
# SIM hot swap
'AT+QSIMDET=1,0',
'AT+QSIMSTAT=1',
# configure modem as data-centric
'AT+QNVW=5280,0,"0102000000000000"',
'AT+QNVFW="/nv/item_files/ims/IMS_enable",00',
'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01',
]
for c in cmds:
self._at(c)
def _do_initializing(self):
if not os.path.exists(AT_PORT):
return State.INITIALIZING
logging.info("port found, initializing")
self._ppp.kill()
self._ppp.cleanup_routes()
if not self._init_at_channel():
logging.warning("AT echo still on, retrying")
return State.INITIALIZING
identity = self._read_identity()
if not identity["iccid"] or not identity["imei"]:
logging.warning(f"identity read incomplete: {identity}, retrying")
return State.INITIALIZING
self._configure_modem(identity["modem_version"])
self.S.update(identity)
self._apn = self._read_param("GsmApn")
self._roaming_allowed = self._is_roaming_allowed()
# 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'}")
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)
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")
if not iccid.isdigit():
iccid = ""
imsi = first_line("AT+CIMI")
mcc_mnc = imsi[:6] if imsi.isdigit() and len(imsi) >= 6 else ""
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}
def _do_searching(self):
new_roaming = self._is_roaming_allowed()
if new_roaming != self._roaming_allowed:
logging.info(f"roaming changed: {self._roaming_allowed} -> {new_roaming}")
self._roaming_allowed = new_roaming
v = self._atv("AT+CREG?", "+CREG:")
if not v:
return self._searching_idle()
reg = self._parse_reg(v)
greg = self._parse_reg(self._atv("AT+CGREG?", "+CGREG:") or "")
logging.debug(f"creg={reg} cgreg={greg} roaming_allowed={self._roaming_allowed}")
if reg == "roaming" and not self._roaming_allowed:
self._publish_state(registration=reg)
return State.SEARCHING
if reg in ("home", "roaming") and greg in ("home", "roaming"):
self._publish_state(registration=reg)
return State.CONNECTING
if reg != self.S.get("registration"):
self._publish_state(registration=reg)
return self._searching_idle()
def _searching_idle(self):
if self._sim_change or not os.path.exists(AT_PORT):
logging.info(f"-> reconnecting (sim_change={self._sim_change} port={os.path.exists(AT_PORT)})")
return State.DISCONNECTING
return State.SEARCHING
def _do_connecting(self):
logging.info("starting pppd")
self._ppp.reset_fail_counter()
self._sim_change = False
self._ppp.start()
return State.CONNECTED
def _handle_pppd_exit(self):
if self._sim_change or not os.path.exists(AT_PORT):
return State.DISCONNECTING
give_up = self._ppp.record_fail()
if give_up:
logging.warning(f"PPP fail {self._ppp.fails}/{self._ppp.MAX_FAILS}, reconnecting")
return State.DISCONNECTING
logging.warning(f"PPP fail {self._ppp.fails}/{self._ppp.MAX_FAILS}, retrying")
self._ppp.reset_data_port()
if not os.path.exists(AT_PORT):
return State.DISCONNECTING
self._ppp.start()
return State.CONNECTED
def _params_changed(self) -> bool:
new_apn = self._read_param("GsmApn")
if new_apn != self._apn:
logging.info(f"GsmApn changed: '{self._apn}' -> '{new_apn}'")
return True
new_roaming = self._is_roaming_allowed()
if new_roaming != self._roaming_allowed:
logging.info(f"roaming changed: {self._roaming_allowed} -> {new_roaming}")
return True
return False
def _check_iccid(self, state):
if state in (State.INITIALIZING, State.DISCONNECTING) or not self.S["iccid"]:
return
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
if iccid and iccid != self.S["iccid"]:
logging.warning(f"iccid changed: {self.S['iccid']} -> {iccid}")
self._sim_change = True
def _do_connected(self):
if self._ppp.has_exited():
return self._handle_pppd_exit()
if self._sim_change or not os.path.exists(AT_PORT) or self._params_changed():
return State.DISCONNECTING
self._poll()
return State.CONNECTED
def _do_disconnecting(self):
logging.warning("reconnecting")
self._publish_state(**INITIAL_STATE)
self._ppp.kill()
self._ppp.cleanup_routes()
self._ppp.reset_data_port()
self._sim_change = False
return State.INITIALIZING
def _poll_signal(self) -> dict:
v = self._atv("AT+CSQ", "+CSQ:")
if not v:
return {}
try:
rssi = int(v.split(",")[0])
if rssi == 99:
return {}
return {"signal_strength": rssi, "signal_quality": min(100, int(rssi / 31 * 100))}
except (ValueError, IndexError):
return {}
def _poll_operator(self) -> dict:
v = self._atv("AT+COPS?", "+COPS:")
if not v:
return {}
p = v.split(",")
out: dict = {}
try:
if len(p) >= 3:
out["operator"] = p[2].strip('"')
if len(p) >= 4:
out["network_type"] = NETWORK_TYPE.get(int(p[3]), "unknown")
except (ValueError, IndexError):
pass
return out
def _poll_band(self) -> dict:
v = self._atv("AT+QNWINFO", "+QNWINFO:")
if not v:
return {}
info = v.replace('"', '').split(",")
try:
if len(info) >= 4:
return {"band": info[2], "channel": int(info[3])}
except ValueError:
pass
return {}
def _poll_extra(self) -> dict:
v = self._atv('AT+QENG="servingcell"', "+QENG:")
return {"extra": v.replace('"', '')} if v else {}
def _poll_temps(self) -> dict:
v = self._atv("AT+QTEMP", "+QTEMP:")
if not v:
return {}
try:
return {"temperatures": [t for t in (int(x) for x in v.split(",") if x.strip()) if t != 255]}
except (ValueError, IndexError):
return {}
def _poll_iface(self) -> dict:
try:
r = subprocess.run(["ip", "-4", "addr", "show", "ppp0"], capture_output=True, text=True, timeout=2)
ip, peer = "", ""
for line in r.stdout.splitlines():
# `inet 10.x.x.x peer 10.64.64.64/32 ...`
parts = line.strip().split()
if "inet" in parts:
i = parts.index("inet")
ip = parts[i + 1].split("/")[0]
if "peer" in parts:
peer = parts[parts.index("peer") + 1].split("/")[0]
break
if ip:
if self._ppp.maybe_install_routes(ip, peer):
self._ppp.maybe_install_dns(self._read_cellular_dns())
return {"ip_address": ip, "connected": True}
if self.S["connected"]:
return {"connected": False, "ip_address": ""}
except Exception:
pass
return {}
def _read_cellular_dns(self) -> list[str]:
v = self._atv(f"AT+CGCONTRDP={DIAL_CID}", "+CGCONTRDP:")
if not v:
return []
# +CGCONTRDP: <cid>,<bearer_id>,<apn>,<local_addr>,<gw_addr>,<dns_prim>,<dns_sec>,...
fields = [f.strip().strip('"') for f in v.split(",")]
dns_servers = []
for d in fields[5:7]:
try:
dns_servers.append(str(IPv4Address(d)))
except (AddressValueError, ValueError):
pass
if not dns_servers:
logging.warning(f"no cellular DNS servers reported by modem: {v!r}")
return dns_servers
def _poll_byte_counters(self) -> dict:
try:
with open("/sys/class/net/ppp0/statistics/tx_bytes") as f:
tx = int(f.read().strip())
with open("/sys/class/net/ppp0/statistics/rx_bytes") as f:
rx = int(f.read().strip())
except Exception:
return {}
return {"tx_bytes": tx, "rx_bytes": rx}
def _poll(self):
s: dict = {}
for fn in (self._poll_signal, self._poll_operator, self._poll_band,
self._poll_extra, self._poll_temps, self._poll_iface,
self._poll_byte_counters):
s.update(fn())
if s:
self._publish_state(**s)
def run(self):
logging.info("starting")
self._publish_state(state=State.INITIALIZING.value)
if self._has_modem_manager():
subprocess.run(["sudo", "systemctl", "mask", "--runtime", "ModemManager"], capture_output=True)
subprocess.run(["sudo", "systemctl", "stop", "ModemManager"], capture_output=True)
self._ppp.kill()
state = State.INITIALIZING
handlers = {
State.INITIALIZING: self._do_initializing,
State.SEARCHING: self._do_searching,
State.CONNECTING: self._do_connecting,
State.CONNECTED: self._do_connected,
State.DISCONNECTING: self._do_disconnecting,
}
while self.running:
try:
self._check_iccid(state)
prev = state
state = handlers[state]()
if state != prev:
self._publish_state(state=state.value)
logging.info(f"{prev.value} -> {state.value}")
except Exception:
logging.exception(f"error in {state.value}")
state = State.DISCONNECTING
time.sleep(STATE_WAIT)
def stop(self):
self.running = False
self._ppp.kill()
self._ppp.cleanup_routes()
try:
os.remove(STATE_PATH)
except FileNotFoundError:
pass
if self._has_modem_manager():
subprocess.run(["sudo", "systemctl", "unmask", "--runtime", "ModemManager"], capture_output=True)
subprocess.run(["sudo", "systemctl", "start", "ModemManager"], capture_output=True)
def main():
m = Modem()
def _sig(*_):
m.running = False
signal.signal(signal.SIGINT, _sig)
signal.signal(signal.SIGTERM, _sig)
m.run()
m.stop()
if __name__ == "__main__":
main()
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env bash
#nmcli connection modify --temporary lte gsm.home-only yes
#nmcli connection modify --temporary lte gsm.auto-config yes
#nmcli connection modify --temporary lte connection.autoconnect-retries 20
sudo nmcli connection reload
sudo systemctl stop ModemManager
nmcli con down lte
nmcli con down blue-prime
# power cycle modem
/usr/comma/lte/lte.sh stop_blocking
/usr/comma/lte/lte.sh start
sudo systemctl restart NetworkManager
#sudo systemctl restart ModemManager
sudo ModemManager --debug
@@ -42,7 +42,7 @@ PROCS = [
class TestPowerDraw:
def setup_method(self):
Params().put("CarParams", get_demo_car_params().to_bytes())
Params().put("CarParams", get_demo_car_params().to_bytes(), block=True)
# wait a bit for power save to disable
time.sleep(5)
+3 -17
View File
@@ -1,17 +1,3 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
AGNOS_PY=$1
MANIFEST=$2
if [[ ! -f "$AGNOS_PY" || ! -f "$MANIFEST" ]]; then
echo "invalid args"
exit 1
fi
if systemctl is-active --quiet weston-ready; then
$DIR/updater_weston $AGNOS_PY $MANIFEST
else
$DIR/updater_magic $AGNOS_PY $MANIFEST
fi
version https://git-lfs.github.com/spec/v1
oid sha256:3a94ab8395f20d20a9d5a2a2bacca0694f072df8421cf13adca6250d28065bdc
size 24709205
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a94ab8395f20d20a9d5a2a2bacca0694f072df8421cf13adca6250d28065bdc
size 24709205
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eba5f44e6a763e1f74d1c718993218adcc72cba4caafe99b595fa701151a4c54
size 10448792
+6 -7
View File
@@ -5,18 +5,17 @@ libs = [common, messaging, visionipc,
'pthread', 'z', 'm', 'zstd']
frameworks = []
src = ['logger.cc', 'zstd_writer.cc', 'video_writer.cc', 'encoder/encoder.cc', 'encoder/v4l_encoder.cc', 'encoder/jpeg_encoder.cc']
if arch != "larch64":
src = ['logger.cc', 'zstd_writer.cc', 'video_writer.cc', 'encoder/encoder.cc', 'encoder/jpeg_encoder.cc']
if arch == "larch64":
src += ['encoder/v4l_encoder.cc']
else:
src += ['encoder/ffmpeg_encoder.cc']
libs += ['yuv']
if arch == "Darwin":
frameworks += ['VideoToolbox', 'CoreMedia', 'CoreFoundation', 'CoreVideo']
else:
libs += ['va', 'va-drm', 'drm']
if arch == "Darwin":
# exclude v4l
del src[src.index('encoder/v4l_encoder.cc')]
if arch != "Darwin":
libs += ['va', 'va-drm', 'drm']
logger_lib = env.Library('logger', src)
libs.insert(0, logger_lib)
+2 -1
View File
@@ -2,7 +2,7 @@
// has to be in this order
#ifdef __linux__
#include "third_party/linux/include/v4l2-controls.h"
#include <linux/v4l2-controls.h>
#include <linux/videodev2.h>
#else
#define V4L2_BUF_FLAG_KEYFRAME 8
@@ -26,6 +26,7 @@ public:
virtual int encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra) = 0;
virtual void encoder_open() = 0;
virtual void encoder_close() = 0;
virtual void set_bitrate(int bitrate) = 0;
void publisher_publish(int segment_num, uint32_t idx, VisionIpcBufExtra &extra, unsigned int flags, kj::ArrayPtr<capnp::byte> header, kj::ArrayPtr<capnp::byte> dat);
+4
View File
@@ -72,6 +72,10 @@ void FfmpegEncoder::encoder_close() {
is_open = false;
}
void FfmpegEncoder::set_bitrate(int bitrate) {
LOGE("adaptive bitrate is not supported for ffmpeg encoder %s", encoder_info.publish_name);
}
int FfmpegEncoder::encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra) {
assert(buf->width == this->in_width);
assert(buf->height == this->in_height);
+1
View File
@@ -21,6 +21,7 @@ public:
int encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra);
void encoder_open();
void encoder_close();
void set_bitrate(int bitrate);
private:
int segment_num = -1;
+23 -2
View File
@@ -7,10 +7,10 @@
#include "common/util.h"
#include "common/timing.h"
#include "third_party/linux/include/msm_media_info.h"
#include <media/msm_media_info.h>
// has to be in this order
#include "third_party/linux/include/v4l2-controls.h"
#include <linux/v4l2-controls.h>
#include <linux/videodev2.h>
#define V4L2_QCOM_BUF_FLAG_CODECCONFIG 0x00020000
#define V4L2_QCOM_BUF_FLAG_EOS 0x02000000
@@ -155,6 +155,8 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
assert(strcmp((const char *)cap.card, "msm_vidc_venc") == 0);
EncoderSettings encoder_settings = encoder_info.get_settings(in_width);
current_bitrate = encoder_settings.bitrate;
adaptive_bitrate = encoder_info.adaptive_bitrate;
bool is_h265 = encoder_settings.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C;
struct v4l2_format fmt_out = {
@@ -304,6 +306,25 @@ void V4LEncoder::encoder_close() {
this->is_open = false;
}
void V4LEncoder::set_bitrate(int bitrate) {
if (!adaptive_bitrate || bitrate == current_bitrate) return;
if (bitrate <= 0) {
LOGE("invalid livestream encoder bitrate %d", bitrate);
return;
}
struct v4l2_control ctrl = {
.id = V4L2_CID_MPEG_VIDEO_BITRATE,
.value = bitrate,
};
if (util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl) == -1) {
LOGE("failed to update %s bitrate to %d", encoder_info.publish_name, bitrate);
return;
}
current_bitrate = bitrate;
}
V4LEncoder::~V4LEncoder() {
encoder_close();
v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
+3
View File
@@ -13,6 +13,7 @@ public:
int encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra);
void encoder_open();
void encoder_close();
void set_bitrate(int bitrate);
private:
int fd;
@@ -20,6 +21,8 @@ private:
bool is_open = false;
int segment_num = -1;
int counter = 0;
int current_bitrate = -1;
bool adaptive_bitrate;
SafeQueue<VisionIpcBufExtra> extras;
+15
View File
@@ -44,11 +44,24 @@ bool sync_encoders(EncoderdState *s, VisionStreamType cam_type, uint32_t frame_i
}
}
void apply_bitrate(std::vector<std::unique_ptr<Encoder>> &encoders) {
static Params params;
std::string val = params.get("LivestreamEncoderBitrate");
if (val.empty()) return;
int bitrate = std::stoi(val);
for (auto &e : encoders) {
e->set_bitrate(bitrate);
}
}
void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) {
util::set_thread_name(cam_info.thread_name);
std::vector<std::unique_ptr<Encoder>> encoders;
bool has_adaptive = std::any_of(cam_info.encoder_infos.begin(), cam_info.encoder_infos.end(),
[](const auto &ei) { return ei.adaptive_bitrate; });
VisionIpcClient vipc_client = VisionIpcClient("camerad", cam_info.stream_type, false);
std::unique_ptr<JpegEncoder> jpeg_encoder;
@@ -108,6 +121,8 @@ void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) {
++cur_seg;
}
if (has_adaptive) apply_bitrate(encoders);
// encode a frame
for (int i = 0; i < encoders.size(); ++i) {
int out_id = encoders[i]->encode_frame(buf, &extra);
+9 -5
View File
@@ -47,8 +47,8 @@ struct EncoderSettings {
}
static EncoderSettings StreamEncoderSettings() {
int _stream_bitrate = getenv("STREAM_BITRATE") ? atoi(getenv("STREAM_BITRATE")) : 1'000'000;
return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = _stream_bitrate , .gop_size = 15};
int _stream_bitrate = getenv("STREAM_BITRATE") ? atoi(getenv("STREAM_BITRATE")) : 5'000'000;
return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = _stream_bitrate , .gop_size = 5};
}
};
@@ -59,6 +59,7 @@ public:
const char *filename = NULL;
bool record = true;
bool include_audio = false;
bool adaptive_bitrate = false;
int frame_width = -1;
int frame_height = -1;
int fps = MAIN_FPS;
@@ -104,6 +105,7 @@ const EncoderInfo stream_road_encoder_info = {
.publish_name = "livestreamRoadEncodeData",
//.thumbnail_name = "thumbnail",
.record = false,
.adaptive_bitrate = true,
.get_settings = [](int){return EncoderSettings::StreamEncoderSettings();},
INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode),
};
@@ -111,6 +113,7 @@ const EncoderInfo stream_road_encoder_info = {
const EncoderInfo stream_wide_road_encoder_info = {
.publish_name = "livestreamWideRoadEncodeData",
.record = false,
.adaptive_bitrate = true,
.get_settings = [](int){return EncoderSettings::StreamEncoderSettings();},
INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode),
};
@@ -118,6 +121,7 @@ const EncoderInfo stream_wide_road_encoder_info = {
const EncoderInfo stream_driver_encoder_info = {
.publish_name = "livestreamDriverEncodeData",
.record = false,
.adaptive_bitrate = true,
.get_settings = [](int){return EncoderSettings::StreamEncoderSettings();},
INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode),
};
@@ -153,19 +157,19 @@ const LogCameraInfo driver_camera_info{
const LogCameraInfo stream_road_camera_info{
.thread_name = "road_cam_encoder",
.stream_type = VISION_STREAM_ROAD,
.encoder_infos = {stream_road_encoder_info}
.encoder_infos = {stream_road_encoder_info},
};
const LogCameraInfo stream_wide_road_camera_info{
.thread_name = "wide_road_cam_encoder",
.stream_type = VISION_STREAM_WIDE_ROAD,
.encoder_infos = {stream_wide_road_encoder_info}
.encoder_infos = {stream_wide_road_encoder_info},
};
const LogCameraInfo stream_driver_camera_info{
.thread_name = "driver_cam_encoder",
.stream_type = VISION_STREAM_DRIVER,
.encoder_infos = {stream_driver_encoder_info}
.encoder_infos = {stream_driver_encoder_info},
};
const LogCameraInfo cameras_logged[] = {road_camera_info, wide_road_camera_info, driver_camera_info};
+2 -2
View File
@@ -76,8 +76,8 @@ class UploaderTestCase:
self.seg_dir = self.seg_format.format(self.seg_num)
self.params = Params()
self.params.put("IsOffroad", True)
self.params.put("DongleId", "0000000000000000")
self.params.put("IsOffroad", True, block=True)
self.params.put("DongleId", "0000000000000000", block=True)
def make_file_with_data(self, f_dir: str, fn: str, size_mb: float = .1, lock: bool = False,
upload_xattr: bytes | None = None, preserve_xattr: bytes | None = None) -> Path:
+1 -1
View File
@@ -53,7 +53,7 @@ class TestEncoder:
# TODO: this should run faster than real time
@parameterized.expand([(True, ), (False, )])
def test_log_rotation(self, record_front):
Params().put_bool("RecordFront", record_front)
Params().put_bool("RecordFront", record_front, block=True)
managed_processes['sensord'].start()
managed_processes['loggerd'].start()
+12 -6
View File
@@ -82,7 +82,7 @@ class TestLoggerd:
assert pm.wait_for_readers_to_update(s, timeout=5)
sent_msgs = defaultdict(list)
for _ in range(random.randint(2, 10) * 100):
for i in range(random.randint(2, 10) * 100):
for s in services:
try:
m = messaging.new_message(s)
@@ -91,6 +91,12 @@ class TestLoggerd:
pm.send(s, m)
sent_msgs[s].append(m)
# Keep msgq's finite per-service queues from wrapping; this test asserts
# that loggerd logged every message we sent.
if (i + 1) % 100 == 0:
for s in services:
assert pm.wait_for_readers_to_update(s, timeout=5)
for s in services:
assert pm.wait_for_readers_to_update(s, timeout=5)
managed_processes["loggerd"].stop()
@@ -161,8 +167,8 @@ class TestLoggerd:
]
params = Params()
for k, _, v in fake_params:
params.put(k, v)
params.put("AccessToken", "abc")
params.put(k, v, block=True)
params.put("AccessToken", "abc", block=True)
lr = list(LogReader(str(self._gen_bootlog())))
initData = lr[0].initData
@@ -188,7 +194,7 @@ class TestLoggerd:
@pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing
def test_rotation(self):
Params().put("RecordFront", True)
Params().put("RecordFront", True, block=True)
expected_files = {"rlog.zst", "qlog.zst", "qcamera.ts", "fcamera.hevc", "dcamera.hevc", "ecamera.hevc"}
@@ -309,7 +315,7 @@ class TestLoggerd:
@pytest.mark.parametrize("record_front", [True, False])
def test_record_front(self, record_front):
params = Params()
params.put_bool("RecordFront", record_front)
params.put_bool("RecordFront", record_front, block=True)
self._publish_camera_and_audio_messages()
@@ -320,7 +326,7 @@ class TestLoggerd:
@pytest.mark.parametrize("record_audio", [True, False])
def test_record_audio(self, record_audio):
params = Params()
params.put_bool("RecordAudio", record_audio)
params.put_bool("RecordAudio", record_audio, block=True)
self._publish_camera_and_audio_messages()
+30 -59
View File
@@ -1,73 +1,58 @@
#!/usr/bin/env python3
import os
import subprocess
from pathlib import Path
# NOTE: Do NOT import anything here that needs be built (e.g. params)
from openpilot.common.basedir import BASEDIR
from openpilot.common.spinner import Spinner
from openpilot.common.text_window import TextWindow
from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.hardware import HARDWARE, AGNOS
from openpilot.system.version import get_build_metadata
MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9
CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache")
TOTAL_SCONS_NODES = 2705
MAX_BUILD_PROGRESS = 100
def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
env = os.environ.copy()
env['SCONS_PROGRESS'] = "1"
nproc = os.cpu_count()
if nproc is None:
nproc = 2
extra_args = ["--minimal"] if minimal else []
def build() -> None:
spinner = Spinner()
spinner.update_progress(0, 100)
HARDWARE.set_power_save(False)
if AGNOS:
HARDWARE.set_power_save(False)
os.sched_setaffinity(0, range(8)) # ensure we can use the isolcpus cores
# building with all cores can result in using too
# much memory, so retry with less parallelism
# building with all cores can result in using too much memory, so retry serially
compile_output: list[bytes] = []
for n in (nproc, nproc/2, 1):
for parallelism in ([], ["-j4"], ["-j1"]):
compile_output.clear()
scons: subprocess.Popen = subprocess.Popen(["scons", f"-j{int(n)}", "--cache-populate", *extra_args], cwd=BASEDIR, env=env, stderr=subprocess.PIPE)
assert scons.stderr is not None
with subprocess.Popen(["scons", *parallelism], cwd=BASEDIR, env={**os.environ, "PWD": BASEDIR}, stderr=subprocess.PIPE) as scons:
assert scons.stderr is not None
# Read progress from stderr and update spinner
while scons.poll() is None:
try:
line = scons.stderr.readline()
if line is None:
continue
# Read progress from stderr and update spinner
while scons.poll() is None:
try:
line = scons.stderr.readline()
if line is None:
continue
line = line.rstrip()
prefix = b'progress: '
if line.startswith(prefix):
progress = float(line[len(prefix):])
spinner.update_progress(100 * min(1., progress / 100.), 100.)
elif len(line):
compile_output.append(line)
print(line.decode('utf8', 'replace'))
except Exception:
pass
# Drain and close the pipe before retrying or returning.
for line in scons.stderr.read().split(b'\n'):
line = line.rstrip()
prefix = b'progress: '
if line.startswith(prefix):
i = int(line[len(prefix):])
spinner.update_progress(MAX_BUILD_PROGRESS * min(1., i / TOTAL_SCONS_NODES), 100.)
elif len(line):
if len(line):
compile_output.append(line)
print(line.decode('utf8', 'replace'))
except Exception:
pass
if scons.returncode == 0:
break
if scons.returncode != 0:
# Read remaining output
if scons.stderr is not None:
compile_output += scons.stderr.read().split(b'\n')
# Build failed log errors
error_s = b"\n".join(compile_output).decode('utf8', 'replace')
add_file_handler(cloudlog)
cloudlog.error("scons build failed\n" + error_s)
# Show TextWindow
spinner.close()
@@ -76,19 +61,5 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
t.wait_for_exit()
exit(1)
# enforce max cache size
cache_files = [f for f in CACHE_DIR.rglob('*') if f.is_file()]
cache_files.sort(key=lambda f: f.stat().st_mtime)
cache_size = sum(f.stat().st_size for f in cache_files)
for f in cache_files:
if cache_size < MAX_CACHE_SIZE:
break
cache_size -= f.stat().st_size
f.unlink()
if __name__ == "__main__":
spinner = Spinner()
spinner.update_progress(0, 100)
build_metadata = get_build_metadata()
build(spinner, build_metadata.openpilot.is_dirty, minimal = AGNOS)
build()
+2 -2
View File
@@ -46,8 +46,8 @@ def unblock_stdout() -> None:
def write_onroad_params(started, params):
params.put_bool("IsOnroad", started)
params.put_bool("IsOffroad", not started)
params.put_bool("IsOnroad", started, block=True)
params.put_bool("IsOffroad", not started, block=True)
def save_bootlog():
+14 -14
View File
@@ -40,7 +40,7 @@ def manager_init() -> None:
# device boot mode
if params.get("DeviceBootMode") == 1: # start in Always Offroad mode
params.put_bool("OffroadMode", True)
params.put_bool("OffroadMode", True, block=True)
# quick boot
if params.get_bool("QuickBootToggle") and not PC:
@@ -49,7 +49,7 @@ def manager_init() -> None:
open(prebuilt_path, 'x').close()
if params.get_bool("RecordFrontLock"):
params.put_bool("RecordFront", True)
params.put_bool("RecordFront", True, block=True)
if not PC:
run_migration(params)
@@ -58,7 +58,7 @@ def manager_init() -> None:
for k in params.all_keys():
default_value = params.get_default_value(k)
if default_value is not None and params.get(k) is None:
params.put(k, default_value)
params.put(k, default_value, block=True)
# Create folders needed for msgq
try:
@@ -70,16 +70,16 @@ def manager_init() -> None:
# set params
serial = HARDWARE.get_serial()
params.put("Version", build_metadata.openpilot.version)
params.put("GitCommit", build_metadata.openpilot.git_commit)
params.put("GitCommitDate", build_metadata.openpilot.git_commit_date)
params.put("GitBranch", build_metadata.channel)
params.put("GitRemote", build_metadata.openpilot.git_origin)
params.put_bool("IsDevelopmentBranch", build_metadata.development_channel)
params.put_bool("IsTestedBranch", build_metadata.tested_channel)
params.put_bool("IsReleaseBranch", build_metadata.release_channel)
params.put_bool("IsReleaseSpBranch", build_metadata.release_sp_channel)
params.put("HardwareSerial", serial)
params.put("Version", build_metadata.openpilot.version, block=True)
params.put("GitCommit", build_metadata.openpilot.git_commit, block=True)
params.put("GitCommitDate", build_metadata.openpilot.git_commit_date, block=True)
params.put("GitBranch", build_metadata.channel, block=True)
params.put("GitRemote", build_metadata.openpilot.git_origin, block=True)
params.put_bool("IsDevelopmentBranch", build_metadata.development_channel, block=True)
params.put_bool("IsTestedBranch", build_metadata.tested_channel, block=True)
params.put_bool("IsReleaseBranch", build_metadata.release_channel, block=True)
params.put_bool("IsReleaseSpBranch", build_metadata.release_sp_channel, block=True)
params.put("HardwareSerial", serial, block=True)
# set dongle id
reg_res = register(show_spinner=True)
@@ -191,7 +191,7 @@ def manager_thread() -> None:
for param in ("DoUninstall", "DoShutdown", "DoReboot"):
if params.get_bool(param):
shutdown = True
params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}")
params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}", block=True)
cloudlog.warning(f"Shutting down manager - {param} set")
if shutdown:
+2 -6
View File
@@ -190,12 +190,8 @@ class PythonProcess(ManagerProcess):
if self.proc is not None:
return
# TODO: this is just a workaround for this tinygrad check:
# https://github.com/tinygrad/tinygrad/blob/ac9c96dae1656dc220ee4acc39cef4dd449aa850/tinygrad/device.py#L26
name = self.name if "modeld" not in self.name else "MainProcess"
cloudlog.info(f"starting python {self.module}")
self.proc = Process(name=name, target=self.launcher, args=(self.module, self.name))
self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name))
self.proc.start()
self.shutting_down = False
@@ -240,7 +236,7 @@ class DaemonProcess(ManagerProcess):
stderr=open('/dev/null', 'w'),
preexec_fn=os.setpgrp)
self.params.put(self.param_name, proc.pid)
self.params.put(self.param_name, proc.pid, block=True)
def stop(self, retry=True, block=True, sig=None) -> None:
pass
+2 -1
View File
@@ -34,7 +34,7 @@ def ublox_available() -> bool:
def ublox(started: bool, params: Params, CP: car.CarParams) -> bool:
use_ublox = ublox_available()
if use_ublox != params.get_bool("UbloxAvailable"):
params.put_bool("UbloxAvailable", use_ublox)
params.put_bool("UbloxAvailable", use_ublox, block=True)
return started and use_ublox
def joystick(started: bool, params: Params, CP: car.CarParams) -> bool:
@@ -148,6 +148,7 @@ procs = [
PythonProcess("lateral_maneuversd", "tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver),
PythonProcess("radard", "selfdrive.controls.radard", only_onroad),
PythonProcess("hardwared", "system.hardware.hardwared", always_run),
PythonProcess("modem", "system.hardware.tici.modem", always_run, enabled=TICI),
PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC),
PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC),
PythonProcess("uploader", "system.loggerd.uploader", uploader_ready),
+20 -94
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env python3
import fcntl
import os
import sys
import signal
import itertools
import math
import time
import requests
import shutil
from serial import Serial
import datetime
from multiprocessing import Process, Event
from typing import NoReturn
from struct import unpack_from, calcsize, pack
@@ -30,9 +28,6 @@ from openpilot.system.qcomgpsd.structs import (dict_unpacker, position_report, r
LOG_GNSS_OEMDRE_SVPOLY_REPORT)
DEBUG = int(os.getenv("DEBUG", "0"))==1
ASSIST_DATA_FILE = '/tmp/xtra3grc.bin'
ASSIST_DATA_FILE_DOWNLOAD = ASSIST_DATA_FILE + '.download'
ASSISTANCE_URL = 'http://xtrapath3.izatcloud.net/xtra3grc.bin'
LOG_TYPES = [
LOG_GNSS_GPS_MEASUREMENT_REPORT,
@@ -91,70 +86,32 @@ def try_setup_logs(diag, logs):
return setup_logs(diag, logs)
AT_PORT = "/dev/modem_at0"
AT_LOCK = "/dev/shm/modem.lock" # shared with modem.py and LPA
@retry(attempts=5, delay=1.0)
def at_cmd(cmd: str) -> str:
with Serial(AT_PORT, baudrate=115200, timeout=5) as ser:
ser.reset_input_buffer()
ser.write(f"{cmd}\r".encode())
lines = []
while True:
line = ser.readline()
if not line:
raise RuntimeError(f"AT command timeout: {cmd}")
line = line.decode('utf-8', errors='replace').strip()
if line in ("OK", "ERROR") or line.startswith("+CME ERROR"):
break
if line and line != cmd:
lines.append(line)
return '\n'.join(lines)
with os.fdopen(os.open(AT_LOCK, os.O_CREAT | os.O_RDWR, 0o666), "r+") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
with Serial(AT_PORT, baudrate=115200, timeout=5) as ser:
ser.reset_input_buffer()
ser.write(f"{cmd}\r".encode())
lines = []
while True:
line = ser.readline()
if not line:
raise RuntimeError(f"AT command timeout: {cmd}")
line = line.decode('utf-8', errors='replace').strip()
if line in ("OK", "ERROR") or line.startswith("+CME ERROR"):
break
if line and line != cmd:
lines.append(line)
return '\n'.join(lines)
def gps_enabled() -> bool:
return "QGPS: 1" in at_cmd("AT+QGPS?")
def download_assistance():
try:
response = requests.get(ASSISTANCE_URL, timeout=5, stream=True)
with open(ASSIST_DATA_FILE_DOWNLOAD, 'wb') as fp:
for chunk in response.iter_content(chunk_size=8192):
fp.write(chunk)
if fp.tell() > 1e5:
cloudlog.error("Qcom assistance data larger than expected")
return
os.rename(ASSIST_DATA_FILE_DOWNLOAD, ASSIST_DATA_FILE)
except requests.exceptions.RequestException:
cloudlog.exception("Failed to download assistance file")
return
def downloader_loop(event):
if os.path.exists(ASSIST_DATA_FILE):
os.remove(ASSIST_DATA_FILE)
alt_path = os.getenv("QCOM_ALT_ASSISTANCE_PATH", None)
if alt_path is not None and os.path.exists(alt_path):
shutil.copyfile(alt_path, ASSIST_DATA_FILE)
try:
while not os.path.exists(ASSIST_DATA_FILE) and not event.is_set():
download_assistance()
event.wait(timeout=10)
except KeyboardInterrupt:
pass
@retry(attempts=5, delay=0.2, ignore_failure=True)
def inject_assistance():
import subprocess
cmd = f"mmcli -m any --timeout 30 --location-inject-assistance-data={ASSIST_DATA_FILE}"
subprocess.check_output(cmd, stderr=subprocess.PIPE, shell=True)
cloudlog.info("successfully loaded assistance data")
@retry(attempts=5, delay=1.0)
def setup_quectel(diag: ModemDiag) -> bool:
ret = False
def setup_quectel(diag: ModemDiag):
# enable OEMDRE in the NV
# TODO: it has to reboot for this to take effect
DIAG_NV_READ_F = 38
@@ -168,26 +125,11 @@ def setup_quectel(diag: ModemDiag) -> bool:
if gps_enabled():
at_cmd("AT+QGPSEND")
if "GPS_COLD_START" in os.environ:
# deletes all assistance
at_cmd("AT+QGPSDEL=0")
else:
# allow module to perform hot start
at_cmd("AT+QGPSDEL=1")
# disable DPO power savings for more accuracy
at_cmd("AT+QGPSCFG=\"dpoenable\",0")
# don't automatically turn on GNSS on powerup
at_cmd("AT+QGPSCFG=\"autogps\",0")
# Do internet assistance
at_cmd("AT+QGPSXTRA=1")
at_cmd("AT+QGPSSUPLURL=\"NULL\"")
if os.path.exists(ASSIST_DATA_FILE):
ret = True
inject_assistance()
os.remove(ASSIST_DATA_FILE)
#at_cmd("AT+QGPSXTRADATA?")
if system_time_valid():
time_str = datetime.datetime.now(datetime.UTC).replace(tzinfo=None).strftime("%Y/%m/%d,%H:%M:%S")
at_cmd(f"AT+QGPSXTRATIME=0,\"{time_str}\",1,1,1000")
@@ -214,7 +156,6 @@ def setup_quectel(diag: ModemDiag) -> bool:
0,0
))
return ret
def teardown_quectel(diag):
at_cmd("AT+QGPSCFG=\"outport\",\"none\"")
@@ -255,9 +196,6 @@ def main() -> NoReturn:
wait_for_modem()
stop_download_event = Event()
assist_fetch_proc = Process(target=downloader_loop, args=(stop_download_event,))
assist_fetch_proc.start()
def cleanup(sig, frame):
cloudlog.warning("caught sig disabling quectel gps")
@@ -268,18 +206,13 @@ def main() -> NoReturn:
except NameError:
cloudlog.warning('quectel not yet setup')
stop_download_event.set()
assist_fetch_proc.kill()
assist_fetch_proc.join()
sys.exit(0)
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)
# connect to modem
diag = ModemDiag()
r = setup_quectel(diag)
want_assistance = not r
setup_quectel(diag)
cloudlog.warning("quectel setup done")
gpio_init(GPIO.GNSS_PWR_EN, True)
gpio_set(GPIO.GNSS_PWR_EN, True)
@@ -287,10 +220,6 @@ def main() -> NoReturn:
pm = messaging.PubMaster(['qcomGnss', 'gpsLocation'])
while 1:
if os.path.exists(ASSIST_DATA_FILE) and want_assistance:
setup_quectel(diag)
want_assistance = False
opcode, payload = diag.recv()
if opcode != DIAG_LOG_F:
cloudlog.error(f"Unhandled opcode: {opcode}")
@@ -383,9 +312,6 @@ def main() -> NoReturn:
gps.speedAccuracy = math.sqrt(sum([x**2 for x in vNEDsigma]))
# quectel gps verticalAccuracy is clipped to 500, set invalid if so
gps.hasFix = gps.verticalAccuracy != 500
if gps.hasFix:
want_assistance = False
stop_download_event.set()
pm.send('gpsLocation', msg)
elif log_type == LOG_GNSS_OEMDRE_SVPOLY_REPORT:
-119
View File
@@ -1,119 +0,0 @@
import os
import pytest
import time
import datetime
import cereal.messaging as messaging
from openpilot.system.qcomgpsd.qcomgpsd import at_cmd, wait_for_modem
from openpilot.system.manager.process_config import managed_processes
GOOD_SIGNAL = bool(int(os.getenv("GOOD_SIGNAL", '0')))
@pytest.mark.tici
class TestRawgpsd:
@classmethod
def setup_class(cls):
os.environ['GPS_COLD_START'] = '1'
os.system("sudo systemctl start systemd-resolved")
os.system("sudo systemctl restart ModemManager lte")
wait_for_modem()
@classmethod
def teardown_class(cls):
managed_processes['qcomgpsd'].stop()
os.system("sudo systemctl restart systemd-resolved")
os.system("sudo systemctl restart ModemManager lte")
def setup_method(self):
self.sm = messaging.SubMaster(['qcomGnss', 'gpsLocation', 'gnssMeasurements'])
def teardown_method(self):
managed_processes['qcomgpsd'].stop()
os.system("sudo systemctl restart systemd-resolved")
def _wait_for_output(self, t):
dt = 0.1
for _ in range(t*int(1/dt)):
self.sm.update(0)
if self.sm.updated['qcomGnss']:
break
time.sleep(dt)
return self.sm.updated['qcomGnss']
def test_no_crash_double_command(self):
wait_for_modem()
at_cmd("AT+QGPSDEL=0")
at_cmd("AT+QGPSDEL=0")
def test_wait_for_modem(self):
os.system("sudo systemctl stop ModemManager")
managed_processes['qcomgpsd'].start()
assert self._wait_for_output(30)
def test_startup_time(self, subtests):
for internet in (True, False):
if not internet:
os.system("sudo systemctl stop systemd-resolved")
with subtests.test(internet=internet):
managed_processes['qcomgpsd'].start()
assert self._wait_for_output(30)
managed_processes['qcomgpsd'].stop()
def test_turns_off_gnss(self, subtests):
for s in (0.1, 1, 5):
with subtests.test(runtime=s):
managed_processes['qcomgpsd'].start()
time.sleep(s)
managed_processes['qcomgpsd'].stop()
wait_for_modem()
resp = at_cmd("AT+QGPS?")
assert "+QGPS: 0" in resp
def check_assistance(self, should_be_loaded):
# after QGPSDEL: '+QGPSXTRADATA: 0,"1980/01/05,19:00:00"'
# after loading: '+QGPSXTRADATA: 10080,"2023/06/24,19:00:00"'
wait_for_modem()
out = at_cmd("AT+QGPSXTRADATA?")
out = out.split("+QGPSXTRADATA:")[1].split("'")[0].strip()
valid_duration, injected_time_str = out.split(",", 1)
if should_be_loaded:
assert valid_duration == "10080" # should be max time
injected_time = datetime.datetime.strptime(injected_time_str.replace("\"", ""), "%Y/%m/%d,%H:%M:%S")
assert abs((datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - injected_time).total_seconds()) < 60*60*12
else:
valid_duration, injected_time_str = out.split(",", 1)
injected_time_str = injected_time_str.replace('\"', '').replace('\'', '')
assert injected_time_str[:] == '1980/01/05,19:00:00'[:]
assert valid_duration == '0'
@pytest.mark.skip(reason="XTRA injection via QMI needs debugging on AGNOS 17")
def test_assistance_loading(self):
managed_processes['qcomgpsd'].start()
assert self._wait_for_output(30)
managed_processes['qcomgpsd'].stop()
self.check_assistance(True)
@pytest.mark.skip(reason="XTRA injection via QMI needs debugging on AGNOS 17")
def test_no_assistance_loading(self):
os.system("sudo systemctl stop systemd-resolved")
managed_processes['qcomgpsd'].start()
assert self._wait_for_output(30)
managed_processes['qcomgpsd'].stop()
self.check_assistance(False)
@pytest.mark.skip(reason="XTRA injection via QMI needs debugging on AGNOS 17")
def test_late_assistance_loading(self):
os.system("sudo systemctl stop systemd-resolved")
managed_processes['qcomgpsd'].start()
self._wait_for_output(17)
assert self.sm.updated['qcomGnss']
os.system("sudo systemctl restart systemd-resolved")
time.sleep(15)
managed_processes['qcomgpsd'].stop()
self.check_assistance(True)
+4 -6
View File
@@ -11,19 +11,21 @@ from openpilot.common.utils import sudo_write
from openpilot.common.realtime import config_realtime_process, Ratekeeper
from openpilot.common.swaglog import cloudlog
from openpilot.common.gpio import gpiochip_get_ro_value_fd, gpioevent_data
from openpilot.system.hardware import HARDWARE
from openpilot.system.sensord.sensors.i2c_sensor import Sensor
from openpilot.system.sensord.sensors.lsm6ds3_accel import LSM6DS3_Accel
from openpilot.system.sensord.sensors.lsm6ds3_gyro import LSM6DS3_Gyro
from openpilot.system.sensord.sensors.lsm6ds3_temp import LSM6DS3_Temp
from openpilot.system.sensord.sensors.mmc5603nj_magn import MMC5603NJ_Magn
I2C_BUS_IMU = 1
def interrupt_loop(sensors: list[tuple[Sensor, str, bool]], event) -> None:
pm = messaging.PubMaster([service for sensor, service, interrupt in sensors if interrupt])
# NOTE: the gyro and accelerometer share an IRQ due to the comma three
# routing only one GPIO from the LSM to the SOC, but comma 3X and four
# have two. if we want better timestamps in the future, we can use both.
# Requesting both edges as the data ready pulse from the lsm6ds sensor is
# very short (75us) and is mostly detected as falling edge instead of rising.
# So if it is detected as rising the following falling edge is skipped.
@@ -97,10 +99,6 @@ def main() -> None:
(LSM6DS3_Gyro(I2C_BUS_IMU), "gyroscope", True),
(LSM6DS3_Temp(I2C_BUS_IMU), "temperatureSensor", False),
]
if HARDWARE.get_device_type() == "tizi":
sensors_cfg.append(
(MMC5603NJ_Magn(I2C_BUS_IMU), "magnetometer", False),
)
# Reset sensors
for sensor, _, _ in sensors_cfg:
-4
View File
@@ -77,13 +77,9 @@ class LSM6DS3_Accel(Sensor):
event = log.SensorEventData.new_message()
event.timestamp = ts
event.version = 1
event.sensor = 1 # SENSOR_ACCELEROMETER
event.type = 1 # SENSOR_TYPE_ACCELEROMETER
event.source = self.source
a = event.init('acceleration')
a.v = [y, -x, z]
a.status = 1
return event
def shutdown(self) -> None:
-4
View File
@@ -73,13 +73,9 @@ class LSM6DS3_Gyro(Sensor):
event = log.SensorEventData.new_message()
event.timestamp = ts
event.version = 2
event.sensor = 5 # SENSOR_GYRO_UNCALIBRATED
event.type = 16 # SENSOR_TYPE_GYROSCOPE_UNCALIBRATED
event.source = self.source
g = event.init('gyroUncalibrated')
g.v = xyz
g.status = 1
return event
def shutdown(self) -> None:
-1
View File
@@ -23,7 +23,6 @@ class LSM6DS3_Temp(Sensor):
def get_event(self, ts: int | None = None) -> log.SensorEventData:
event = log.SensorEventData.new_message()
event.version = 1
event.timestamp = int(time.monotonic() * 1e9)
event.source = self.source
event.temperature = self._read_temperature()
-76
View File
@@ -1,76 +0,0 @@
import time
from cereal import log
from openpilot.system.sensord.sensors.i2c_sensor import Sensor
# https://www.mouser.com/datasheet/2/821/Memsic_09102019_Datasheet_Rev.B-1635324.pdf
# Register addresses
REG_ODR = 0x1A
REG_INTERNAL_0 = 0x1B
REG_INTERNAL_1 = 0x1C
# Control register settings
CMM_FREQ_EN = (1 << 7)
AUTO_SR_EN = (1 << 5)
SET = (1 << 3)
RESET = (1 << 4)
class MMC5603NJ_Magn(Sensor):
@property
def device_address(self) -> int:
return 0x30
def init(self):
self.verify_chip_id(0x39, [0x10, ])
self.writes((
(REG_ODR, 0),
# Set BW to 0b01 for 1-150 Hz operation
(REG_INTERNAL_1, 0b01),
))
def _read_data(self, cycle) -> list[float]:
# start measurement
self.write(REG_INTERNAL_0, cycle)
self.wait()
# read out XYZ
scale = 1.0 / 16384.0
b = self.read(0x00, 9)
return [
(self.parse_20bit(b[6], b[1], b[0]) * scale) - 32.0,
(self.parse_20bit(b[7], b[3], b[2]) * scale) - 32.0,
(self.parse_20bit(b[8], b[5], b[4]) * scale) - 32.0,
]
def get_event(self, ts: int | None = None) -> log.SensorEventData:
ts = time.monotonic_ns()
# SET - RESET cycle
xyz = self._read_data(SET)
reset_xyz = self._read_data(RESET)
vals = [*xyz, *reset_xyz]
event = log.SensorEventData.new_message()
event.timestamp = ts
event.version = 1
event.sensor = 3 # SENSOR_MAGNETOMETER_UNCALIBRATED
event.type = 14 # SENSOR_TYPE_MAGNETIC_FIELD_UNCALIBRATED
event.source = log.SensorEventData.SensorSource.mmc5603nj
m = event.init('magneticUncalibrated')
m.v = vals
m.status = int(all(int(v) != -32 for v in vals))
return event
def shutdown(self) -> None:
v = self.read(REG_INTERNAL_0, 1)[0]
self.writes((
# disable auto-reset of measurements
(REG_INTERNAL_0, (v & (~(CMM_FREQ_EN | AUTO_SR_EN)))),
# disable continuous mode
(REG_ODR, 0),
))
+50 -91
View File
@@ -5,44 +5,19 @@ import numpy as np
from collections import namedtuple, defaultdict
import cereal.messaging as messaging
from cereal import log
from cereal.services import SERVICE_LIST
from openpilot.common.gpio import get_irqs_for_action
from openpilot.common.timeout import Timeout
from openpilot.system.hardware import HARDWARE
from openpilot.system.manager.process_config import managed_processes
LSM = {
('lsm6ds3', 'acceleration'),
('lsm6ds3', 'gyroUncalibrated'),
('lsm6ds3', 'temperature'),
}
LSM_C = {(x[0]+'trc', x[1]) for x in LSM}
SensorConfig = namedtuple('SensorConfig', ['service', 'measurement', 'sanity_min', 'sanity_max', 'std_max'])
MMC = {
('mmc5603nj', 'magneticUncalibrated'),
}
SENSOR_CONFIGURATIONS: list[set] = {
"mici": [LSM, LSM_C],
"tizi": [MMC | LSM, MMC | LSM_C],
"tici": [LSM, LSM_C, MMC | LSM, MMC | LSM_C],
}.get(HARDWARE.get_device_type(), [])
Sensor = log.SensorEventData.SensorSource
SensorConfig = namedtuple('SensorConfig', ['type', 'sanity_min', 'sanity_max'])
ALL_SENSORS = {
Sensor.lsm6ds3trc: {
SensorConfig("acceleration", 5, 15),
SensorConfig("gyroUncalibrated", 0, .2),
SensorConfig("temperature", 10, 40), # set for max range of our office
},
Sensor.mmc5603nj: {
SensorConfig("magneticUncalibrated", 0, 300),
}
}
ALL_SENSORS[Sensor.lsm6ds3] = ALL_SENSORS[Sensor.lsm6ds3trc]
SENSOR_CONFIGS = (
SensorConfig("accelerometer", "acceleration", 5, 15, 5),
SensorConfig("gyroscope", "gyroUncalibrated", 0, .15, 0.5),
SensorConfig("temperatureSensor", "temperature", 10, 40, 0.5), # set for max range of our office
)
SENSOR_CONFIGS_BY_MEASUREMENT = {config.measurement: config for config in SENSOR_CONFIGS}
def get_irq_count(irq: int):
with open(f"/sys/kernel/irq/{irq}/per_cpu_count") as f:
@@ -50,12 +25,11 @@ def get_irq_count(irq: int):
return sum(per_cpu)
def read_sensor_events(duration_sec):
sensor_types = ['accelerometer', 'gyroscope', 'magnetometer', 'temperatureSensor',]
socks = {}
poller = messaging.Poller()
events = defaultdict(list)
for stype in sensor_types:
socks[stype] = messaging.sub_sock(stype, poller=poller, timeout=100)
for config in SENSOR_CONFIGS:
socks[config.service] = messaging.sub_sock(config.service, poller=poller, timeout=100)
# wait for sensors to come up
with Timeout(int(os.environ.get("SENSOR_WAIT", "5")), "sensors didn't come up"):
@@ -70,11 +44,15 @@ def read_sensor_events(duration_sec):
for s in socks:
events[s] += messaging.drain_sock(socks[s])
time.sleep(0.1)
assert sum(map(len, events.values())) != 0, "No sensor events collected!"
return {k: v for k, v in events.items() if len(v) > 0}
def iter_measurements(events):
for msgs in events.values():
for measurement in msgs:
yield measurement, getattr(measurement, measurement.which())
@pytest.mark.tici
class TestSensord:
@classmethod
@@ -102,31 +80,19 @@ class TestSensord:
def teardown_method(self):
managed_processes["sensord"].stop()
def test_sensors_present(self):
# verify correct sensors configuration
seen = set()
for etype in self.events:
for measurement in self.events[etype]:
m = getattr(measurement, measurement.which())
seen.add((str(m.source), m.which()))
assert seen in SENSOR_CONFIGURATIONS
def test_all_sensors_present(self):
missing = [config.service for config in SENSOR_CONFIGS if config.service not in self.events]
assert len(missing) == 0, f"missing sensors: {missing}"
def test_lsm6ds3_timing(self, subtests):
# verify measurements are sampled and published at 104Hz
sensor_t = {
1: [], # accel
5: [], # gyro
}
sensor_t = {service: [] for service in ('accelerometer', 'gyroscope')}
for measurement in self.events['accelerometer']:
m = getattr(measurement, measurement.which())
sensor_t[m.sensor].append(m.timestamp)
for measurement in self.events['gyroscope']:
m = getattr(measurement, measurement.which())
sensor_t[m.sensor].append(m.timestamp)
for service in sensor_t:
for measurement in self.events.get(service, []):
m = getattr(measurement, measurement.which())
sensor_t[service].append(m.timestamp)
for s, vals in sensor_t.items():
with subtests.test(sensor=s):
@@ -153,19 +119,16 @@ class TestSensord:
def test_logmonottime_timestamp_diff(self):
# ensure diff between the message logMonotime and sample timestamp is small
tdiffs = list()
for etype in self.events:
for measurement in self.events[etype]:
m = getattr(measurement, measurement.which())
tdiffs = []
for measurement, m in iter_measurements(self.events):
# check if gyro and accel timestamps are before logMonoTime
if str(m.source).startswith("lsm6ds3") and m.which() != 'temperature':
err_msg = f"Timestamp after logMonoTime: {m.timestamp} > {measurement.logMonoTime}"
assert m.timestamp < measurement.logMonoTime, err_msg
# check if gyro and accel timestamps are before logMonoTime
if str(m.source).startswith("lsm6ds3") and m.which() != 'temperature':
err_msg = f"Timestamp after logMonoTime: {m.timestamp} > {measurement.logMonoTime}"
assert m.timestamp < measurement.logMonoTime, err_msg
# negative values might occur, as non interrupt packages created
# before the sensor is read
tdiffs.append(abs(measurement.logMonoTime - m.timestamp) / 1e6)
# negative values might occur, as non interrupt packages created
# before the sensor is read
tdiffs.append(abs(measurement.logMonoTime - m.timestamp) / 1e6)
# some sensors have a read procedure that will introduce an expected diff on the order of 20ms
high_delay_diffs = set(filter(lambda d: d >= 25., tdiffs))
@@ -175,32 +138,29 @@ class TestSensord:
assert avg_diff < 4, f"Avg packet diff: {avg_diff:.1f}ms"
def test_sensor_values(self):
sensor_values = dict()
for etype in self.events:
for measurement in self.events[etype]:
m = getattr(measurement, measurement.which())
key = (m.source.raw, m.which())
values = getattr(m, m.which())
sensor_values = defaultdict(list)
for _, m in iter_measurements(self.events):
key = (m.source.raw, m.which())
values = getattr(m, m.which())
if hasattr(values, 'v'):
values = values.v
values = np.atleast_1d(values)
if key in sensor_values:
sensor_values[key].append(values)
else:
sensor_values[key] = [values]
if hasattr(values, 'v'):
values = values.v
sensor_values[key].append(np.atleast_1d(values))
# Sanity check sensor values
for sensor, stype in sensor_values:
for s in ALL_SENSORS[sensor]:
if s.type != stype:
continue
for (sensor, stype), values in sensor_values.items():
config = SENSOR_CONFIGS_BY_MEASUREMENT[stype]
key = (sensor, s.type)
mean_norm = np.mean(np.linalg.norm(sensor_values[key], axis=1))
err_msg = f"Sensor '{sensor} {s.type}' failed sanity checks {mean_norm} is not between {s.sanity_min} and {s.sanity_max}"
assert s.sanity_min <= mean_norm <= s.sanity_max, err_msg
if config.measurement == 'temperature':
measurement_stat = np.mean(values)
else:
measurement_stat = np.mean(np.linalg.norm(values, axis=1))
err_msg = f"Sensor '{sensor} {config.measurement}' failed sanity checks {measurement_stat} is not between {config.sanity_min} and {config.sanity_max}"
assert config.sanity_min <= measurement_stat <= config.sanity_max, err_msg
std_dev = np.std(values, axis=0)
err_msg = f"Sensor '{sensor} {config.measurement}' failed std dev test {std_dev} is not under {config.std_max}"
assert np.all(std_dev <= config.std_max), err_msg
def test_sensor_verify_no_interrupts_after_stop(self):
managed_processes["sensord"].start()
@@ -222,4 +182,3 @@ class TestSensord:
time.sleep(1)
state_two = get_irq_count(self.sensord_irq)
assert state_one == state_two, "Interrupts received after sensord stop!"
+9 -4
View File
@@ -321,8 +321,9 @@ class GuiApplication(GuiApplicationExt):
self._ffmpeg_thread = threading.Thread(target=self._ffmpeg_writer_thread, daemon=True)
self._ffmpeg_thread.start()
# OFFSCREEN disables FPS limiting for fast offline rendering (e.g. clips)
rl.set_target_fps(0 if OFFSCREEN else fps)
# four display runs slightly faster than 60 FPS, let it dictate rate so we don't drift and drop frames
vblank_control = HARDWARE.get_device_type() == 'mici'
rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps)
self._target_fps = fps
self._set_styles()
@@ -590,6 +591,8 @@ class GuiApplication(GuiApplicationExt):
self._render_profiler.enable()
while not (self._window_close_requested or rl.window_should_close()):
frame_start = time.monotonic()
if PC:
# Thread is not used on PC, need to manually add mouse events
self._mouse._handle_mouse_event()
@@ -604,7 +607,7 @@ class GuiApplication(GuiApplicationExt):
if PC:
rl.poll_input_events()
time.sleep(1 / self._target_fps)
yield False
yield False, 0.0, 0.0
continue
if self._render_texture:
@@ -626,7 +629,9 @@ class GuiApplication(GuiApplicationExt):
for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]:
widget.render(rl.Rectangle(0, 0, self.width, self.height))
yield True
frame_time = rl.get_frame_time()
cpu_time = time.monotonic() - frame_start
yield True, frame_time, cpu_time
if self._scale != 1.0:
rl.rl_pop_matrix()
+1 -1
View File
@@ -177,7 +177,7 @@ class Multilang:
self._plurals = {}
def change_language(self, language_code: str) -> None:
self._params.put("LanguageSetting", language_code)
self._params.put("LanguageSetting", language_code, block=True)
self._language = language_code
self.setup()
+22 -8
View File
@@ -14,6 +14,7 @@ MIN_DRAG_PIXELS = 12
AUTO_SCROLL_TC_SNAP = 0.025
AUTO_SCROLL_TC = 0.18
BOUNCE_RETURN_RATE = 10.0
SNAP_RATE = 6.3 # matches previous Scroller snapping. exp rate of approach to snap target, 1/s
REJECT_DECELERATION_FACTOR = 3
MAX_SPEED = 10000.0 # px/s
@@ -44,10 +45,8 @@ class ScrollState(Enum):
class GuiScrollPanel2:
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
def __init__(self, horizontal: bool = True) -> None:
self._horizontal = horizontal
self._handle_out_of_bounds = handle_out_of_bounds
self._AUTO_SCROLL_TC = AUTO_SCROLL_TC_SNAP if not self._handle_out_of_bounds else AUTO_SCROLL_TC
self._state = ScrollState.STEADY
self._offset: rl.Vector2 = rl.Vector2(0, 0)
self._initial_click_event: MouseEvent | None = None
@@ -63,7 +62,7 @@ class GuiScrollPanel2:
def enabled(self) -> bool:
return self._enabled() if callable(self._enabled) else self._enabled
def update(self, bounds: rl.Rectangle, content_size: float) -> float:
def update(self, bounds: rl.Rectangle, content_size: float, snap_target: float | None = None) -> float:
if DEBUG:
print('Old state:', self._state)
@@ -73,7 +72,7 @@ class GuiScrollPanel2:
self._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
self._previous_mouse_event = mouse_event
self._update_state(bounds_size, content_size)
self._update_state(bounds_size, content_size, snap_target)
if DEBUG:
print('Velocity:', self._velocity)
@@ -86,7 +85,7 @@ class GuiScrollPanel2:
"""Returns (max_offset, min_offset) for the given bounds and content size."""
return 0.0, min(0.0, bounds_size - content_size)
def _update_state(self, bounds_size: float, content_size: float) -> None:
def _update_state(self, bounds_size: float, content_size: float, snap_target: float | None) -> None:
"""Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity."""
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
@@ -97,8 +96,9 @@ class GuiScrollPanel2:
elif self._state == ScrollState.AUTO_SCROLL:
# simple exponential return if out of bounds
# out of bounds is handled by snapping, so skip if set
out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset
if out_of_bounds and self._handle_out_of_bounds:
if out_of_bounds and snap_target is None:
target = max_offset if self.get_offset() > max_offset else min_offset
dt = rl.get_frame_time() or 1e-6
@@ -121,9 +121,23 @@ class GuiScrollPanel2:
# Update the offset based on the current velocity
dt = rl.get_frame_time()
self.set_offset(self.get_offset() + self._velocity * dt) # Adjust the offset based on velocity
alpha = 1 - (dt / (self._AUTO_SCROLL_TC + dt))
# fast decay in snap mode so velocity yields to the snap pull instead of fighting it
auto_scroll_tc = AUTO_SCROLL_TC_SNAP if snap_target is not None else AUTO_SCROLL_TC
alpha = 1 - (dt / (auto_scroll_tc + dt))
self._velocity *= alpha
# Ease toward snap target when not in user control. Composes with velocity coast above:
# high velocity dominates initially, snap dominates as velocity decays.
if snap_target is not None and self._state not in (ScrollState.PRESSED, ScrollState.MANUAL_SCROLL):
snap_target = max(min_offset, min(max_offset, snap_target))
dist = snap_target - self.get_offset()
if abs(dist) < 1: # finished snap
self.set_offset(snap_target)
else:
dt = rl.get_frame_time() or 1e-6
factor = 1.0 - math.exp(-SNAP_RATE * dt)
self.set_offset(self.get_offset() + dist * factor)
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
content_size: float) -> None:
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
+5 -87
View File
@@ -23,7 +23,7 @@ from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_80
NM_802_11_AP_FLAGS_PRIVACY, NM_802_11_AP_FLAGS_WPS,
NM_PATH, NM_IFACE, NM_ACCESS_POINT_IFACE, NM_SETTINGS_PATH,
NM_SETTINGS_IFACE, NM_CONNECTION_IFACE, NM_DEVICE_IFACE,
NM_DEVICE_TYPE_WIFI, NM_DEVICE_TYPE_MODEM, NM_ACTIVE_CONNECTION_IFACE,
NM_DEVICE_TYPE_WIFI, NM_ACTIVE_CONNECTION_IFACE,
NM_IP4_CONFIG_IFACE, NM_PROPERTIES_IFACE, NMDeviceState, NMDeviceStateReason)
try:
@@ -207,15 +207,16 @@ class WifiManager:
def worker():
self._wait_for_wifi_device()
# TODO: wait for state thread to start before adding tethering connection, tiny race currently
self._scan_thread.start()
self._state_thread.start()
self._init_connections()
if Params is not None and self._tethering_ssid not in self._connections:
self._add_tethering_connection()
self._init_wifi_state()
self._scan_thread.start()
self._state_thread.start()
self._tethering_password = self._get_tethering_password()
cloudlog.debug("WifiManager initialized")
@@ -930,89 +931,6 @@ class WifiManager:
def __del__(self):
self.stop()
def update_gsm_settings(self, roaming: bool, apn: str, metered: bool):
"""Update GSM settings for cellular connection"""
def worker():
try:
lte_connection_path = self._get_lte_connection_path()
if not lte_connection_path:
cloudlog.warning("No LTE connection found")
return
settings = self._get_connection_settings(lte_connection_path)
if len(settings) == 0:
cloudlog.warning(f"Failed to get connection settings for {lte_connection_path}")
return
# Ensure dicts exist
if 'gsm' not in settings:
settings['gsm'] = {}
if 'connection' not in settings:
settings['connection'] = {}
changes = False
auto_config = apn == ""
if settings['gsm'].get('auto-config', ('b', False))[1] != auto_config:
cloudlog.warning(f'Changing gsm.auto-config to {auto_config}')
settings['gsm']['auto-config'] = ('b', auto_config)
changes = True
if settings['gsm'].get('apn', ('s', ''))[1] != apn:
cloudlog.warning(f'Changing gsm.apn to {apn}')
settings['gsm']['apn'] = ('s', apn)
changes = True
if settings['gsm'].get('home-only', ('b', False))[1] == roaming:
cloudlog.warning(f'Changing gsm.home-only to {not roaming}')
settings['gsm']['home-only'] = ('b', not roaming)
changes = True
# Unknown means NetworkManager decides
metered_int = int(MeteredType.UNKNOWN if metered else MeteredType.NO)
if settings['connection'].get('metered', ('i', 0))[1] != metered_int:
cloudlog.warning(f'Changing connection.metered to {metered_int}')
settings['connection']['metered'] = ('i', metered_int)
changes = True
if changes:
# Update the connection settings (temporary update)
conn_addr = DBusAddress(lte_connection_path, bus_name=NM, interface=NM_CONNECTION_IFACE)
reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'UpdateUnsaved', 'a{sa{sv}}', (settings,)))
if reply.header.message_type == MessageType.error:
cloudlog.warning(f"Failed to update GSM settings: {reply}")
return
self._activate_modem_connection(lte_connection_path)
except Exception as e:
cloudlog.exception(f"Error updating GSM settings: {e}")
threading.Thread(target=worker, daemon=True).start()
def _get_lte_connection_path(self) -> str | None:
try:
settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE)
known_connections = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'ListConnections')).body[0]
for conn_path in known_connections:
settings = self._get_connection_settings(conn_path)
if settings and settings.get('connection', {}).get('id', ('s', ''))[1] == 'lte':
return str(conn_path)
except Exception as e:
cloudlog.exception(f"Error finding LTE connection: {e}")
return None
def _activate_modem_connection(self, connection_path: str):
try:
modem_device = self._get_adapter(NM_DEVICE_TYPE_MODEM)
if modem_device and connection_path:
self._router_main.send_and_get_reply(new_method_call(self._nm, 'ActivateConnection', 'ooo', (connection_path, modem_device, "/")))
except Exception as e:
cloudlog.exception(f"Error activating modem connection: {e}")
def stop(self):
if not self._exit:
self._exit = True
+2 -2
View File
@@ -316,7 +316,7 @@ class NetworkSetupPageBase(Scroller):
def on_waiting_click():
offset = (self._wifi_button.rect.x + self._wifi_button.rect.width / 2) - (self._rect.x + self._rect.width / 2)
self._scroller.scroll_to(offset, smooth=True, block_interaction=True)
self._scroller.scroll_to(offset, smooth=True, block_interrupt=True, block_widget_interaction=True)
# trigger grow when wifi button in view
self._pending_wifi_grow_animation = True
@@ -399,7 +399,7 @@ class NetworkSetupPageBase(Scroller):
self._scroller._layout()
end_offset = -(self._scroller.content_size - self._rect.width)
remaining = self._scroller.scroll_panel.get_offset() - end_offset
self._scroller.scroll_to(remaining, smooth=True, block_interaction=True)
self._scroller.scroll_to(remaining, smooth=True, block_interrupt=True, block_widget_interaction=True)
self._pending_continue_grow_animation = True
def set_custom_software(self, custom_software: bool):
+1 -1
View File
@@ -353,7 +353,7 @@ class MiciKeyboard(Widget):
# draw black circle behind selected key
circle_alpha = int(self._selected_key_filter.x * 225)
rl.draw_circle_gradient(int(key_x + key.rect.width / 2), int(key_y + key.rect.height / 2),
rl.draw_circle_gradient(rl.Vector2(key_x + key.rect.width / 2, key_y + key.rect.height / 2),
SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK)
else:
# move other keys away from selected key a bit
+3 -13
View File
@@ -160,10 +160,6 @@ class AdvancedNetworkSettings(Widget):
self._scroller = Scroller(items, line_separator=True, spacing=0)
# Set initial config
metered = self._params.get_bool("GsmMetered")
self._wifi_manager.update_gsm_settings(roaming_enabled, self._params.get("GsmApn") or "", metered)
def _on_network_updated(self, networks: list[Network]):
self._tethering_action.set_enabled(True)
self._tethering_action.set_state(self._wifi_manager.is_tethering_active())
@@ -185,9 +181,7 @@ class AdvancedNetworkSettings(Widget):
self._wifi_manager.set_tethering_active(checked)
def _toggle_roaming(self):
roaming_state = self._roaming_action.get_state()
self._params.put_bool("GsmRoaming", roaming_state)
self._wifi_manager.update_gsm_settings(roaming_state, self._params.get("GsmApn") or "", self._params.get_bool("GsmMetered"))
self._params.put_bool("GsmRoaming", self._roaming_action.get_state(), block=True)
def _edit_apn(self):
def update_apn(result: DialogResult):
@@ -198,9 +192,7 @@ class AdvancedNetworkSettings(Widget):
if apn == "":
self._params.remove("GsmApn")
else:
self._params.put("GsmApn", apn)
self._wifi_manager.update_gsm_settings(self._params.get_bool("GsmRoaming"), apn, self._params.get_bool("GsmMetered"))
self._params.put("GsmApn", apn, block=True)
current_apn = self._params.get("GsmApn") or ""
self._keyboard.reset(min_text_size=0)
@@ -210,9 +202,7 @@ class AdvancedNetworkSettings(Widget):
gui_app.push_widget(self._keyboard)
def _toggle_cellular_metered(self):
metered = self._cellular_metered_action.get_state()
self._params.put_bool("GsmMetered", metered)
self._wifi_manager.update_gsm_settings(self._params.get_bool("GsmRoaming"), self._params.get("GsmApn") or "", metered)
self._params.put_bool("GsmMetered", self._cellular_metered_action.get_state(), block=True)
def _toggle_wifi_metered(self, metered):
metered_type = {0: MeteredType.UNKNOWN, 1: MeteredType.YES, 2: MeteredType.NO}.get(metered, MeteredType.UNKNOWN)
+19 -48
View File
@@ -75,12 +75,13 @@ class _Scroller(Widget):
self._items: list[Widget] = []
self._horizontal = horizontal
self._snap_items = snap_items
assert not self._snap_items or self._horizontal, "Snapping is only supported for horizontal scrolling"
self._spacing = spacing
self._pad = pad
self._reset_scroll_at_show = True
self._scrolling_to: tuple[float | None, bool] = (None, False) # target offset, block_interaction
self._scrolling_to: tuple[float | None, bool, bool] = (None, False, False) # target offset, block_interrupt, block_widget_interaction
self._scrolling_to_filter = FirstOrderFilter(0.0, SCROLL_RC, 1 / gui_app.target_fps)
self._zoom_filter = FirstOrderFilter(1.0, 0.2, 1 / gui_app.target_fps)
self._zoom_out_t: float = 0.0
@@ -92,10 +93,7 @@ class _Scroller(Widget):
self._item_pos_filter = BounceFilter(0.0, 0.05, 1 / gui_app.target_fps)
# when not pressed, snap to closest item to be center
self._scroll_snap_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps)
self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items)
self.scroll_panel = GuiScrollPanel2(self._horizontal)
self._scroll_enabled: bool | Callable[[], bool] = True
self._show_scroll_indicator = scroll_indicator and self._horizontal
@@ -116,8 +114,8 @@ class _Scroller(Widget):
def set_reset_scroll_at_show(self, scroll: bool):
self._reset_scroll_at_show = scroll
def scroll_to(self, pos: float, smooth: bool = False, block_interaction: bool = False):
assert not block_interaction or smooth, "Instant scroll cannot block user interaction"
def scroll_to(self, pos: float, smooth: bool = False, block_interrupt: bool = False, block_widget_interaction: bool = False):
assert smooth or (not block_interrupt and not block_widget_interaction), "Instant scroll cannot block interaction"
# already there
if abs(pos) < 1:
@@ -127,7 +125,7 @@ class _Scroller(Widget):
scroll_offset = self.scroll_panel.get_offset() - pos
if smooth:
self._scrolling_to_filter.x = self.scroll_panel.get_offset()
self._scrolling_to = scroll_offset, block_interaction
self._scrolling_to = scroll_offset, block_interrupt, block_widget_interaction
else:
self.scroll_panel.set_offset(scroll_offset)
@@ -148,7 +146,7 @@ class _Scroller(Widget):
# preserve original touch valid callback
original_touch_valid_callback = item._touch_valid_callback
item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and self._scrolling_to[0] is None
item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and not self._scrolling_to[2]
and not self.moving_items and (original_touch_valid_callback() if
original_touch_valid_callback else True))
@@ -175,56 +173,29 @@ class _Scroller(Widget):
# Cancel auto-scroll if user starts manually scrolling (unless block_interaction)
if (self.scroll_panel.state in (ScrollState.PRESSED, ScrollState.MANUAL_SCROLL) and
self._scrolling_to[0] is not None and not self._scrolling_to[1]):
self._scrolling_to = None, False
self._scrolling_to = None, False, False
if self._scrolling_to[0] is not None and len(self._pending_lift) == 0:
self._scrolling_to_filter.update(self._scrolling_to[0])
self.scroll_panel.set_offset(self._scrolling_to_filter.x)
if abs(self._scrolling_to_filter.x - self._scrolling_to[0]) < 1:
if abs(self._scrolling_to_filter.x - self._scrolling_to[0]) < 1: # finished scroll
self.scroll_panel.set_offset(self._scrolling_to[0])
self._scrolling_to = None, False
self._scrolling_to = None, False, False
def _get_scroll(self, visible_items: list[Widget], content_size: float) -> float:
scroll_enabled = self._scroll_enabled() if callable(self._scroll_enabled) else self._scroll_enabled
self.scroll_panel.set_enabled(scroll_enabled and self.enabled and not self._scrolling_to[1])
self.scroll_panel.update(self._rect, content_size)
if not self._snap_items:
return self.scroll_panel.get_offset()
# Snap closest item to center
center_pos = self._rect.x + self._rect.width / 2 if self._horizontal else self._rect.y + self._rect.height / 2
closest_delta_pos = float('inf')
scroll_snap_idx: int | None = None
for idx, item in enumerate(visible_items):
if self._horizontal:
delta_pos = (item.rect.x + item.rect.width / 2) - center_pos
else:
delta_pos = (item.rect.y + item.rect.height / 2) - center_pos
if abs(delta_pos) < abs(closest_delta_pos):
closest_delta_pos = delta_pos
scroll_snap_idx = idx
# Snap closest item to center. Skipped while scroll_to() is animating
snap_target: float | None = None
if self._snap_items and visible_items and self._scrolling_to[0] is None:
# TODO: this doesn't handle two small buttons at the edges well
center_pos = self._rect.x + self._rect.width / 2
closest_delta_pos = min((((item.rect.x + item.rect.width / 2) - center_pos) for item in visible_items), key=abs)
snap_target = self.scroll_panel.get_offset() - closest_delta_pos
if scroll_snap_idx is not None:
snap_item = visible_items[scroll_snap_idx]
if self.is_pressed:
# no snapping until released
self._scroll_snap_filter.x = 0
else:
# TODO: this doesn't handle two small buttons at the edges well
if self._horizontal:
snap_delta_pos = (center_pos - (snap_item.rect.x + snap_item.rect.width / 2)) / 10
snap_delta_pos = min(snap_delta_pos, -self.scroll_panel.get_offset() / 10)
snap_delta_pos = max(snap_delta_pos, (self._rect.width - self.scroll_panel.get_offset() - content_size) / 10)
else:
snap_delta_pos = (center_pos - (snap_item.rect.y + snap_item.rect.height / 2)) / 10
snap_delta_pos = min(snap_delta_pos, -self.scroll_panel.get_offset() / 10)
snap_delta_pos = max(snap_delta_pos, (self._rect.height - self.scroll_panel.get_offset() - content_size) / 10)
self._scroll_snap_filter.update(snap_delta_pos)
self.scroll_panel.set_offset(self.scroll_panel.get_offset() + self._scroll_snap_filter.x)
return self.scroll_panel.get_offset()
return self.scroll_panel.update(self._rect, content_size, snap_target=snap_target)
@property
def moving_items(self) -> bool:
@@ -409,7 +380,7 @@ class _Scroller(Widget):
self._move_lift.clear()
self._pending_lift.clear()
self._pending_move.clear()
self._scrolling_to = None, False
self._scrolling_to = None, False, False
self._scrolling_to_filter.x = 0.0
def hide_event(self):
+1 -1
View File
@@ -86,7 +86,7 @@ class TestBaseUpdate:
mocker.patch("openpilot.common.basedir.BASEDIR", self.basedir)
def set_target_branch(self, branch):
self.params.put("UpdaterTargetBranch", branch)
self.params.put("UpdaterTargetBranch", branch, block=True)
def setup_basedir_release(self, release):
self.params = Params()
+1 -1
View File
@@ -28,7 +28,7 @@ def test_target_branch_migration_from_current_branch(mocker, device_type, branch
])
def test_target_branch_migration_from_param(mocker, device_type, branch, expected):
params = Params()
params.put("UpdaterTargetBranch", branch)
params.put("UpdaterTargetBranch", branch, block=True)
mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
+23 -23
View File
@@ -65,7 +65,7 @@ class WaitTimeHelper:
def write_time_to_param(params, param) -> None:
t = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
params.put(param, t)
params.put(param, t, block=True)
def run(cmd: list[str], cwd: str | None = None) -> str:
return subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8')
@@ -138,7 +138,7 @@ def init_overlay() -> None:
cloudlog.info("preparing new safe staging area")
params = Params()
params.put_bool("UpdateAvailable", False)
params.put_bool("UpdateAvailable", False, block=True)
set_consistent_flag(False)
dismount_overlay()
run(["sudo", "rm", "-rf", STAGING_ROOT])
@@ -169,7 +169,7 @@ def init_overlay() -> None:
run(["sudo", "chmod", "755", os.path.join(OVERLAY_METADATA, "work")])
git_diff = run(["git", "diff", "--submodule=diff"], OVERLAY_MERGED)
params.put("GitDiff", git_diff)
params.put("GitDiff", git_diff, block=True)
cloudlog.info(f"git diff output:\n{git_diff}")
@@ -260,19 +260,19 @@ class Updater:
return run(["git", "rev-parse", "HEAD"], path).rstrip()
def set_params(self, update_success: bool, failed_count: int, exception: str | None) -> None:
self.params.put("UpdateFailedCount", failed_count)
self.params.put("UpdaterTargetBranch", self.target_branch)
self.params.put("UpdateFailedCount", failed_count, block=True)
self.params.put("UpdaterTargetBranch", self.target_branch, block=True)
self.params.put_bool("UpdaterFetchAvailable", self.update_available)
self.params.put_bool("UpdaterFetchAvailable", self.update_available, block=True)
if len(self.branches):
self.params.put("UpdaterAvailableBranches", ','.join(self.branches.keys()))
self.params.put("UpdaterAvailableBranches", ','.join(self.branches.keys()), block=True)
last_uptime_onroad = self.params.get("UptimeOnroad", return_default=True)
last_route_count = self.params.get("RouteCount", return_default=True)
if update_success:
self.params.put("LastUpdateTime", datetime.datetime.now(datetime.UTC).replace(tzinfo=None))
self.params.put("LastUpdateUptimeOnroad", last_uptime_onroad)
self.params.put("LastUpdateRouteCount", last_route_count)
self.params.put("LastUpdateTime", datetime.datetime.now(datetime.UTC).replace(tzinfo=None), block=True)
self.params.put("LastUpdateUptimeOnroad", last_uptime_onroad, block=True)
self.params.put("LastUpdateRouteCount", last_route_count, block=True)
else:
last_uptime_onroad = self.params.get("LastUpdateUptimeOnroad", return_default=True)
last_route_count = self.params.get("LastUpdateRouteCount", return_default=True)
@@ -280,7 +280,7 @@ class Updater:
if exception is None:
self.params.remove("LastUpdateException")
else:
self.params.put("LastUpdateException", exception)
self.params.put("LastUpdateException", exception, block=True)
# Write out current and new version info
def get_description(basedir: str) -> str:
@@ -303,11 +303,11 @@ class Updater:
except Exception:
cloudlog.exception("updater.get_description")
return f"{version} / {branch} / {commit} / {commit_date}"
self.params.put("UpdaterCurrentDescription", get_description(BASEDIR))
self.params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
self.params.put("UpdaterNewDescription", get_description(FINALIZED))
self.params.put("UpdaterNewReleaseNotes", parse_release_notes(FINALIZED))
self.params.put_bool("UpdateAvailable", self.update_ready)
self.params.put("UpdaterCurrentDescription", get_description(BASEDIR), block=True)
self.params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR), block=True)
self.params.put("UpdaterNewDescription", get_description(FINALIZED), block=True)
self.params.put("UpdaterNewReleaseNotes", parse_release_notes(FINALIZED), block=True)
self.params.put_bool("UpdateAvailable", self.update_ready, block=True)
# Handle user prompt
for alert in ("Offroad_UpdateFailed", "Offroad_ConnectivityNeeded", "Offroad_ConnectivityNeededPrompt"):
@@ -362,11 +362,11 @@ class Updater:
def fetch_update(self) -> None:
cloudlog.info("attempting git fetch inside staging overlay")
self.params.put("UpdaterState", "downloading...")
self.params.put("UpdaterState", "downloading...", block=True)
# TODO: cleanly interrupt this and invalidate old update
set_consistent_flag(False)
self.params.put_bool("UpdateAvailable", False)
self.params.put_bool("UpdateAvailable", False, block=True)
setup_git_options(OVERLAY_MERGED)
@@ -394,7 +394,7 @@ class Updater:
handle_agnos_update()
# Create the finalized, ready-to-swap update
self.params.put("UpdaterState", "finalizing update...")
self.params.put("UpdaterState", "finalizing update...", block=True)
finalize_update()
cloudlog.info("finalize success!")
@@ -423,7 +423,7 @@ def main() -> None:
if not params.get("InstallDate"):
t = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
params.put("InstallDate", t)
params.put("InstallDate", t, block=True)
updater = Updater()
update_failed_count = 0 # TODO: Load from param?
@@ -433,7 +433,7 @@ def main() -> None:
set_consistent_flag(False)
# set initial state
params.put("UpdaterState", "idle")
params.put("UpdaterState", "idle", block=True)
# Run the update loop
first_run = True
@@ -457,7 +457,7 @@ def main() -> None:
update_failed_count += 1
# check for update
params.put("UpdaterState", "checking...")
params.put("UpdaterState", "checking...", block=True)
updater.check_for_update()
# download update
@@ -487,7 +487,7 @@ def main() -> None:
OVERLAY_INIT.unlink(missing_ok=True)
try:
params.put("UpdaterState", "idle")
params.put("UpdaterState", "idle", block=True)
update_successful = (update_failed_count == 0)
updater.set_params(update_successful, update_failed_count, exception)
except Exception:
+31 -4
View File
@@ -1,4 +1,5 @@
import asyncio
import struct
import time
import av
@@ -7,6 +8,13 @@ from teleoprtc.tracks import TiciVideoStreamTrack
from cereal import messaging
from openpilot.common.realtime import DT_MDL, DT_DMON
# arbitrary 16-byte UUID identifying openpilot frame-timing SEI messages
TIMING_SEI_UUID = bytes([
0xa5, 0xe0, 0xc4, 0xa4, 0x5b, 0x6e, 0x4e, 0x1e,
0x9c, 0x7e, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc,
])
_SEI_PREFIX = b'\x00\x00\x00\x01\x06\x05\x30' + TIMING_SEI_UUID
class LiveStreamVideoStreamTrack(TiciVideoStreamTrack):
camera_to_sock_mapping = {
@@ -19,9 +27,30 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack):
dt = DT_DMON if camera_type == "driver" else DT_MDL
super().__init__(camera_type, dt)
self._sock = messaging.sub_sock(self.camera_to_sock_mapping[camera_type], conflate=True)
self._sock = self._make_sock(camera_type)
self._pts = 0
self._t0_ns = time.monotonic_ns()
self.timing_sei_enabled = False
def _make_sock(self, camera_type: str) -> messaging.SubSocket:
return messaging.sub_sock(self.camera_to_sock_mapping[camera_type], conflate=True)
def switch_camera(self, camera_type: str) -> None:
self._sock = self._make_sock(camera_type)
def _build_frame_data(self, msg) -> bytes:
encode_data = getattr(msg, msg.which())
if not self.timing_sei_enabled:
return encode_data.header + encode_data.data
idx = encode_data.idx
sei_nal = _SEI_PREFIX + struct.pack('>4d',
(idx.timestampEof - idx.timestampSof) / 1e6,
(msg.logMonoTime - idx.timestampEof) / 1e6,
(time.monotonic_ns() - msg.logMonoTime) / 1e6,
time.time() * 1000, # noqa: TID251
) + b'\x80'
return encode_data.header + sei_nal + encode_data.data
async def recv(self):
while True:
@@ -30,9 +59,7 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack):
break
await asyncio.sleep(0.005)
evta = getattr(msg, msg.which())
packet = av.Packet(evta.header + evta.data)
packet = av.Packet(self._build_frame_data(msg))
packet.time_base = self._time_base
self._pts = ((time.monotonic_ns() - self._t0_ns) * self._clock_rate) // 1_000_000_000
+204 -62
View File
@@ -1,7 +1,11 @@
#!/usr/bin/env python3
from abc import abstractmethod
import os
import time
import argparse
import asyncio
import contextlib
import json
import uuid
import logging
@@ -19,11 +23,39 @@ if TYPE_CHECKING:
from aiortc.rtcdatachannel import RTCDataChannel
from openpilot.system.webrtc.schema import generate_field
from openpilot.common.params import Params
from cereal import messaging, log
class CerealOutgoingMessageProxy:
class AsyncTaskRunner:
def __init__(self):
self.is_running = False
self.task = None
self.logger = logging.getLogger("webrtcd")
def start(self):
assert self.task is None
self.task = asyncio.create_task(self.run())
async def stop(self):
if self.task is None:
return
task = self.task
self.task = None
if task.done():
return
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
@abstractmethod
async def run(self):
pass
class CerealOutgoingMessageProxy(AsyncTaskRunner):
def __init__(self, sm: messaging.SubMaster):
super().__init__()
self.sm = sm
self.channels: list[RTCDataChannel] = []
@@ -55,6 +87,19 @@ class CerealOutgoingMessageProxy:
for channel in self.channels:
channel.send(encoded_msg)
async def run(self):
from aiortc.exceptions import InvalidStateError
while True:
try:
self.update()
except InvalidStateError:
self.logger.warning("Cereal outgoing proxy invalid state (connection closed)")
break
except Exception:
self.logger.exception("Cereal outgoing proxy failure")
await asyncio.sleep(0.01)
class CerealIncomingMessageProxy:
def __init__(self, pm: messaging.PubMaster):
@@ -72,37 +117,6 @@ class CerealIncomingMessageProxy:
self.pm.send(msg_type, msg)
class CerealProxyRunner:
def __init__(self, proxy: CerealOutgoingMessageProxy):
self.proxy = proxy
self.is_running = False
self.task = None
self.logger = logging.getLogger("webrtcd")
def start(self):
assert self.task is None
self.task = asyncio.create_task(self.run())
def stop(self):
if self.task is None or self.task.done():
return
self.task.cancel()
self.task = None
async def run(self):
from aiortc.exceptions import InvalidStateError
while True:
try:
self.proxy.update()
except InvalidStateError:
self.logger.warning("Cereal outgoing proxy invalid state (connection closed)")
break
except Exception:
self.logger.exception("Cereal outgoing proxy failure")
await asyncio.sleep(0.01)
class DynamicPubMaster(messaging.PubMaster):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -115,21 +129,90 @@ class DynamicPubMaster(messaging.PubMaster):
self.sock[service] = messaging.pub_sock(service)
class LivestreamBitrateController(AsyncTaskRunner):
bitrates = [500_000, 1_500_000, int(os.environ.get("STREAM_BITRATE", 5_000_000))]
label_to_bitrate = { "high": bitrates[2], "med": bitrates[1], "low": bitrates[0]}
sample_interval = 0.2
high_level = 0.1 # drop immediately
med_level = 0.05 # drop after # of samples
low_level = 0 # raise after # of samples
down_samples = 5 # 1s
param_name = "LivestreamEncoderBitrate"
def __init__(self, peer_connection: Any):
super().__init__()
self.pc = peer_connection
self.params = Params()
self.level = 0
self.prev_lost, self.prev_sent = None, None
self.counter = 0
self.up_samples = 5 # 1s
self._auto = True
async def run(self):
while True:
await asyncio.sleep(self.sample_interval)
if not self._auto:
continue
loss_rate = await self._sample()
if loss_rate is None:
continue
if loss_rate >= self.med_level and self.level > 0:
self.counter += 1
if self.counter >= self.down_samples or loss_rate >= self.high_level:
self.level -= 1
self.up_samples *= 2 # exponential backoff before raising again
self.counter = 0
self._publish(self.bitrates[self.level])
elif loss_rate <= self.low_level and self.level < len(self.bitrates) - 1:
self.counter -= 1
if -self.counter >= self.up_samples:
self.level += 1
self.counter = 0
self._publish(self.bitrates[self.level])
async def _sample(self) -> float | None:
report = await self.pc.getStats()
packets_lost = packets_sent = 0
for s in report.values():
if s.type == "remote-inbound-rtp":
packets_lost += s.packetsLost
elif s.type == "outbound-rtp":
packets_sent += s.packetsSent
if self.prev_lost is None:
self.prev_lost, self.prev_sent = packets_lost, packets_sent
return None
lost_delta = max(0, packets_lost - self.prev_lost)
sent_delta = max(0, packets_sent - self.prev_sent)
self.prev_lost, self.prev_sent = packets_lost, packets_sent
return lost_delta / sent_delta if sent_delta else 0.0
def _publish(self, bitrate: float):
self.params.put(self.param_name, bitrate)
def set_quality(self, quality):
if quality in self.label_to_bitrate:
self._publish(self.label_to_bitrate[quality])
self._auto = False
elif quality == "auto":
self._auto = True
class StreamSession:
shared_pub_master = DynamicPubMaster([])
def __init__(self, sdp: str, cameras: list[str], incoming_services: list[str], outgoing_services: list[str], debug_mode: bool = False):
def __init__(self, sdp: str, init_camera: str, incoming_services: list[str], outgoing_services: list[str], debug_mode: bool = False):
from aiortc.mediastreams import VideoStreamTrack
from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack
from teleoprtc import WebRTCAnswerBuilder
from teleoprtc.info import parse_info_from_offer
config = parse_info_from_offer(sdp)
builder = WebRTCAnswerBuilder(sdp)
assert len(cameras) == config.n_expected_camera_tracks, "Incoming stream has misconfigured number of video tracks"
for cam in cameras:
builder.add_video_stream(cam, LiveStreamVideoStreamTrack(cam) if not debug_mode else VideoStreamTrack())
self.video_track = LiveStreamVideoStreamTrack(init_camera) if not debug_mode else VideoStreamTrack()
builder.add_video_stream(init_camera, self.video_track)
self.stream = builder.stream()
self.identifier = str(uuid.uuid4())
@@ -137,35 +220,60 @@ class StreamSession:
self.incoming_bridge: CerealIncomingMessageProxy | None = None
self.incoming_bridge_services = incoming_services
self.outgoing_bridge: CerealOutgoingMessageProxy | None = None
self.outgoing_bridge_runner: CerealProxyRunner | None = None
self.bitrate_controller: LivestreamBitrateController | None = None
if len(incoming_services) > 0:
self.incoming_bridge = CerealIncomingMessageProxy(self.shared_pub_master)
if len(outgoing_services) > 0:
self.outgoing_bridge = CerealOutgoingMessageProxy(messaging.SubMaster(outgoing_services))
self.outgoing_bridge_runner = CerealProxyRunner(self.outgoing_bridge)
self.bitrate_controller = LivestreamBitrateController(self.stream.peer_connection)
self.run_task: asyncio.Task | None = None
self._cleanup_lock = asyncio.Lock()
self._cleanup_done = False
self.logger = logging.getLogger("webrtcd")
self.logger.info("New stream session (%s), cameras %s, incoming services %s, outgoing services %s",
self.identifier, cameras, incoming_services, outgoing_services)
self.logger.info(
"New stream session (%s), init camera %s, incoming services %s, outgoing services %s",
self.identifier, init_camera, incoming_services, outgoing_services,
)
def start(self):
self.run_task = asyncio.create_task(self.run())
def stop(self):
if self.run_task.done():
return
self.run_task.cancel()
async def stop(self):
if self.run_task is not None and not self.run_task.done() and self.run_task is not asyncio.current_task():
self.run_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self.run_task
self.run_task = None
asyncio.run(self.post_run_cleanup())
await self.post_run_cleanup()
async def get_answer(self):
return await self.stream.start()
async def message_handler(self, message: bytes):
def message_handler(self, message: bytes):
assert self.incoming_bridge is not None
try:
self.incoming_bridge.send(message)
payload = json.loads(message) if isinstance(message, (bytes, str)) else None
if isinstance(payload, dict):
msg_type = payload.get("type")
match msg_type:
case "livestreamCameraSwitch":
self.video_track.switch_camera(payload["data"]["camera"])
case "livestreamSettings":
self.bitrate_controller.set_quality(payload["data"]["quality"])
case "clockSync":
pong = json.dumps({"type": "clockSync", "data": {
"action": "pong", "browserSendTime": payload["data"]["browserSendTime"], "deviceTime": time.time() * 1000, # noqa: TID251
}})
self.stream.get_messaging_channel().send(pong)
case "enableTimingSei":
if hasattr(self.video_track, 'timing_sei_enabled'):
self.video_track.timing_sei_enabled = bool(payload["data"]["enabled"])
case _:
if payload.get("type") not in self.incoming_bridge_services:
return
self.incoming_bridge.send(message)
except Exception:
self.logger.exception("Cereal incoming proxy failure")
@@ -176,29 +284,38 @@ class StreamSession:
if self.incoming_bridge is not None:
await self.shared_pub_master.add_services_if_needed(self.incoming_bridge_services)
self.stream.set_message_handler(self.message_handler)
if self.outgoing_bridge_runner is not None:
if self.outgoing_bridge is not None:
channel = self.stream.get_messaging_channel()
self.outgoing_bridge_runner.proxy.add_channel(channel)
self.outgoing_bridge_runner.start()
self.outgoing_bridge.add_channel(channel)
self.outgoing_bridge.start()
self.bitrate_controller.start()
self.logger.info("Stream session (%s) connected", self.identifier)
await self.stream.wait_for_disconnection()
await self.post_run_cleanup()
self.logger.info("Stream session (%s) ended", self.identifier)
except Exception:
self.logger.exception("Stream session failure")
finally:
await self.post_run_cleanup()
async def post_run_cleanup(self):
await self.stream.stop()
if self.outgoing_bridge is not None:
self.outgoing_bridge_runner.stop()
async with self._cleanup_lock:
if self._cleanup_done:
return
self._cleanup_done = True
if self.bitrate_controller is not None:
await self.bitrate_controller.stop()
if self.outgoing_bridge is not None:
await self.outgoing_bridge.stop()
await self.stream.stop()
@dataclass
class StreamRequestBody:
sdp: str
cameras: list[str]
initCamera: str
bridge_services_in: list[str] = field(default_factory=list)
bridge_services_out: list[str] = field(default_factory=list)
@@ -208,11 +325,33 @@ async def get_stream(request: 'web.Request'):
raw_body = await request.json()
body = StreamRequestBody(**raw_body)
session = StreamSession(body.sdp, body.cameras, body.bridge_services_in, body.bridge_services_out, debug_mode)
answer = await session.get_answer()
session.start()
async with request.app['stream_lock']:
# Fully disconnect any other active stream before starting the replacement.
for sid, s in list(stream_dict.items()):
if s.run_task and not s.run_task.done():
try:
ch = s.stream.get_messaging_channel()
ch.send(json.dumps({"type": "connectionReplaced", "data": "Another device has connected, closing this session."}))
except Exception:
pass
await s.stop()
del stream_dict[sid]
stream_dict[session.identifier] = session
session = StreamSession(body.sdp, body.initCamera, body.bridge_services_in, body.bridge_services_out, debug_mode)
try:
answer = await session.get_answer()
except ValueError as e:
await session.stop()
raise web.HTTPBadRequest(
text=json.dumps({"error": "invalid_sdp", "message": str(e)}),
content_type="application/json",
) from e
except Exception:
await session.stop()
raise
session.start()
stream_dict[session.identifier] = session
return web.json_response({"sdp": answer.sdp, "type": answer.type})
@@ -224,6 +363,7 @@ async def get_schema(request: 'web.Request'):
schema_dict = {s: generate_field(log.Event.schema.fields[s]) for s in services}
return web.json_response(schema_dict)
async def post_notify(request: 'web.Request'):
try:
payload = await request.json()
@@ -239,9 +379,10 @@ async def post_notify(request: 'web.Request'):
return web.Response(status=200, text="OK")
async def on_shutdown(app: 'web.Application'):
for session in app['streams'].values():
session.stop()
await session.stop()
del app['streams']
@@ -254,6 +395,7 @@ def webrtcd_thread(host: str, port: int, debug: bool):
app = web.Application()
app['streams'] = dict()
app['stream_lock'] = asyncio.Lock()
app['debug'] = debug
app.on_shutdown.append(on_shutdown)
app.router.add_post("/stream", get_stream)