mirror of
https://github.com/infiniteCable2/openpilot.git
synced 2026-09-30 11:33:41 +08:00
Merge branch 'upstream/master' into sync-20260517-new-new
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -12,8 +12,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "media/cam_sensor_cmn_header.h"
|
||||
|
||||
#include "common/params.h"
|
||||
#include "common/swaglog.h"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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]
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Executable
+587
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3a94ab8395f20d20a9d5a2a2bacca0694f072df8421cf13adca6250d28065bdc
|
||||
size 24709205
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eba5f44e6a763e1f74d1c718993218adcc72cba4caafe99b595fa701151a4c54
|
||||
size 10448792
|
||||
@@ -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,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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
@@ -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!"
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user