mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
353 lines
15 KiB
Python
353 lines
15 KiB
Python
"""Opt-in onroad camera observation producer; no Params mailbox or control writes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import os
|
|
import time
|
|
import uuid
|
|
|
|
import numpy as np
|
|
|
|
from openpilot.common.constants import CV
|
|
from openpilot.starpilot.speed_limits.vision.model import VisionModelCore, cv2
|
|
from openpilot.starpilot.speed_limits.vision.observation import MODEL_ID, MAX_FRAME_AGE_NS, MAX_RESULT_AGE_NS, MAX_PAIR_SKEW_NS, clock_pair_ns
|
|
from openpilot.starpilot.spot_monitor.preferences import enabled as vasm_enabled
|
|
from openpilot.starpilot.vision_cpu import LITTLE_CORES, VISION_WITH_VASM
|
|
|
|
UNAVAILABLE_RETRY_SECONDS = 1.0
|
|
INFERENCE_INTERVAL_NS = 166_666_667
|
|
FOLLOWUP_INFERENCE_INTERVAL_NS = 100_000_000
|
|
FOLLOWUP_WINDOW_NS = 2_000_000_000
|
|
# Retain a cooldown even when processing reaches the result-age budget.
|
|
BUSY_INFERENCE_INTERVAL_NS = MAX_RESULT_AGE_NS * 3 // 2
|
|
|
|
|
|
def _inference_interval_ns(processing_interval_ns: int, last_detection_ns: int, now_ns: int) -> int:
|
|
# Original Dom follow-up after a usable candidate, still bounded by measured
|
|
# processing cost. Holding a published sign alone does not renew this window.
|
|
followup = last_detection_ns > 0 and 0 <= now_ns - last_detection_ns < FOLLOWUP_WINDOW_NS
|
|
return max(processing_interval_ns, FOLLOWUP_INFERENCE_INTERVAL_NS if followup else INFERENCE_INTERVAL_NS)
|
|
|
|
|
|
def _configure_process() -> None:
|
|
from openpilot.common.hardware import PC
|
|
from openpilot.common.realtime import drop_realtime, set_core_affinity
|
|
|
|
if PC:
|
|
return
|
|
for configure in (drop_realtime, lambda: set_core_affinity(list(LITTLE_CORES))):
|
|
try:
|
|
configure()
|
|
except OSError:
|
|
pass
|
|
try:
|
|
os.nice(max(0, 10 - os.getpriority(os.PRIO_PROCESS, 0)))
|
|
except (AttributeError, OSError):
|
|
pass
|
|
|
|
|
|
def _place_inference(params) -> None:
|
|
from openpilot.common.realtime import set_core_affinity
|
|
try:
|
|
set_core_affinity(list(VISION_WITH_VASM if vasm_enabled(params, development=True) else LITTLE_CORES))
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _driving_model_ready(sm, now_ns: int) -> bool:
|
|
service = 'drivingModelData'
|
|
if not sm.seen[service] or not sm.valid[service]:
|
|
return False
|
|
stamp_ns = sm.logMonoTime[service]
|
|
drop = float(sm[service].frameDropPerc)
|
|
return (stamp_ns > 0 and 0 <= now_ns - stamp_ns <= MAX_FRAME_AGE_NS and
|
|
math.isfinite(drop) and 0 <= drop <= 1)
|
|
|
|
|
|
def _camera_buffer(client):
|
|
buffer = client.recv(100)
|
|
if buffer is None:
|
|
return None
|
|
eof_boot_ns, frame_id = int(client.timestamp_eof), int(client.frame_id)
|
|
stride, width, height = int(client.stride), int(client.width), int(client.height)
|
|
uv_offset = int(client.uv_offset)
|
|
if (stride < width or width <= 0 or height <= 0 or width % 2 or height % 2 or
|
|
uv_offset < stride * height or uv_offset % stride):
|
|
return None
|
|
return buffer, eof_boot_ns, frame_id, stride, width, height, uv_offset
|
|
|
|
|
|
def _convert_camera_buffer(info) -> tuple[np.ndarray, int, int] | None:
|
|
buffer, eof_boot_ns, frame_id, stride, width, height, uv_offset = info
|
|
# VisionBuf borrows client-owned shared memory. Copy before another receive
|
|
# or reconnect; only the owned array is used for conversion and inference.
|
|
raw = np.frombuffer(buffer.data, dtype=np.uint8).copy()
|
|
required = uv_offset + stride * (height // 2)
|
|
if raw.size < required:
|
|
return None
|
|
y = raw[:stride * height].reshape((height, stride))[:, :width]
|
|
uv = raw[uv_offset:required].reshape((height // 2, stride))[:, :width]
|
|
nv12 = np.concatenate((y, uv), axis=0)
|
|
frame = cv2.cvtColor(nv12, cv2.COLOR_YUV2BGR_NV12)
|
|
return frame, eof_boot_ns, frame_id
|
|
|
|
|
|
def _camera_frame(client) -> tuple[np.ndarray, int, int] | None:
|
|
info = _camera_buffer(client)
|
|
return _convert_camera_buffer(info) if info is not None else None
|
|
|
|
|
|
def _send(pm, *, session: str, status: str, stream: str, observed_ns: int = 0,
|
|
eof_boot_ns: int = 0, frame_id: int = 0, result: tuple[int, float, int, int] | None = None,
|
|
valid_until_ns: int | None = None, is_metric: bool = False) -> None:
|
|
from openpilot.cereal import messaging
|
|
|
|
msg = messaging.new_message('slcVisionObservation', valid=True)
|
|
event = msg.slcVisionObservation.init('vision')
|
|
event.producerSessionId = session
|
|
event.status = status
|
|
event.modelId = MODEL_ID
|
|
event.stream = stream
|
|
event.observedMonoTime = observed_ns
|
|
event.cameraFrameEofBootTime = eof_boot_ns
|
|
event.frameId = frame_id
|
|
if result is not None:
|
|
speed, confidence, support, episode = result
|
|
event.speedMps = speed * (CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS)
|
|
event.confidence = confidence
|
|
event.supportCount = support
|
|
event.episode = episode
|
|
event.validUntilMonoTime = observed_ns + MAX_RESULT_AGE_NS if valid_until_ns is None else valid_until_ns
|
|
pm.send('slcVisionObservation', msg)
|
|
|
|
|
|
def main() -> None:
|
|
import openpilot.cereal.messaging as messaging
|
|
from openpilot.cereal.visionipc import VisionStreamType
|
|
from msgq.visionipc import VisionIpcClient
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.swaglog import cloudlog
|
|
|
|
_configure_process()
|
|
params = Params()
|
|
pm = messaging.PubMaster(['slcVisionObservation'])
|
|
sm = messaging.SubMaster(['drivingModelData'])
|
|
session = uuid.uuid4().hex
|
|
stream = 'unknown'
|
|
client = None
|
|
core = None
|
|
last_unavailable = 0.0
|
|
try:
|
|
if cv2 is not None:
|
|
cv2.setNumThreads(1)
|
|
is_metric = params.get_bool('IsMetric')
|
|
core = VisionModelCore(is_metric=is_metric)
|
|
except Exception as error:
|
|
cloudlog.error('vision SLC model unavailable: %s', error)
|
|
last_inference_ns = 0
|
|
last_publish_ns = 0
|
|
last_result = None
|
|
last_result_session = ''
|
|
last_result_observed_ns = 0
|
|
last_result_valid_until_ns = 0
|
|
processing_interval_ns = 0
|
|
last_placement_check = -2.0
|
|
last_clock_offset_ns = None
|
|
model_paused = False
|
|
while True:
|
|
now = time.monotonic()
|
|
if now - last_placement_check >= 2.0:
|
|
_place_inference(params)
|
|
last_placement_check = now
|
|
if core is None:
|
|
now = time.monotonic()
|
|
if now - last_unavailable >= UNAVAILABLE_RETRY_SECONDS:
|
|
_send(pm, session=session, status='unavailable', stream=stream)
|
|
last_unavailable = now
|
|
time.sleep(0.1)
|
|
continue
|
|
try:
|
|
sm.update(0)
|
|
current_metric = params.get_bool('IsMetric')
|
|
if current_metric != is_metric:
|
|
# A retained numeral and its supporting reads belong to the old units.
|
|
is_metric = core.is_metric = current_metric
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
last_result = None
|
|
_send(pm, session=session, status='unknown', stream=stream)
|
|
available = VisionIpcClient.available_streams('camerad', block=False)
|
|
wanted = (VisionStreamType.VISION_STREAM_NARROW_ROAD if VisionStreamType.VISION_STREAM_NARROW_ROAD in available else
|
|
VisionStreamType.VISION_STREAM_WIDE_ROAD if VisionStreamType.VISION_STREAM_WIDE_ROAD in available else None)
|
|
if wanted is None:
|
|
if client is not None:
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
client = None
|
|
stream = 'unknown'
|
|
_send(pm, session=session, status='unavailable', stream='unknown')
|
|
time.sleep(UNAVAILABLE_RETRY_SECONDS)
|
|
continue
|
|
new_stream = 'road' if wanted == VisionStreamType.VISION_STREAM_NARROW_ROAD else 'wideRoad'
|
|
if client is None or stream != new_stream:
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
client = VisionIpcClient('camerad', wanted, True)
|
|
stream = new_stream
|
|
if not client.is_connected():
|
|
# Even a successful reconnect of the same client starts a new camera
|
|
# session. Earlier sign support cannot confirm a new producer's frame.
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
last_inference_ns = 0
|
|
processing_interval_ns = 0
|
|
last_clock_offset_ns = None
|
|
client.connect(False)
|
|
if not client.is_connected():
|
|
core.reset()
|
|
client = None
|
|
time.sleep(0.1)
|
|
continue
|
|
buffer_info = _camera_buffer(client)
|
|
if buffer_info is None:
|
|
continue
|
|
eof_boot_ns, frame_id = buffer_info[1:3]
|
|
admission_pair = clock_pair_ns()
|
|
if (admission_pair is None or eof_boot_ns <= 0 or
|
|
not 0 <= admission_pair[1] - eof_boot_ns <= MAX_FRAME_AGE_NS):
|
|
del buffer_info
|
|
_send(pm, session=session, status='stale', stream=stream, eof_boot_ns=eof_boot_ns, frame_id=frame_id)
|
|
continue
|
|
if not _driving_model_ready(sm, admission_pair[0]):
|
|
del buffer_info
|
|
if not model_paused:
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
_send(pm, session=session, status='unavailable', stream=stream)
|
|
last_unavailable = admission_pair[0] / 1e9
|
|
elif admission_pair[0] / 1e9 - last_unavailable >= UNAVAILABLE_RETRY_SECONDS:
|
|
_send(pm, session=session, status='unavailable', stream=stream)
|
|
last_unavailable = admission_pair[0] / 1e9
|
|
model_paused = True
|
|
time.sleep(0.1)
|
|
continue
|
|
model_paused = False
|
|
admission_offset_ns = admission_pair[1] - admission_pair[0]
|
|
offset_stable = (last_clock_offset_ns is None or
|
|
abs(admission_offset_ns - last_clock_offset_ns) <= MAX_PAIR_SKEW_NS)
|
|
inference_interval_ns = _inference_interval_ns(
|
|
processing_interval_ns, int(core.last_detection_at * 1e9), admission_pair[0])
|
|
if offset_stable and admission_pair[0] - last_inference_ns < inference_interval_ns:
|
|
del buffer_info
|
|
# Camera/model liveness is separate from costly sign inference. Keep
|
|
# the existing road-limit confirmation and its real support timestamp.
|
|
# No conversion or new detector evidence is needed for this heartbeat.
|
|
if (admission_pair[0] - last_publish_ns >= INFERENCE_INTERVAL_NS and
|
|
last_result is not None and session == last_result_session and
|
|
0 < last_result_observed_ns <= admission_pair[0] <= last_result_valid_until_ns):
|
|
sm.update(0)
|
|
heartbeat_pair = clock_pair_ns()
|
|
if (heartbeat_pair is None or
|
|
abs((heartbeat_pair[1] - heartbeat_pair[0]) - admission_offset_ns) > MAX_PAIR_SKEW_NS or
|
|
not 0 <= heartbeat_pair[1] - eof_boot_ns <= MAX_FRAME_AGE_NS):
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
last_clock_offset_ns = None
|
|
_send(pm, session=session, status='stale', stream=stream, eof_boot_ns=eof_boot_ns, frame_id=frame_id)
|
|
continue
|
|
if heartbeat_pair[0] > last_result_valid_until_ns:
|
|
continue
|
|
if not _driving_model_ready(sm, heartbeat_pair[0]):
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
model_paused = True
|
|
last_unavailable = heartbeat_pair[0] / 1e9
|
|
_send(pm, session=session, status='unavailable', stream=stream)
|
|
continue
|
|
if params.get_bool('IsMetric') != is_metric:
|
|
continue
|
|
_send(pm, session=session, status='valid', stream=stream,
|
|
observed_ns=last_result_observed_ns, eof_boot_ns=eof_boot_ns,
|
|
frame_id=frame_id, result=last_result, valid_until_ns=last_result_valid_until_ns, is_metric=is_metric)
|
|
last_publish_ns = heartbeat_pair[0]
|
|
continue
|
|
try:
|
|
frame_info = _convert_camera_buffer(buffer_info)
|
|
finally:
|
|
del buffer_info
|
|
if frame_info is None:
|
|
continue
|
|
frame, eof_boot_ns, frame_id = frame_info
|
|
pair = clock_pair_ns()
|
|
if pair is None or eof_boot_ns <= 0 or not 0 <= pair[1] - eof_boot_ns <= MAX_FRAME_AGE_NS:
|
|
_send(pm, session=session, status='stale', stream=stream, eof_boot_ns=eof_boot_ns, frame_id=frame_id)
|
|
continue
|
|
offset_ns = pair[1] - pair[0]
|
|
if last_clock_offset_ns is not None and abs(offset_ns - last_clock_offset_ns) > MAX_PAIR_SKEW_NS:
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
last_inference_ns = 0
|
|
processing_interval_ns = 0
|
|
last_clock_offset_ns = offset_ns
|
|
inference_interval_ns = _inference_interval_ns(processing_interval_ns, int(core.last_detection_at * 1e9), pair[0])
|
|
if pair[0] - last_inference_ns < inference_interval_ns:
|
|
continue
|
|
# Budget camera conversion together with inference, as in the original Dom loop.
|
|
last_inference_ns = admission_pair[0]
|
|
try:
|
|
# The observation belongs to the original camera frame, not the end of
|
|
# CPU inference. Completion never restamps an older camera observation.
|
|
source_observed_ns = eof_boot_ns - offset_ns
|
|
result = core.observe(frame, now=source_observed_ns / 1e9)
|
|
except Exception as error:
|
|
cloudlog.error('vision SLC inference unavailable: %s', error)
|
|
_send(pm, session=session, status='unavailable', stream=stream)
|
|
client = None
|
|
core = None
|
|
continue
|
|
sm.update(0)
|
|
observed_pair = clock_pair_ns()
|
|
if observed_pair is not None:
|
|
processing_ns = observed_pair[0] - last_inference_ns
|
|
if processing_ns >= 0:
|
|
# Original Dom processing-cost cooldown: keep slow CPU inference from
|
|
# immediately starting another pass, including after stale rejection.
|
|
processing_interval_ns = min(BUSY_INFERENCE_INTERVAL_NS, processing_ns * 5 // 2)
|
|
if params.get_bool('IsMetric') != is_metric:
|
|
continue
|
|
if (observed_pair is None or
|
|
abs((observed_pair[1] - observed_pair[0]) - offset_ns) > MAX_PAIR_SKEW_NS or
|
|
not 0 <= observed_pair[1] - eof_boot_ns <= MAX_RESULT_AGE_NS):
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
last_clock_offset_ns = None
|
|
_send(pm, session=session, status='stale', stream=stream, eof_boot_ns=eof_boot_ns, frame_id=frame_id)
|
|
continue
|
|
if not _driving_model_ready(sm, observed_pair[0]):
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
model_paused = True
|
|
last_unavailable = observed_pair[0] / 1e9
|
|
_send(pm, session=session, status='unavailable', stream=stream)
|
|
continue
|
|
# A confirmed sign remains the known road limit after it leaves view.
|
|
# Keep its actual support time separate from this fresh camera heartbeat.
|
|
sign_observed_ns = int(core.last_published_support_at * 1e9) if result is not None else source_observed_ns
|
|
_send(pm, session=session, status='valid' if result is not None else 'unknown', stream=stream,
|
|
observed_ns=sign_observed_ns, eof_boot_ns=eof_boot_ns, frame_id=frame_id, result=result,
|
|
valid_until_ns=source_observed_ns + MAX_RESULT_AGE_NS, is_metric=is_metric)
|
|
last_publish_ns = observed_pair[0]
|
|
last_result = result
|
|
last_result_session = session
|
|
last_result_observed_ns = sign_observed_ns
|
|
last_result_valid_until_ns = source_observed_ns + MAX_RESULT_AGE_NS if result is not None else 0
|
|
except Exception as error:
|
|
cloudlog.error('vision SLC camera unavailable: %s', error)
|
|
_send(pm, session=session, status='unavailable', stream=stream)
|
|
client = None
|
|
core.reset()
|
|
session = uuid.uuid4().hex
|
|
last_clock_offset_ns = None
|
|
time.sleep(0.1)
|