CD210 model, WebCarrotMan, fix SpeedTF (#248)

* scc_contro: a_val=> no jerk limitation.

* fix..

* fix..

* fix..

* test. for isg

* carrot web server

* fix.. change branches..

* fix..

* CD210 model..

* fix..

* fix..

* ff

* fix..

* test full screen

* fix..

* fix..

* disable webrtc

* fix..

* fix.. enableWebRTC,

* fix..

* revert dm model.

* register my ip

* fix..

* test

* fix..

* fix..

* fix.. speed TF, more TFs. 0, 10, 20, 30%

* fix..

* fix..

* fix..

* fix..

* fix..

* fix

* fix..

* fix..

* ff

* ff

* fix.. quick link

* fix..

* fix..

* fix quick link... text..

* fix SCC error (canfd)

* fix.. speed TF < 0

* hud

* fix..

* fix.. tf..

* fleet..

* release..

* web cmd

* fix run cmd..

* fix cmd

* cmd allow

* fix..

* fix.. release..
This commit is contained in:
carrot
2026-02-13 11:16:13 +09:00
committed by GitHub
parent 0ab1092298
commit 5d3546b8da
21 changed files with 3636 additions and 28 deletions
+6
View File
@@ -1,3 +1,9 @@
Carrot2-v9 (2026-02-xx)
========================
* CD210 model
* web carrot_man (http://ip:7000)
* fix speed based TF
Carrot2-v9 (2026-01-xx)
========================
* WMI model
@@ -307,7 +307,7 @@ def create_acc_control_scc2(packer, CAN, enabled, accel_last, accel, stopping, g
a_val, a_raw = 0, 0
else:
a_raw = accel
a_val = np.clip(accel, accel_last - jn, accel_last + jn)
a_val = accel #np.clip(accel, accel_last - jn, accel_last + jn)
values = copy.copy(CS.cruise_info)
values.pop("COUNTER", None)
@@ -440,6 +440,8 @@ def create_tcs_messages(packer, CAN, CS):
if CS.tcs_info_373 is not None:
values = copy.copy(CS.tcs_info_373)
values["DriverBraking"] = 0
values["NEW_SIGNAL_20"] = 0
values["NEW_SIGNAL_11"] = 0
values["DriverBrakingLowSens"] = 0
#values["NEW_SIGNAL_1"] = 0 # accel과 관련.. 옆두부 꺼지는것과 관련? 확인필요
#values["ACC_REQ"] = 1 # 옆두부 꺼지는것과 관련? 확인필요.. 항상 켜지게함..
@@ -991,7 +993,7 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
values["ALERTS_3"] = 0
values["SOUNDS_3"] = 0
if values["ALERTS_5"] in [1, 2, 4, 5]:
if values["ALERTS_5"] in [1, 2, 3, 4, 5]:
values["ALERTS_5"] = 0
if values["ALERTS_5"] in [11] and CS.softHoldActive == 0:
@@ -1002,13 +1004,13 @@ def create_ccnc_messages(CP, packer, CAN, frame, CC, CS, hud_control,
values["LANELINE_CURVATURE"] = (min(abs(curvature), 15) + (-1 if curvature < 0 else 0)) if lat_active else 0
values["LANELINE_CURVATURE_DIRECTION"] = 1 if curvature < 0 and lat_active else 0
lane_color = 2 if CS.out.leftLaneLine < 20 else 4
lane_color = 4 if CS.out.leftLaneLine >= 20 or CS.out.leftBlindspot else 2
if hud_control.leftLaneDepart:
values["LANELINE_LEFT"] = 4 if (frame // 50) % 2 == 0 else 1
else:
values["LANELINE_LEFT"] = lane_color if hud_control.leftLaneVisible else 0
lane_color = 2 if CS.out.rightLaneLine < 20 else 4
lane_color = 4 if CS.out.rightLaneLine >= 20 or CS.out.rightBlindspot else 2
if hud_control.rightLaneDepart:
values["LANELINE_RIGHT"] = 4 if (frame // 50) % 2 == 0 else 1
else:
+12 -1
View File
@@ -205,11 +205,20 @@ class CarrotPlanner:
[self.tFollowGap1, self.tFollowGap2, self.tFollowGap3, self.tFollowGap4]))
self.jerk_factor = float(np.interp(v_kph, bp, [1.0, 0.7, 0.5, 0.5]))
"""
personality = int(np.clip(np.digitize(v_kph, bp[1:], right=False), 0, 3))
if self.params_count % 100 == 0:
self.params.put_int_nonblocking("LongitudinalPersonality", personality)
self.personality = personality
"""
if personality == log.LongitudinalPersonality.moreRelaxed:
tf_target *= 2.0
elif personality == log.LongitudinalPersonality.relaxed:
tf_target *= 1.6
elif personality == log.LongitudinalPersonality.standard:
tf_target *= 1.3
elif personality == log.LongitudinalPersonality.aggressive:
tf_target *= 1.0
else:
tf_target = 1.0
@@ -233,6 +242,8 @@ class CarrotPlanner:
s = float(np.clip(v_ego * CV.MS_TO_KPH / 100.0, 0.0, 1.0))
scale = (1.0 - reduce) + reduce * s
tf_target *= scale
else:
return tf_target
# ------------------------------------------------------------
# 2) Decel-hold only (no smoothing constants)
+57 -1
View File
@@ -13,6 +13,10 @@ from datetime import datetime
from ftplib import FTP
from cereal import log
import urllib.request
import urllib.error
import ssl
import cereal.messaging as messaging
from openpilot.common.realtime import Ratekeeper
from openpilot.common.params import Params
@@ -256,6 +260,55 @@ class CarrotMan:
except Exception as e:
return f"Error: {e}"
def register_my_ip(self):
try:
token = "12345678"
local_ip = self.get_local_ip()
version = self.params.get("Version")
github_id = self.params.get("GithubUsername")
port = 7000
is_onroad = self.params.get_bool("IsOnroad")
ts = int(time.time())
url = "https://shind0.synology.me/carrot/api_heartbeat.php"
timeout_s = 3.5
payload = {
"github_id": github_id,
"token": token,
"local_ip": local_ip,
"port": int(port),
"version": version,
"is_onroad": bool(is_onroad),
"ts": int(time.time()),
}
#if extra:
# payload.update(extra)
data = json.dumps(payload).encode("utf-8")
print(data)
req = urllib.request.Request(
url=url,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
try:
ctx = ssl._create_unverified_context()
with urllib.request.urlopen(req, timeout=timeout_s, context=ctx) as resp:
body = resp.read().decode("utf-8", errors="replace")
# 서버가 {"ok":true} 같은 JSON을 주는 경우가 많음
return (200 <= resp.status < 300), body
except urllib.error.HTTPError as e:
try:
body = e.read().decode("utf-8", errors="replace")
except Exception:
body = ""
return False, f"HTTPError {e.code}: {body}"
except Exception as e:
return False, f"Exception: {e}"
except Exception as e:
print(f"register_my_ip error: {e}")
# 브로드캐스트 메시지 전송
def broadcast_version_info(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -302,6 +355,9 @@ class CarrotMan:
if carrot_speed_active_count > 0:
self.carrot_speed_serv(carrot_speed, frame)
if frame % (20 * 30) == 0:
ok, msg = self.register_my_ip()
print(f"[heartbeat] ok: {ok}, msg: {msg}")
if frame % 20 == 0 or remote_addr is not None:
try:
self.broadcast_ip = self.get_broadcast_address() if remote_addr is None else remote_addr[0]
@@ -325,7 +381,7 @@ class CarrotMan:
# sock.sendto(dat, address)
if remote_addr is None:
print(f"Broadcasting: {self.broadcast_ip}:{msg}")
print(f"Broadcasting: {self.broadcast_ip}") #:{msg}")
if not self.navd_active:
#print("clear path_points: navd_active: ", self.navd_active)
self.navi_points = []
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+280
View File
@@ -0,0 +1,280 @@
/* ===== Driving HUD card ===== */
#driveHudCard { padding: 10px; }
.hudWrap{
border: 2px solid rgba(255,255,255,0.65);
border-radius: 18px;
padding: 10px;
background: rgba(0,0,0,0.22);
aspect-ratio: 1 / 1;
width: 100%;
max-width: 320px;
margin: 0 auto;
box-sizing: border-box;
position: relative;
}
.hudTop{
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.hudMini{
background: #1e8f3f;
border: 2px solid rgba(0,0,0,0.35);
border-radius: 10px;
padding: 6px 8px;
text-align: center;
color: #fff;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.15);
}
.hudMiniLabel{
font-weight: 900;
font-size: 12px;
line-height: 1.0;
}
.hudMiniVal{
font-weight: 900;
font-size: 16px;
line-height: 1.1;
margin-top: 2px;
}
.hudMain {
position: relative;
height: 140px
}
.hudLowerGroup {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
.hudSpeedBg {
position: absolute;
width: 225px;
height: 180px;
left: 40%;
top: 6px;
transform: translateX(-50%);
background-image: url("/speed_bg.png");
background-repeat: no-repeat;
background-size: contain;
background-position: center;
pointer-events: none;
opacity: 0.95;
}
.hudSpeed{
position: absolute;
font-size: 78px;
font-weight: 1000;
letter-spacing: -2px;
color: #fff;
text-shadow: 0 2px 0 rgba(0,0,0,0.45);
left: 12%;
top: 18px;
}
.hudRedDot{
position: absolute;
left: 12px;
top: 30px;
width: 18px;
height: 18px;
border-radius: 999px;
background: #ff2a2a;
box-shadow: 0 0 0 3px rgba(0,0,0,0.35);
}
.hudSignalDot{
position: absolute;
left: 2px;
top: 6px;
width: 20px;
height: 20px;
border-radius: 999px;
background: #15d14b; /* green default */
box-shadow: 0 0 0 3px rgba(0,0,0,0.35);
}
.hudTempReason {
position: absolute;
left: 73%;
top: 13px;
font-weight: 900;
font-size: 14px;
color: #22ff61;
}
.hudTempSpeed{
position: absolute;
left:73%;
top: 22px;
font-weight: 1000;
font-size: 33px;
color: #22ff61;
letter-spacing: -1px;
}
.hudSetSpeed{
font-weight: 1000;
font-size: 33px;
color: #22ff61;
letter-spacing: -1px;
position: absolute;
left: 55%;
top: 56px;
}
.hudGapNum{
position: absolute;
left: 69%;
top: 109px;
display:flex;
align-items:center;
justify-content:center;
font-weight: 900;
font-size: 20px;
color:#fff;
}
.hudGear{
position: absolute;
right: 0%;
bottom: 0%;
align-items:center;
justify-content:center;
font-weight: 1000;
font-size: 48px;
border: 2px solid rgba(255,255,255,0.65);
border-radius: 14px;
color:#1cff57;
}
.hudBottom{
display:flex;
gap: 10px;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.hudLeftStack {
display: flex;
flex-direction: column;
align-items: center;
gap: 0px;
}
.hudGps{
padding: 6px 10px;
border-radius: 12px;
color: #fff;
font-weight: 900;
font-size: 14px;
min-width: 54px;
text-align:center;
}
.hudGps.off{
color: rgba(255,255,255,0.75);
}
.hudDriveMode {
padding: 4px 10px;
align-items: center;
border-radius: 12px;
border: 2px solid rgba(0,0,0,0.35);
font-weight: 900;
font-size: 14px;
min-width: 66px;
text-align: center;
background: #e7eef7;
color: #1a1f26;
}
.hudDriveMode.mode_normal{ background:#e7eef7; color:#1a1f26; }
.hudDriveMode.mode_eco{ background:#10c248; color:#fff; }
.hudDriveMode.mode_safe{ background:#ff9c2a; color:#1a1f26; }
.hudDriveMode.mode_sport{ background:#ff2a2a; color:#fff; }
.hudRoadLimitLabel {
padding: 6px 10px;
border-radius: 12px;
color: #fff;
font-weight: 900;
font-size: 14px;
min-width: 54px;
text-align: center;
}
.hudRoadLimitVal {
padding: 4px 10px;
align-items: center;
border-radius: 12px;
border: 2px solid rgba(0,0,0,0.35);
font-weight: 900;
font-size: 14px;
min-width: 66px;
text-align: center;
color: #fff;
}
.hudRoadLimitVal.over{
background: #ff2a2a;
border-color: rgba(0,0,0,0.35);
}
.hudBars{
display:flex;
flex-direction: column;
gap: 5px;
align-items: flex-end;
height: 75px;
}
.hudBar {
width: 40px;
height: 16px;
border-radius: 4px;
background: rgba(255,255,255,0.28);
border: 1px solid rgba(0,0,0,0.35);
transition: height 0.15s ease, background 0.15s ease;
}
.hudBar.on{
background: #1cff57;
}
@media (max-width: 360px){
#driveHudCard .hudWrap{ max-width: 280px; }
#driveHudCard .hudSpeed{ font-size: 68px; }
#driveHudCard .hudSetSpeed{ font-size: 40px; }
}
/* ===== WebRTC overlay dock ===== */
.hudOverlayHost{
position:absolute;
inset:0;
pointer-events:none;
}
/* default: top-center overlay (non-fullscreen) */
.hudOverlayHost.dock_top .hudWrap{
position:absolute;
top:10px;
left:50%;
transform: translateX(-50%) scale(0.82);
transform-origin: top center;
width: 320px;
max-width: 320px;
}
/* fullscreen/landscape: bottom-left overlay */
.hudOverlayHost.dock_bl .hudWrap{
position:absolute;
left:12px;
bottom:12px;
transform: scale(0.86);
transform-origin: left bottom;
width: 320px;
max-width: 320px;
}
+163
View File
@@ -0,0 +1,163 @@
/* Driving HUD widget (standalone)
* - Exposes: window.DrivingHud.init(), window.DrivingHud.update(payload)
* Payload (suggested):
* {
* cpuTempC, memPct, diskPct, diskLabel,
* vEgoKph, vSetKph,
* temp: { reason, speedKph, isDecel }, // optional
* redDot, tlight, // tlight: "green"|"red"|"off"
* tfGap, gear, gpsOk,
* driveMode: { name, kind }, // kind: "normal"|"eco"|"safe"|"sport"
* speedLimitKph, speedLimitOver,
* apm, tfBars
* }
*/
(function () {
function $(id) { return document.getElementById(id); }
function setText(id, v) { const el=$(id); if (el) el.textContent = (v==null? "" : String(v)); }
function show(id, on) { const el=$(id); if (el) el.style.display = on ? "" : "none"; }
function clamp01(x){ x = Number(x); if (!isFinite(x)) return 0; return Math.max(0, Math.min(1, x)); }
function setMini(elId, labelId, label, valueText, good=true){
const el = $(elId);
if (!el) return;
if (labelId) setText(labelId, label);
setText(elId, valueText);
}
function setSignalDot(kind){
const el = $("hudSignalDot");
if (!el) return;
if (kind === "red") el.style.background = "#ff2a2a";
else if (kind === "green") el.style.background = "#15d14b";
else el.style.background = "rgba(255,255,255,0.18)";
}
function setGear(txt){
const el = $("hudGear");
if (!el) return;
el.textContent = txt || "U";
// green for D/number, gray for unknown, orange for R, blue for N (taste)
const t = String(txt || "U").toUpperCase();
if (t === "R") el.style.color = "#ff9c2a";
else if (t === "N") el.style.color = "#7ec8ff";
else if (t === "U") el.style.color = "rgba(255,255,255,0.7)";
else el.style.color = "#1cff57";
}
function setDriveMode(name, kind){
const el = $("hudDriveMode");
if (!el) return;
el.textContent = name || "일반";
el.classList.remove("mode_normal","mode_eco","mode_safe","mode_sport");
if (kind === "eco") el.classList.add("mode_eco");
else if (kind === "safe") el.classList.add("mode_safe");
else if (kind === "sport") el.classList.add("mode_sport");
else el.classList.add("mode_normal");
}
function setGps(ok){
const el = $("hudGps");
if (!el) return;
el.classList.toggle("off", !ok);
}
function setRoadLimit(speedKph, over){
const box = $("hudRoadLimitVal");
setText("hudRoadLimitVal", (speedKph==null || !isFinite(speedKph)) ? "--" : Math.round(speedKph));
if (box) box.classList.toggle("over", !!over);
}
function setBars(n){
const wrap = $("hudBars");
if (!wrap) return;
const bars = wrap.querySelectorAll(".hudBar");
const k = Math.max(0, Math.min(bars.length, Number(n) || 0));
const start = bars.length - k;
bars.forEach((b, i) => b.classList.toggle("on", i >= start));
}
function setGapNum(n){
const el = $("hudGapNum");
if (!el) return;
if (n == null || !isFinite(n)) el.textContent = "-";
else el.textContent = String(Math.round(n));
}
function setTemp(temp){
//const row = $("hudTemphudTempReason");
if (!temp || temp.speed == null || !isFinite(temp.speed) || !temp.source) {
//row.style.display = "none";
return;
}
//row.style.display = "";
$("hudTempReason").textContent = String(temp.source);
$("hudTempSpeed").textContent = String(Math.round(temp.speed));
const isDecel = !!temp.is_decel;
const color = isDecel ? "#ff9c2a" : "#22ff61";
$("hudTempReason").style.color = color;
$("hudTempSpeed").style.color = color;
}
function setSpeed(vEgoKph){
const el = $("hudSpeed");
//console.log("[HUD] setSpeed", vEgoKph, "el?", !!el);
if (!el) return;
if (vEgoKph == null || !isFinite(vEgoKph)) { el.textContent = "--"; return; }
el.textContent = String(Math.round(vEgoKph));
}
function setSetSpeed(vSetKph){
const el = $("hudSetSpeed");
if (!el) return;
if (vSetKph == null || !isFinite(vSetKph)) { el.textContent = "--"; return; }
el.textContent = String(Math.round(vSetKph));
}
function setRedDot(on){
show("hudRedDot", !!on);
}
function setSys(cpuTempC, memPct, diskPct, diskLabel){
setText("hudCpuVal", (cpuTempC==null || !isFinite(cpuTempC)) ? "--°C" : `${cpuTempC.toFixed(0)}°C`);
setText("hudMemVal", (memPct==null || !isFinite(memPct)) ? "--%" : `${memPct.toFixed(0)}%`);
setText("hudDiskVal", (diskPct==null || !isFinite(diskPct)) ? "--%" : `${diskPct.toFixed(0)}%`);
if (diskLabel) setText("hudDiskLabel", diskLabel);
}
const DrivingHud = {
init() {
// default visuals
setBars(0);
setSignalDot("off");
setGps(false);
setDriveMode("일반","normal");
setRoadLimit(null, false);
setGapNum(null);
setGear("U");
setRedDot(false);
setTemp(null);
},
update(p) {
if (!p) return;
setSys(p.cpuTempC, p.memPct, p.diskPct, p.diskLabel);
setSpeed(p.vEgoKph);
setSetSpeed(p.vSetKph);
setSignalDot(p.tlight || "off");
setRedDot(p.redDot);
setTemp(p.temp);
setGapNum(p.tfGap);
setBars(p.tfBars != null ? p.tfBars : p.tfGap); // default same meaning
setGear(p.gear);
setGps(!!p.gpsOk);
if (p.driveMode) setDriveMode(p.driveMode.name, p.driveMode.kind);
setRoadLimit(p.speedLimitKph, p.speedLimitOver);
}
};
window.DrivingHud = DrivingHud;
})();
+541
View File
@@ -0,0 +1,541 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Carrot</title>
<link rel="stylesheet" href="/hud_card.css?v=2602-01" />
<style>
body {
font-family: system-ui, sans-serif;
margin: 0;
background: #0b0d10;
color: #e8eef5;
}
.topbar {
display: flex;
gap: 10px;
padding: 12px;
background: #11161d;
position: sticky;
top: 0;
z-index: 10;
}
.btn {
padding: 10px 14px;
border: 1px solid #2a3542;
background: #141b24;
color: #e8eef5;
border-radius: 10px;
cursor: pointer;
}
.btn.active {
background: #1e2a3a;
border-color: #3b5168;
}
.wrap {
padding: 12px;
}
.card {
border: 1px solid #253040;
background: #0f141c;
border-radius: 14px;
padding: 12px;
margin-bottom: 12px;
}
.muted {
color: #9bb0c6;
font-size: 12px;
}
.groupBtn {
width: 100%;
text-align: left;
margin-bottom: 8px;
}
.setting {
border: 1px solid #253040;
border-radius: 14px;
padding: 12px;
margin-bottom: 10px;
background: #0c1118;
}
.settingTop {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: flex-start;
}
.title {
font-weight: 700;
font-size: 15px;
}
.name {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
color: #9bb0c6;
}
.descr {
white-space: pre-wrap;
margin-top: 6px;
color: #c7d6e6;
font-size: 13px;
}
.ctrl {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
}
.pill {
border: 1px solid #2a3542;
background: #141b24;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
color: #c7d6e6;
}
.smallBtn {
padding: 8px 10px;
border-radius: 10px;
border: 1px solid #2a3542;
background: #141b24;
color: #e8eef5;
cursor: pointer;
}
.val {
min-width: 84px;
text-align: center;
font-weight: 700;
}
.langToggle {
margin-left: auto;
}
.btnRow {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.backupRow {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.restoreRow {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.restoreRow input[type="file"] {
flex: 1;
min-width: 180px;
}
pre {
white-space: pre-wrap;
overflow: auto;
}
/* screen transition */
.screen {
transition: opacity .16s ease, transform .16s ease;
will-change: opacity, transform;
}
.screen.hidden {
opacity: 0;
transform: translateX(12px);
pointer-events: none;
}
/* Mobile improvements */
@media (max-width: 640px) {
.topbar {
gap: 8px;
padding: 10px;
}
.btn {
padding: 10px 12px;
border-radius: 12px;
}
.settingTop {
flex-direction: column;
align-items: stretch;
}
.ctrl {
justify-content: flex-start;
}
.val {
min-width: 120px;
}
.smallBtn {
padding: 10px 12px;
}
}
.rtcVideo {
width: 100%;
border-radius: 14px;
background: #000;
object-fit: contain;
/* 세로모드 기본: 너무 작게 안 만들고 싶으면 제한 안 둬도 됨 */
}
@media (orientation: landscape) {
.rtcVideo {
max-height: 40vh; /* 35~50vh 사이에서 취향 */
width: auto; /* 가로모드에서 “폭 100%” 강제 안 함 */
max-width: 100%;
margin: 0 auto;
}
}
/* ===== Fullscreen override (video or wrapper) ===== */
/* ===== Fullscreen override (wrapper) ===== */
#rtcWrap:fullscreen,
#rtcWrap:-webkit-full-screen {
position: fixed !important;
inset: 0 !important;
width: 100vw !important;
height: 100vh !important;
margin: 0 !important;
border-radius: 0 !important;
background: #000 !important;
z-index: 9999 !important;
/* fullscreen에서만 중앙정렬 */
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* fullscreen일 때 video는 무조건 꽉 채움 */
#rtcWrap:fullscreen #rtcVideo,
#rtcWrap:-webkit-full-screen #rtcVideo {
width: 100vw !important;
height: 100vh !important;
max-width: none !important;
max-height: none !important;
margin: 0 !important;
border-radius: 0 !important;
object-fit: contain !important;
}
</style>
</head>
<body>
<div class="topbar">
<button id="btnHome" class="btn active">Home</button>
<button id="btnSetting" class="btn">Setting</button>
<button id="btnTools" class="btn">Tools</button>
<button id="btnFleet" class="btn">Fleet</button>
<button id="btnLang" class="btn langToggle">Lang: <span id="langLabel">KO</span></button>
</div>
<div class="wrap">
<!-- HOME -->
<div id="pageHome" class="card">
<h3 style="margin:0 0 8px 0;">Home</h3>
<!-- Driving HUD (card mode). JS will auto-dock this into WebRTC overlay when video is visible. -->
<div id="driveHudCard" class="card" style="margin:12px 0 0 0;">
<div class="hudWrap" id="hudRoot">
<div class="hudTop">
<div class="hudMini">
<div class="hudMiniLabel">CPU</div>
<div class="hudMiniVal" id="hudCpuVal">--°C</div>
</div>
<div class="hudMini">
<div class="hudMiniLabel">MEM</div>
<div class="hudMiniVal" id="hudMemVal">--%</div>
</div>
<div class="hudMini">
<div class="hudMiniLabel" id="hudDiskLabel">DISK</div>
<div class="hudMiniVal" id="hudDiskVal">--%</div>
</div>
</div>
<div class="hudLowerGroup">
<div class="hudMain">
<div class="hudSpeedBg" aria-hidden="true"></div>
<div class="hudRedDot" id="hudRedDot" style="display:none;"></div>
<div class="hudSpeed" id="hudSpeed">--</div>
<div class="hudSignalDot" id="hudSignalDot"></div>
<div class="hudTempReason" id="hudTempReason">eco</div>
<div class="hudTempSpeed" id="hudTempSpeed">--</div>
<div class="hudSetSpeed" id="hudSetSpeed">--</div>
<div class="hudGapNum" id="hudGapNum">-</div>
<div class="hudGear" id="hudGear">U</div>
</div>
<div class="hudBottom">
<div class="hudLeftStack">
<div class="hudGps" id="hudGps">GPS</div>
<div class="hudDriveMode mode_normal" id="hudDriveMode">일반</div>
</div>
<div class="hudRoadLimitBox">
<div class="hudRoadLimitLabel">LIMIT</div>
<div class="hudRoadLimitVal" id="hudRoadLimitVal">--</div>
</div>
<div class="hudBars" id="hudBars">
<div class="hudBar"></div>
<div class="hudBar"></div>
<div class="hudBar"></div>
<div class="hudBar"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Server State (기존 유지) -->
<div id="rtcCard" class="card" style="margin:12px 0 0 0; display:none;">
<h4 style="margin:0 0 8px 0;">WebRTC</h4>
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<div class="muted" id="rtcStatus">connecting...</div>
<button id="btnRtcFs" class="smallBtn">Full</button>
</div>
<div style="height:8px;"></div>
<!-- wrapper 추가 (relative) -->
<div id="rtcWrap" style="position:relative;">
<video id="rtcVideo"
class="rtcVideo"
autoplay playsinline muted style="display:none;">
</video>
<!-- HUD overlay host (video overlay mode) -->
<div id="hudOverlayHost" class="hudOverlayHost" style="display:none;"></div>
</div>
</div>
<div style="height:10px;"></div>
<div class="card" style="margin:0;">
<h4 style="margin:0 0 6px 0;">Server State</h4>
<pre id="stateBox" class="pill" style="display:block; padding:10px; border-radius:14px;">connecting...</pre>
</div>
<div class="card" style="margin-top:12px;">
<h4 style="margin:0 0 6px 0;">Quick Link</h4>
<a id="quickLink" href="#" target="_blank" rel="noopener"
style="word-break:break-all; display:none; text-decoration:none; color:#9bb0c6;">
</a>
<div class="muted" style="margin-top:6px;">
* 길게 눌러 링크저장
</div>
</div>
</div>
<!-- CAR SELECT (new page) -->
<div id="pageCar" class="card" style="display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<h3 id="carTitle" style="margin:0 0 8px 0; cursor:pointer;">Car Select</h3>
<button id="btnBackCar" class="btn">Back</button>
</div>
<!-- Current Car -->
<div class="card" style="margin:0 0 12px 0;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<!-- 여기 헤더가 차량명 -->
<div id="curCarLabelCar" style="font-weight:900; font-size:14px; line-height:1.2;">-</div>
</div>
</div>
<div class="muted" id="carMeta">loading...</div>
<div style="height:10px;"></div>
<!-- Maker screen -->
<div id="carScreenMakers" class="screen">
<div class="card" style="margin:0;">
<h4 style="margin:0 0 10px 0;">Makers</h4>
<div id="makerList"></div>
</div>
</div>
<!-- Model screen -->
<div id="carScreenModels" class="screen hidden" style="display:none;">
<div class="card" style="margin:0;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<h4 id="modelTitle" style="margin:0; cursor:pointer;">Models</h4>
<div class="muted" id="modelMeta">-</div>
</div>
<div style="height:10px;"></div>
<div id="modelList"></div>
</div>
</div>
</div>
<!-- SETTING -->
<div id="pageSetting" class="card" style="display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<h3 id="settingTitle" style="margin:0 0 8px 0; cursor:pointer;">Setting</h3>
<button id="btnBackGroups" class="btn" style="display:none;">Back</button>
</div>
<div class="muted" id="settingsMeta">loading...</div>
<div style="height:10px;"></div>
<!-- Current Car -->
<div class="card" style="margin:0 0 12px 0;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<!-- 여기 헤더가 차량명 -->
<div id="curCarLabelSetting" style="font-weight:900; font-size:14px; line-height:1.2;">-</div>
<button id="btnChangeCar" class="btn">Change</button>
</div>
</div>
<!-- Groups Screen -->
<div id="settingScreenGroups" class="screen">
<div class="card" style="margin:0;">
<h4 style="margin:0 0 10px 0;">Groups</h4>
<div id="groupList"></div>
</div>
</div>
<!-- Items Screen -->
<div id="settingScreenItems" class="screen hidden" style="display:none;">
<div class="card" style="margin:0;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<h4 id="itemsTitle" style="margin:0; cursor:pointer;">Items</h4>
<div class="muted" id="groupMeta">-</div>
</div>
<div style="height:10px;"></div>
<div id="items"></div>
</div>
</div>
</div>
<!-- TOOLS -->
<div id="pageTools" class="card" style="display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<h3 style="margin:0 0 8px 0;">Tools</h3>
<button id="btnToolsBack" class="btn">Back</button>
</div>
<div class="muted" id="toolsMeta">-</div>
<div style="height:10px;"></div>
<!-- Git / Update -->
<div class="card" style="margin:0 0 12px 0;">
<h4 style="margin:0 0 10px 0;">Git Commands</h4>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button id="btnGitPull" class="btn">git pull</button>
<button id="btnGitSync" class="btn">git sync</button>
<button id="btnGitReset" class="btn">git reset</button>
<button id="btnGitBranch" class="btn">change branch</button>
</div>
<div class="muted" style="margin-top:10px;">
* reset/branch는 위험할 수 있으니 confirm 뜹니다.
</div>
</div>
<!-- System / User -->
<div class="card" style="margin:0;">
<h4 style="margin:0 0 10px 0;">User / System</h4>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button id="btnSendTmuxLog" class="btn">tmux log</button>
<button id="btnDeleteVideos" class="btn">delete all videos</button>
<button id="btnDeleteLogs" class="btn">delete all logs</button>
<button id="btnReboot" class="btn">reboot</button>
</div>
<div class="muted" style="margin-top:10px;">
* delete/reboot는 confirm 후 실행합니다.
</div>
<!-- Backup / Restore -->
<div class="card">
<div class="backupRow">
<button id="btnBackupSettings" class="btn">backup settings</button>
</div>
<div class="restoreRow">
<input id="restoreFile" type="file" accept="application/json" />
<button id="btnRestoreSettings" class="btn">restore settings</button>
</div>
<div class="muted">
* restore 후 reboot 권장
</div>
</div>
<div class="card">
<h4 style="margin:0 0 6px 0;">System Command</h4>
<div class="muted" style="margin-bottom:8px;">
Allowed: git pull/status/branch/log, df, free, uptime (whitelist)
</div>
<div style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
<input id="sysCmdInput"
type="text"
placeholder='e.g. git pull'
style="flex:1; min-width:220px; padding:10px; border-radius:12px; border:1px solid #2a3542; background:#0c1118; color:#e8eef5;" />
<button id="btnSysCmdRun" class="smallBtn">Run</button>
</div>
</div>
<div style="height:12px;"></div>
<!-- output -->
<div class="card" style="margin:0;">
<h4 style="margin:0 0 8px 0;">Output</h4>
<pre id="toolsOut" class="pill" style="display:block; padding:10px; border-radius:14px; min-height:120px;">-</pre>
</div>
</div>
</div>
<!-- BRANCH SELECT -->
<div id="pageBranch" class="card" style="display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">
<h3 id="branchTitle" style="margin:0 0 8px 0; cursor:pointer;">Branch Select</h3>
<button id="btnBackBranch" class="btn">Back</button>
</div>
<div class="muted" id="branchMeta">-</div>
<div style="height:10px;"></div>
<div id="branchList"></div>
</div>
</div>
<script src="/hud_card.js?v=2602-02"></script>
<script src="/app.js?v=2602-02"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+217
View File
@@ -0,0 +1,217 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>webrtcd browser test</title>
<style>
body {
font-family: sans-serif;
margin: 16px;
}
video {
width: 48%;
background: #000;
margin: 4px;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
pre {
background: #111;
color: #0f0;
padding: 8px;
height: 220px;
overflow: auto;
}
input, button {
padding: 6px;
margin: 4px 0;
}
</style>
</head>
<body>
<h2>webrtcd browser test</h2>
<label>Device IP:Port (webrtcd)</label><br />
<input id="baseUrl" style="width:420px" value="http://192.168.0.171:5001" />
<br />
<label>Cameras (comma separated)</label><br />
<input id="cameras" style="width:420px" value="road" />
<div style="opacity:0.8">예: road / wideRoad / driver 등 (서버가 기대하는 개수와 맞추세요)</div>
<label><input type="checkbox" id="sendMic" /> Send microphone to device (incoming_audio_track)</label><br />
<label><input type="checkbox" id="recvAudio" checked /> Receive audio from device (expected_audio_track)</label><br />
<label><input type="checkbox" id="useDataChannel" checked /> Create datachannel (messaging)</label><br />
<button id="btnStart">Start</button>
<button id="btnStop" disabled>Stop</button>
<h3>Remote streams</h3>
<div class="row" id="videos"></div>
<h3>Log</h3>
<pre id="log"></pre>
<script>
const logEl = document.getElementById("log");
function log(...args) {
const s = args.map(a => (typeof a === "string" ? a : JSON.stringify(a))).join(" ");
console.log(s);
logEl.textContent += s + "\n";
logEl.scrollTop = logEl.scrollHeight;
}
let pc = null;
let dc = null;
let localStream = null;
async function start() {
const baseUrl = document.getElementById("baseUrl").value.trim().replace(/\/+$/, "");
const cameras = document.getElementById("cameras").value.split(",").map(s => s.trim()).filter(Boolean);
const sendMic = document.getElementById("sendMic").checked;
const recvAudio = document.getElementById("recvAudio").checked;
const useDataChannel = document.getElementById("useDataChannel").checked;
if (!baseUrl) return log("baseUrl empty");
if (!cameras.length) return log("cameras empty");
pc = new RTCPeerConnection({
// 같은 LAN이면 STUN 없어도 되는 경우가 많습니다.
iceServers: []
});
pc.oniceconnectionstatechange = () => log("ICE:", pc.iceConnectionState);
pc.onconnectionstatechange = () => log("PC :", pc.connectionState);
// --- Remote track handler ---
const videosDiv = document.getElementById("videos");
videosDiv.innerHTML = "";
let videoCount = 0;
pc.ontrack = (ev) => {
log("ontrack kind=", ev.track.kind, "streams=", ev.streams.length);
const stream = ev.streams[0] || new MediaStream([ev.track]);
if (ev.track.kind === "video") {
videoCount++;
const v = document.createElement("video");
v.autoplay = true;
v.playsInline = true;
v.controls = true;
v.muted = true; // 브라우저 자동재생 정책 회피 (영상은 mute)
v.srcObject = stream;
v.title = "remote video " + videoCount;
videosDiv.appendChild(v);
} else if (ev.track.kind === "audio") {
// 오디오는 mute 하면 안 들리니 별도 오디오 엘리먼트
const a = document.createElement("audio");
a.autoplay = true;
a.controls = true;
a.srcObject = stream;
videosDiv.appendChild(a);
}
};
// --- DataChannel (optional) ---
if (useDataChannel) {
dc = pc.createDataChannel("messaging"); // label이 서버 기대와 다르면 안 붙을 수 있음
dc.onopen = () => log("DC open");
dc.onclose = () => log("DC close");
dc.onerror = (e) => log("DC error", e);
dc.onmessage = (e) => {
// 서버는 json bytes를 보냅니다.
try {
const txt = typeof e.data === "string" ? e.data : new TextDecoder().decode(e.data);
const obj = JSON.parse(txt);
log("DC msg:", obj.type);
} catch (err) {
log("DC raw msg:", e.data);
}
};
}
// --- Transceivers: video recvonly for each camera ---
for (let i = 0; i < cameras.length; i++) {
pc.addTransceiver("video", { direction: "recvonly" });
}
// --- Audio: receive from device (expected_audio_track) ---
if (recvAudio) {
pc.addTransceiver("audio", { direction: "recvonly" });
}
// --- Audio: send mic to device (incoming_audio_track) ---
if (sendMic) {
// getUserMedia는 보통 HTTPS 또는 localhost에서만 됩니다.
// (LAN IP로 열면 막힐 수 있어요)
localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
for (const track of localStream.getAudioTracks()) {
pc.addTrack(track, localStream);
}
}
// Create offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Wait ICE gathering complete (간단히)
await new Promise((resolve) => {
if (pc.iceGatheringState === "complete") return resolve();
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === "complete") resolve();
};
});
log("POST /stream ...");
const res = await fetch(baseUrl + "/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sdp: pc.localDescription.sdp,
cameras: cameras,
bridge_services_in: [], // 필요하면 예: ["controlsState"]
bridge_services_out: [] // 필요하면 예: ["carState","deviceState"]
})
});
if (!res.ok) {
const t = await res.text();
throw new Error("stream failed: " + res.status + " " + t);
}
const answer = await res.json();
log("answer type:", answer.type);
await pc.setRemoteDescription({ type: answer.type, sdp: answer.sdp });
document.getElementById("btnStart").disabled = true;
document.getElementById("btnStop").disabled = false;
log("started.");
}
async function stop() {
if (dc) { try { dc.close(); } catch(e) {} dc = null; }
if (pc) {
try { pc.getSenders().forEach(s => s.track && s.track.stop && s.track.stop()); } catch(e) {}
try { pc.close(); } catch(e) {}
pc = null;
}
if (localStream) {
localStream.getTracks().forEach(t => t.stop());
localStream = null;
}
document.getElementById("btnStart").disabled = false;
document.getElementById("btnStop").disabled = true;
log("stopped.");
}
document.getElementById("btnStart").onclick = () => start().catch(e => log("ERR:", e.message));
document.getElementById("btnStop").onclick = () => stop();
</script>
</body>
</html>
+5 -5
View File
@@ -967,12 +967,12 @@
"group": "시작",
"name": "DisableDM",
"title": "DisableDM",
"descr": "운전자 감시 해제, 재부팅 필요",
"descr": "1.DisableDM, 2: +EnableWebRTC, reboot required",
"egroup": "START",
"etitle": "DisableDM",
"edescr": "Reboot required",
"edescr": "1.DisableDM, 2: +EnableWebRTC, Reboot required",
"min": 0,
"max": 1,
"max": 2,
"default": 0,
"unit": 1
},
@@ -1566,10 +1566,10 @@
"group": "차량간격",
"name": "EnableSpeedTF",
"title": "속도별 차간거리 설정(0)",
"descr": "-1: 자동단계선택, >0: 저속 차간거리 비율 감소",
"descr": "-1:0/30/60/90km/h 단계\n-2:0/40/80/120km/h 단계\n-3:0/50/100/150km/h 단계\n1~50%:100km/h 이하 TF 감소",
"egroup": "FDIST",
"etitle": "EnableSpeedTF(0)",
"edescr": "-1:Auto select by speed, >0: Reduce TFs by ratio",
"edescr": "-1:0/30/60/90km/h steps\n-2:0/40/80/120km/h steps\n-3:0/50/100/150km/h steps\n1~50%:Reduce TF below 100km/h",
"min": -3,
"max": 50,
"default": 0,
+4 -1
View File
@@ -181,7 +181,10 @@ class LatControlTorque(LatControl):
actual_curvature_rate = -VM.calc_curvature(math.radians(CS.steeringRateDeg), CS.vEgo, 0.0)
actual_lateral_jerk = actual_curvature_rate * CS.vEgo ** 2
else:
actual_curvature_llk = CC.angularVelocity[2] / CS.vEgo #llk.angularVelocityCalibrated.value[2] / CS.vEgo
if len(CC.angularVelocity) >= 2:
actual_curvature_llk = 0
else:
actual_curvature_llk = CC.angularVelocity[2] / CS.vEgo #llk.angularVelocityCalibrated.value[2] / CS.vEgo
actual_curvature = np.interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_llk])
curvature_deadzone = 0.0
desired_lateral_accel = desired_curvature * CS.vEgo ** 2
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -2435,9 +2435,9 @@ public:
else if (carState.getGearShifter() == cereal::CarState::GearShifter::PARK) strcpy(gear_str, "P");
else if (carState.getGearShifter() == cereal::CarState::GearShifter::DRIVE) {
if (carState.getGearStep() > 0)
sprintf(gear_str, "%d", carState.getGearStep());
else
strcpy(gear_str, "D");
sprintf(gear_str, "%d", carState.getGearStep());
else
strcpy(gear_str, "D");
}
else if(carState.getGearShifter() == cereal::CarState::GearShifter::NEUTRAL) strcpy(gear_str, "N");
else if (carState.getGearShifter() == cereal::CarState::GearShifter::REVERSE) strcpy(gear_str, "R");
+1 -1
View File
@@ -842,7 +842,7 @@ CarrotPanel::CarrotPanel(QWidget* parent) : QWidget(parent) {
startToggles->addItem(new CValueControl("NNFFLite", tr("NNFFLite"), tr("Twilsonco's NNFF-Lite(Reboot required)"), 0, 1, 1));
startToggles->addItem(new CValueControl("AutoGasSyncSpeed", tr("Auto update Cruise speed"), "", 0, 1, 1));
startToggles->addItem(new CValueControl("DisableMinSteerSpeed", tr("Disable Min.SteerSpeed"), "", 0, 1, 1));
startToggles->addItem(new CValueControl("DisableDM", tr("Disable DM"), "", 0, 1, 1));
startToggles->addItem(new CValueControl("DisableDM", tr("Disable DM"), "", 0, 2, 1));
startToggles->addItem(new CValueControl("HotspotOnBoot", tr("Hotspot enabled on boot"), "", 0, 1, 1));
startToggles->addItem(new CValueControl("SoftwareMenu", tr("Enable Software Menu"), "", 0, 1, 1));
startToggles->addItem(new CValueControl("IsLdwsCar", tr("IsLdwsCar"), "", 0, 1, 1));
+9 -1
View File
@@ -59,7 +59,15 @@ void FfmpegEncoder::encoder_open() {
this->codec_ctx->height = frame->height;
this->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
this->codec_ctx->time_base = (AVRational){ 1, encoder_info.fps };
int err = avcodec_open2(this->codec_ctx, codec, NULL);
//int err = avcodec_open2(this->codec_ctx, codec, NULL);
bool env_dashy = true;
AVDictionary* opts = NULL;
if (env_dashy && codec_id == AV_CODEC_ID_H264) {
av_dict_set(&opts, "preset", "ultrafast", 0);
av_dict_set(&opts, "tune", "zerolatency", 0);
}
int err = avcodec_open2(this->codec_ctx, codec, &opts);
av_dict_free(&opts);
assert(err >= 0);
is_open = true;
+7 -2
View File
@@ -78,6 +78,9 @@ def enable_connect(started, params, CP: car.CarParams) -> bool:
def enable_xiaoge_data(started, params, CP: car.CarParams) -> bool:
return params.get_bool("ShareData")
def enable_webrtc(started, params, CP: car.CarParams) -> bool:
return params.get_int("DisableDM") == 2
def c3x_lite(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("HardwareC3xLite")
@@ -86,7 +89,7 @@ procs = [
NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging),
NativeProcess("encoderd", "system/loggerd", ["./encoderd"], only_onroad),
NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar),
NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], or_(notcar, and_(only_onroad, enable_webrtc))),
PythonProcess("logmessaged", "system.logmessaged", always_run),
NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM),
@@ -132,7 +135,7 @@ procs = [
# debug procs
NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar),
PythonProcess("webrtcd", "system.webrtc.webrtcd", notcar),
PythonProcess("webrtcd", "system.webrtc.webrtcd", or_(notcar, and_(only_onroad, enable_webrtc))),
PythonProcess("webjoystick", "tools.bodyteleop.web", notcar),
PythonProcess("joystick", "tools.joystick.joystick_control", and_(joystick, iscar)),
@@ -140,6 +143,8 @@ procs = [
PythonProcess("fleet_manager", "selfdrive.frogpilot.fleetmanager.fleet_manager", check_fleet),
PythonProcess("carrot_man", "selfdrive.carrot.carrot_man", always_run),#, enabled=not PC),
PythonProcess("carrot_server", "selfdrive.carrot.carrot_server", always_run),
#Xiaoge data broadcaster (conditional on ShareData param)
PythonProcess("xiaoge_data", "selfdrive.carrot.xiaoge_data", enable_xiaoge_data),
# c3x lite
+5
View File
@@ -22,11 +22,16 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack):
self._pts = 0
async def recv(self):
waited = 0
while True:
msg = messaging.recv_one_or_none(self._sock)
if msg is not None:
break
await asyncio.sleep(0.005)
waited += 0.005
if waited > 1.0:
print("########### recv timedout....")
waited = 0
evta = getattr(msg, msg.which())
+44 -9
View File
@@ -42,7 +42,7 @@ class CerealOutgoingMessageProxy:
return msg_dict
def update(self):
async def update(self):
# this is blocking in async context...
self.sm.update(0)
for service, updated in self.sm.updated.items():
@@ -53,7 +53,11 @@ class CerealOutgoingMessageProxy:
outgoing_msg = {"type": service, "logMonoTime": mono_time, "valid": valid, "data": msg_dict}
encoded_msg = json.dumps(outgoing_msg).encode()
for channel in self.channels:
channel.send(encoded_msg)
#channel.send(encoded_msg)
if isinstance(channel, web.WebSocketResponse):
await channel.send_bytes(encoded_msg)
else:
channel.send(encoded_msg)
class CerealIncomingMessageProxy:
@@ -94,7 +98,7 @@ class CerealProxyRunner:
while True:
try:
self.proxy.update()
await self.proxy.update()
except InvalidStateError:
self.logger.warning("Cereal outgoing proxy invalid state (connection closed)")
break
@@ -126,12 +130,19 @@ class StreamSession:
from teleoprtc import WebRTCAnswerBuilder
from teleoprtc.info import parse_info_from_offer
self.logger = logging.getLogger("webrtcd")
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())
try:
track = LiveStreamVideoStreamTrack(cam) if not debug_mode else VideoStreamTrack()
builder.add_video_stream(cam, track)
self.logger.info("added camera track: %s", cam)
except Exception:
self.logger.exception("failed to create camera track: %s", cam)
raise
if config.expected_audio_track:
builder.add_audio_stream(AudioInputStreamTrack() if not debug_mode else AudioStreamTrack())
if config.incoming_audio_track:
@@ -153,15 +164,19 @@ class StreamSession:
self.audio_output: AudioOutputSpeaker | MediaBlackhole | None = None
self.run_task: asyncio.Task | None = None
self.logger = logging.getLogger("webrtcd")
self.logger.info("New stream session (%s), cameras %s, audio in %s out %s, incoming services %s, outgoing services %s",
self.identifier, cameras, config.incoming_audio_track, config.expected_audio_track, incoming_services, outgoing_services)
config = parse_info_from_offer(sdp)
self.logger.info("offer expects video tracks=%d, audio_expected=%s, audio_incoming=%s",
config.n_expected_camera_tracks, config.expected_audio_track, config.incoming_audio_track)
self.logger.info("request cameras=%s", cameras)
def start(self):
self.run_task = asyncio.create_task(self.run())
def stop(self):
if self.run_task.done():
if self.run_task is None or self.run_task.done():
return
self.run_task.cancel()
self.run_task = None
@@ -229,7 +244,7 @@ async def get_stream(request: 'web.Request'):
stream_dict[session.identifier] = session
return web.json_response({"sdp": answer.sdp, "type": answer.type})
return web.json_response({"sdp": answer.sdp, "type": answer.type}, headers={'Access-Control-Allow-Origin': '*'})
async def get_schema(request: 'web.Request'):
@@ -245,21 +260,41 @@ async def on_shutdown(app: 'web.Application'):
session.stop()
del app['streams']
@web.middleware
async def cors_middleware(request, handler):
response = await handler(request)
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response
async def handle_cors_preflight(request):
if request.method == 'OPTIONS':
headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
}
return web.Response(status=200, headers=headers)
return await request.app['handler'](request)
def webrtcd_thread(host: str, port: int, debug: bool):
logging.basicConfig(level=logging.CRITICAL, handlers=[logging.StreamHandler()])
#logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()])
logging_level = logging.DEBUG if debug else logging.INFO
logging.getLogger("WebRTCStream").setLevel(logging_level)
logging.getLogger("webrtcd").setLevel(logging_level)
app = web.Application()
app = web.Application(middlewares=[cors_middleware])
app['streams'] = dict()
app['debug'] = debug
app.on_shutdown.append(on_shutdown)
app.router.add_post("/stream", get_stream)
app.router.add_get("/schema", get_schema)
app.router.add_route('OPTIONS', '/{tail:.*}', handle_cors_preflight)
web.run_app(app, host=host, port=port)