feat(mesh): device-detected setup modal with board images + full radio settings
Plugging in a LoRa radio now pops a global two-step setup modal on any page: step 1 shows the ACTUAL detected board (25 board SVGs vendored from the Meshtastic web-flasher, GPL-3.0; matched by USB product string on native-USB boards, vid:pid heuristics for bridge chips, animated fallback otherwise); step 2 configures with Archipelago presets — the LoRa region pre-selected from the node's location (new lat/lon→region mapping), channel 'archipelago', node name, and an advanced firmware pin. Options adapt per protocol: region+channel program Meshtastic; MeshCore shows its community frequency plan (firmware owns RF); RNode defers to the Reticulum daemon config. Backend: mesh.configure now accepts lora_region (validated against the driver's region table) and device_kind (probe pin); mesh.status returns the persisted values plus per-port USB identity (sysfs vid/pid/product) for board identification. The Mesh Device tab gains a full settings form (region with duty-cycle/legality hints, firmware pin, channel, name, identity broadcast) replacing the read-only panel; the old Mesh-page-only onboarding modal is superseded by the global flow. Mock backend: stateful mesh config so the dev UI demos the full detect→configure round-trip (disable mesh → modal fires with Heltec V3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ -145,6 +145,32 @@ impl RpcHandler {
|
||||
{
|
||||
config.receive_block_headers = receive;
|
||||
}
|
||||
// LoRa region (Meshtastic): validated against the driver's region
|
||||
// table so a typo can't be persisted and silently ignored on connect.
|
||||
// Empty string clears the setting (radio keeps/uses its own region).
|
||||
if let Some(region) = params.get("lora_region").and_then(|v| v.as_str()) {
|
||||
let trimmed = region.trim();
|
||||
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("unset") {
|
||||
config.lora_region = None;
|
||||
} else if mesh::meshtastic_region_is_valid(trimmed) {
|
||||
config.lora_region = Some(trimmed.to_uppercase());
|
||||
} else {
|
||||
anyhow::bail!("Unknown LoRa region: {trimmed}");
|
||||
}
|
||||
}
|
||||
// Firmware pin: probe only the named firmware on the port ("auto"/""
|
||||
// clears the pin and restores strict-probe auto-detect).
|
||||
if let Some(kind) = params.get("device_kind").and_then(|v| v.as_str()) {
|
||||
config.device_kind = match kind.trim().to_lowercase().as_str() {
|
||||
"" | "auto" => None,
|
||||
"meshcore" => Some(mesh::types::DeviceType::Meshcore),
|
||||
"meshtastic" => Some(mesh::types::DeviceType::Meshtastic),
|
||||
"reticulum" | "rnode" => Some(mesh::types::DeviceType::Reticulum),
|
||||
other => anyhow::bail!(
|
||||
"Unknown device_kind: {other} (expected auto|meshcore|meshtastic|reticulum)"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
mesh::save_config(&self.config.data_dir, &config).await?;
|
||||
|
||||
@ -161,6 +187,8 @@ impl RpcHandler {
|
||||
"device_path": config.device_path,
|
||||
"announce_block_headers": config.announce_block_headers,
|
||||
"receive_block_headers": config.receive_block_headers,
|
||||
"lora_region": config.lora_region,
|
||||
"device_kind": config.device_kind.map(|k| k.to_string()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,24 @@ impl RpcHandler {
|
||||
"receive_block_headers".into(),
|
||||
config.receive_block_headers.into(),
|
||||
);
|
||||
// Persisted config values the settings UI edits (distinct from the
|
||||
// live radio-reported `region`): the configured LoRa region and
|
||||
// the firmware pin ("meshcore"|"meshtastic"|"reticulum"|null=auto).
|
||||
obj.insert("lora_region".into(), config.lora_region.clone().into());
|
||||
obj.insert(
|
||||
"device_kind".into(),
|
||||
config
|
||||
.device_kind
|
||||
.map(|k| k.to_string().to_lowercase())
|
||||
.into(),
|
||||
);
|
||||
// USB identity per detected port so the setup modal can show the
|
||||
// actual board (product string on native-USB boards, vid:pid as
|
||||
// the fallback for bridge chips).
|
||||
obj.insert(
|
||||
"detected_device_info".into(),
|
||||
serde_json::to_value(mesh::detect_devices_info().await).unwrap_or_default(),
|
||||
);
|
||||
// Raw serial-device presence, in BOTH branches. MeshStatus has no
|
||||
// such field, so while the service was running the UI couldn't
|
||||
// tell "no radio plugged in" from "radio present but the session
|
||||
|
||||
@ -1309,7 +1309,7 @@ fn derive_channel_psk(channel_name: &str) -> Vec<u8> {
|
||||
/// Map a Meshtastic `RegionCode` name (as set in `mesh-config.json`, e.g.
|
||||
/// "EU_868", "US", "ANZ") to its protobuf enum value. Case-insensitive.
|
||||
/// Returns `None` for an unrecognised name so we never write a bogus region.
|
||||
fn region_name_to_code(name: &str) -> Option<u32> {
|
||||
pub(crate) fn region_name_to_code(name: &str) -> Option<u32> {
|
||||
Some(match name.trim().to_uppercase().as_str() {
|
||||
"UNSET" => 0,
|
||||
"US" => 1,
|
||||
|
||||
@ -401,6 +401,12 @@ pub struct MeshConfig {
|
||||
pub reticulum_tcp: Option<types::ReticulumTcpConfig>,
|
||||
}
|
||||
|
||||
/// True when `name` is a LoRa region the Meshtastic driver can program
|
||||
/// (the canonical region list for the mesh settings UI).
|
||||
pub fn meshtastic_region_is_valid(name: &str) -> bool {
|
||||
meshtastic::region_name_to_code(name).is_some()
|
||||
}
|
||||
|
||||
fn default_assistant_backend() -> String {
|
||||
"claude".to_string()
|
||||
}
|
||||
@ -545,6 +551,12 @@ pub async fn detect_devices() -> Vec<String> {
|
||||
serial::detect_serial_devices().await
|
||||
}
|
||||
|
||||
/// Detected serial ports with USB identity metadata (for the device-detected
|
||||
/// setup modal's hardware graphic).
|
||||
pub async fn detect_devices_info() -> Vec<serial::DetectedDeviceInfo> {
|
||||
serial::detect_serial_devices_info().await
|
||||
}
|
||||
|
||||
// ─── MeshService ────────────────────────────────────────────────────────
|
||||
|
||||
/// Top-level mesh networking service.
|
||||
|
||||
@ -509,6 +509,83 @@ pub async fn detect_serial_devices() -> Vec<String> {
|
||||
devices
|
||||
}
|
||||
|
||||
/// USB identity of a detected serial port, read from sysfs — lets the UI
|
||||
/// show the actual board (native-USB boards like T-Deck/RAK4631 report their
|
||||
/// name in `product`; bridge chips like CP2102/CH340 only identify the chip,
|
||||
/// so vid:pid is the fallback signal).
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct DetectedDeviceInfo {
|
||||
pub path: String,
|
||||
pub vid: Option<String>,
|
||||
pub pid: Option<String>,
|
||||
pub product: Option<String>,
|
||||
pub manufacturer: Option<String>,
|
||||
}
|
||||
|
||||
/// Like `detect_serial_devices`, but with USB metadata per port.
|
||||
pub async fn detect_serial_devices_info() -> Vec<DetectedDeviceInfo> {
|
||||
let mut out = Vec::new();
|
||||
for path in detect_serial_devices().await {
|
||||
let usb = usb_info_for_tty(&path).await;
|
||||
out.push(DetectedDeviceInfo {
|
||||
path,
|
||||
vid: usb.0,
|
||||
pid: usb.1,
|
||||
product: usb.2,
|
||||
manufacturer: usb.3,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Resolve a tty path (following the /dev/mesh-radio symlink) to its USB
|
||||
/// device sysfs node and read idVendor/idProduct/product/manufacturer.
|
||||
/// Best-effort: any miss returns None fields (e.g. non-USB UARTs).
|
||||
async fn usb_info_for_tty(
|
||||
path: &str,
|
||||
) -> (
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
) {
|
||||
let resolved = tokio::fs::canonicalize(path)
|
||||
.await
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from(path));
|
||||
let Some(name) = resolved.file_name().and_then(|n| n.to_str()) else {
|
||||
return (None, None, None, None);
|
||||
};
|
||||
// /sys/class/tty/<name>/device -> .../usbN/N-M/N-M:1.0/(ttyUSBx|tty). Walk
|
||||
// up from the device link until a directory with idVendor appears.
|
||||
let mut dir = std::path::PathBuf::from(format!("/sys/class/tty/{name}/device"));
|
||||
for _ in 0..6 {
|
||||
if tokio::fs::metadata(dir.join("idVendor")).await.is_ok() {
|
||||
let read = |f: &str| {
|
||||
let p = dir.join(f);
|
||||
async move {
|
||||
tokio::fs::read_to_string(p)
|
||||
.await
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
};
|
||||
return (
|
||||
read("idVendor").await,
|
||||
read("idProduct").await,
|
||||
read("product").await,
|
||||
read("manufacturer").await,
|
||||
);
|
||||
}
|
||||
dir.push("..");
|
||||
let Ok(canon) = tokio::fs::canonicalize(&dir).await else {
|
||||
break;
|
||||
};
|
||||
dir = canon;
|
||||
}
|
||||
(None, None, None, None)
|
||||
}
|
||||
|
||||
/// Try to open and handshake with each detected serial device.
|
||||
/// Returns the first device that responds as Meshcore.
|
||||
pub async fn probe_for_meshcore(paths: &[String]) -> Option<(String, DeviceInfo)> {
|
||||
|
||||
@ -2632,20 +2632,32 @@ app.post('/rpc/v1', (req, res) => {
|
||||
// =====================================================================
|
||||
case 'mesh.status': {
|
||||
globalThis.__meshHeaders ||= { announce_block_headers: false, receive_block_headers: true }
|
||||
// Stateful enable/disable so the dev UI can demo the global
|
||||
// "mesh device detected" setup modal: disable mesh on the Mesh page
|
||||
// and the modal fires (device present but not connected).
|
||||
globalThis.__meshCfg ||= { enabled: true, lora_region: 'EU_868', device_kind: null, channel_name: 'archipelago', advert_name: 'archy-228' }
|
||||
const mc = globalThis.__meshCfg
|
||||
return res.json({
|
||||
result: {
|
||||
enabled: true,
|
||||
device_type: 'Meshcore',
|
||||
enabled: mc.enabled,
|
||||
device_type: mc.enabled ? 'Meshcore' : 'unknown',
|
||||
device_path: '/dev/ttyUSB0',
|
||||
device_connected: true,
|
||||
firmware_version: '2.3.1',
|
||||
self_node_id: 42,
|
||||
self_advert_name: 'archy-228',
|
||||
peer_count: 4,
|
||||
channel_name: 'archipelago',
|
||||
device_connected: mc.enabled,
|
||||
firmware_version: mc.enabled ? '2.3.1' : null,
|
||||
self_node_id: mc.enabled ? 42 : null,
|
||||
self_advert_name: mc.advert_name,
|
||||
peer_count: mc.enabled ? 4 : 0,
|
||||
channel_name: mc.channel_name,
|
||||
messages_sent: 23,
|
||||
messages_received: 47,
|
||||
detected_devices: ['/dev/ttyUSB0'],
|
||||
device_present: true,
|
||||
detected_device_info: [
|
||||
{ path: '/dev/ttyUSB0', vid: '10c4', pid: 'ea60', product: 'HELTEC LoRa 32 V3', manufacturer: 'Heltec' },
|
||||
],
|
||||
lora_region: mc.lora_region,
|
||||
device_kind: mc.device_kind,
|
||||
region: mc.enabled ? mc.lora_region : null,
|
||||
announce_block_headers: globalThis.__meshHeaders.announce_block_headers,
|
||||
receive_block_headers: globalThis.__meshHeaders.receive_block_headers,
|
||||
},
|
||||
@ -2756,7 +2768,16 @@ app.post('/rpc/v1', (req, res) => {
|
||||
globalThis.__meshHeaders ||= { announce_block_headers: false, receive_block_headers: true }
|
||||
if (params && typeof params.announce_block_headers === 'boolean') globalThis.__meshHeaders.announce_block_headers = params.announce_block_headers
|
||||
if (params && typeof params.receive_block_headers === 'boolean') globalThis.__meshHeaders.receive_block_headers = params.receive_block_headers
|
||||
return res.json({ result: { configured: true, ...globalThis.__meshHeaders } })
|
||||
// Stateful radio settings (see mesh.status) — lets the dev UI demo
|
||||
// the detected-device modal + the Device panel settings round-trip.
|
||||
globalThis.__meshCfg ||= { enabled: true, lora_region: 'EU_868', device_kind: null, channel_name: 'archipelago', advert_name: 'archy-228' }
|
||||
const cfg = globalThis.__meshCfg
|
||||
if (params && typeof params.enabled === 'boolean') cfg.enabled = params.enabled
|
||||
if (params && typeof params.lora_region === 'string') cfg.lora_region = params.lora_region || null
|
||||
if (params && typeof params.device_kind === 'string') cfg.device_kind = params.device_kind === 'auto' ? null : params.device_kind
|
||||
if (params && typeof params.channel_name === 'string') cfg.channel_name = params.channel_name
|
||||
if (params && typeof params.advert_name === 'string') cfg.advert_name = params.advert_name
|
||||
return res.json({ result: { configured: true, enabled: cfg.enabled, lora_region: cfg.lora_region, device_kind: cfg.device_kind, ...globalThis.__meshHeaders } })
|
||||
}
|
||||
|
||||
case 'mesh.send-invoice': {
|
||||
|
||||
1
neode-ui/public/assets/img/mesh-devices/diy.svg
Normal file
|
After Width: | Height: | Size: 96 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="795.27 277.13 409.46 1319.35"><defs><style>.cls-1{fill:#353535;}.cls-2{fill:#1e1e1d;}.cls-3{fill:#b1a368;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8,.cls-9{fill:none;}.cls-4,.cls-6{stroke:#050606;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8{stroke-miterlimit:10;}.cls-4{stroke-width:2.41px;}.cls-5{fill:#30c2db;}.cls-6{stroke-width:3.91px;}.cls-7{fill:#dcf0f2;}.cls-10,.cls-11,.cls-8{stroke:#dcf0f2;}.cls-8{stroke-width:1.81px;}.cls-9{stroke:#17afbf;stroke-linecap:round;stroke-linejoin:round;stroke-width:7.23px;}.cls-10{stroke-width:1.78px;}.cls-11{stroke-width:1.81px;}</style></defs><g id="Layer_7" data-name="Layer 7"><path class="cls-1" fill="#353535" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-2" fill="#1e1e1d" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-2" fill="#1e1e1d" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-3" fill="#b1a368" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-3" fill="#b1a368" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-3" fill="#b1a368" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-3" fill="#b1a368" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-3" fill="#b1a368" x="885.8" y="715.74" width="84.14" height="7.28"></rect><rect class="cls-4" stroke-width="2.41px" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-4" stroke-width="2.41px" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-4" stroke-width="2.41px" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-4" stroke-width="2.41px" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-4" stroke-width="2.41px" x="885.8" y="715.74" width="84.14" height="7.28"></rect><path class="cls-4" stroke-width="2.41px" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-4" stroke-width="2.41px" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-4" stroke-width="2.41px" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-5" fill="#30c2db" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><rect class="cls-1" fill="#353535" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-6" stroke-width="3.91px" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-7" fill="#dcf0f2" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-7" fill="#dcf0f2" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" stroke-width="2.41px" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" stroke-width="2.41px" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><circle class="cls-8" stroke-width="1.81px" cx="841.7" cy="1537.01" r="16.25"></circle><circle class="cls-8" stroke-width="1.81px" cx="841.7" cy="913.26" r="16.25"></circle><circle class="cls-8" stroke-width="1.81px" cx="1157.32" cy="913.26" r="16.25"></circle><circle class="cls-8" stroke-width="1.81px" cx="1157.32" cy="1504.51" r="16.25"></circle><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="942.51" y1="1592.42" x2="942.51" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="966.52" y1="1592.42" x2="966.52" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="990.57" y1="1592.42" x2="990.57" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="1014.59" y1="1592.42" x2="1014.59" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="1038.63" y1="1592.42" x2="1038.63" y2="1381.55"></line><line class="cls-9" stroke="#17afbf" stroke-width="7.23px" stroke-linecap="round" stroke-linejoin="round" x1="1062.65" y1="1592.42" x2="1062.65" y2="1381.55"></line><rect class="cls-4" stroke-width="2.41px" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><path class="cls-10" stroke-width="1.78px" d="M1040.1,947.74H960.65A13.93,13.93,0,0,1,947,936.64l-10.23-49.2a13.93,13.93,0,0,1,13.64-16.77h97.72a13.93,13.93,0,0,1,13.75,16.18l-8,49.2A13.94,13.94,0,0,1,1040.1,947.74Z"></path><rect class="cls-11" stroke-width="1.81px" x="816.35" y="870.67" width="365.51" height="703.12" rx="32.37"></rect><rect class="cls-11" stroke-width="1.81px" x="888.77" y="963.84" width="223.2" height="374.66" rx="25.21"></rect></g></svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="404.68 390.65 1217.15 959.26"><defs><style>.cls-1{fill:#dfeaf7;}.cls-2{fill:#17907f;}.cls-3{fill:#2b2b2b;}.cls-4,.cls-5{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-4{stroke-width:2.25px;}.cls-5{stroke-width:4px;}.cls-6{fill:#050606;}</style></defs><g id="Layer_5" data-name="Layer 5"><path class="cls-1" fill="#dfeaf7" d="M1517.73,392.65h0a102.1,102.1,0,0,0-102.1,102.1V770.37A40.62,40.62,0,0,1,1375,811H455.16a48.49,48.49,0,0,0-48.48,48.48v126a11.85,11.85,0,0,0,3.46,8.37l15.34,15.34a11.81,11.81,0,0,1,3.47,8.37v137.16a11.81,11.81,0,0,1-3.47,8.37l-15.34,15.34a11.85,11.85,0,0,0-3.46,8.37v112.67a48.49,48.49,0,0,0,48.48,48.49H1571.34a48.51,48.51,0,0,0,48.49-48.5V494.75A102.1,102.1,0,0,0,1517.73,392.65Zm-110.61,815V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><path class="cls-2" fill="#17907f" d="M1516,439.16c-30.23.91-53.92,26.54-53.92,56.79V770.37A87.11,87.11,0,0,1,1375,857.48H732.31A27.51,27.51,0,0,0,704.8,885v388.93a27.51,27.51,0,0,0,27.51,27.51h828.38a12.7,12.7,0,0,0,12.65-12.65v-794A55.69,55.69,0,0,0,1516,439.16Zm-108.9,768.47V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><rect class="cls-3" fill="#2b2b2b" x="787.14" y="943.38" width="429.45" height="224.42"></rect><path class="cls-1" fill="#dfeaf7" d="M1478.6,915.35A54.23,54.23,0,0,0,1386,953.69v254.23a54.23,54.23,0,1,0,108.45,0V953.69A54,54,0,0,0,1478.6,915.35Zm-5.21,292.28a33.14,33.14,0,0,1-66.27,0V954a33.14,33.14,0,0,1,66.27,0Z"></path></g><g id="Layer_2" data-name="Layer 2"><path class="cls-4" stroke-width="2.25px" d="M1573.34,494.75v794a12.68,12.68,0,0,1-12.65,12.65H732.31a27.51,27.51,0,0,1-27.51-27.51V885a27.51,27.51,0,0,1,27.51-27.51H1375a87.11,87.11,0,0,0,87.11-87.11V496c0-30.25,23.69-55.88,53.92-56.79A55.69,55.69,0,0,1,1573.34,494.75Z"></path><path class="cls-5" stroke-width="4px" d="M410.14,1178.39,425.49,1163a11.78,11.78,0,0,0,3.46-8.35V1017.5a11.8,11.8,0,0,0-3.46-8.35l-15.35-15.36a11.77,11.77,0,0,1-3.46-8.35v-126A48.47,48.47,0,0,1,455.16,811H1375a40.63,40.63,0,0,0,40.63-40.63V494.75a102.1,102.1,0,0,1,102.1-102.1h0a102.1,102.1,0,0,1,102.1,102.1v804.66a48.51,48.51,0,0,1-48.49,48.5H455.16a48.48,48.48,0,0,1-48.48-48.49V1186.74A11.78,11.78,0,0,1,410.14,1178.39Z"></path><rect class="cls-4" stroke-width="2.25px" x="1407.12" y="920.85" width="66.26" height="319.9" rx="33.13"></rect><rect class="cls-4" stroke-width="2.25px" x="1386.03" y="899.46" width="108.46" height="362.69" rx="54.23"></rect><path class="cls-6" fill="#050606" d="M639.76,1070.55a2.91,2.91,0,0,1-2.91-2.91v-30.53a5.42,5.42,0,0,0-1.6-3.86l-32.44-32.44a11.86,11.86,0,0,1-3.5-8.44V901a12.52,12.52,0,0,0-12.51-12.51H483.92a12.7,12.7,0,0,0-12.68,12.69v76.78a24.13,24.13,0,0,0,7.11,17.18l14.33,14.33a24.13,24.13,0,0,0,17.18,7.11h50.75a11.86,11.86,0,0,1,8.44,3.5l24.26,24.26a12.47,12.47,0,0,1,3.68,8.88v14.46a2.91,2.91,0,1,1-5.81,0v-14.46a6.72,6.72,0,0,0-2-4.77l-24.26-24.26a6.09,6.09,0,0,0-4.33-1.8H509.86a29.91,29.91,0,0,1-21.29-8.81l-14.33-14.33a29.87,29.87,0,0,1-8.81-21.29V901.14a18.51,18.51,0,0,1,18.49-18.5H586.8A18.34,18.34,0,0,1,605.12,901v91.41a6.09,6.09,0,0,0,1.8,4.33l32.44,32.44a11.19,11.19,0,0,1,3.3,8v30.53A2.9,2.9,0,0,1,639.76,1070.55Z"></path><path class="cls-6" fill="#050606" d="M586.8,1289.46H483.92a18.51,18.51,0,0,1-18.49-18.5v-76.78a29.87,29.87,0,0,1,8.81-21.29l14.33-14.34a29.91,29.91,0,0,1,21.29-8.81h50.75a6.09,6.09,0,0,0,4.33-1.8l24.26-24.25a6.74,6.74,0,0,0,2-4.78v-14.46a2.91,2.91,0,0,1,5.81,0v14.46a12.51,12.51,0,0,1-3.68,8.89l-24.26,24.25a11.86,11.86,0,0,1-8.44,3.5H509.86a24.17,24.17,0,0,0-17.18,7.11L478.35,1177a24.09,24.09,0,0,0-7.11,17.18V1271a12.71,12.71,0,0,0,12.68,12.69H586.8a12.53,12.53,0,0,0,12.51-12.52v-91.4a11.83,11.83,0,0,1,3.5-8.44l32.44-32.44a5.46,5.46,0,0,0,1.6-3.87v-30.53a2.91,2.91,0,0,1,5.81,0V1135a11.19,11.19,0,0,1-3.3,8l-32.44,32.45a6.06,6.06,0,0,0-1.8,4.33v91.4A18.35,18.35,0,0,1,586.8,1289.46Z"></path><rect class="cls-4" stroke-width="2.25px" x="787.14" y="943.38" width="429.45" height="224.42"></rect></g></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1
neode-ui/public/assets/img/mesh-devices/heltec-v3.svg
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 46 KiB |
1
neode-ui/public/assets/img/mesh-devices/heltec_v4.svg
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
3514
neode-ui/public/assets/img/mesh-devices/rak4631.svg
Normal file
|
After Width: | Height: | Size: 137 KiB |
1
neode-ui/public/assets/img/mesh-devices/rak4631_case.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
neode-ui/public/assets/img/mesh-devices/rpipicow.svg
Normal file
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 32 KiB |
1
neode-ui/public/assets/img/mesh-devices/station-g2.svg
Normal file
|
After Width: | Height: | Size: 34 KiB |
1
neode-ui/public/assets/img/mesh-devices/t-deck.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
1
neode-ui/public/assets/img/mesh-devices/t-echo.svg
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
502
neode-ui/public/assets/img/mesh-devices/t-echo_plus.svg
Normal file
@ -0,0 +1,502 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 293.56501 470.885"
|
||||
version="1.1"
|
||||
id="svg207"
|
||||
sodipodi:docname="buyer benmeshtastic 2-03.svg"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
width="293.565"
|
||||
height="470.88501"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview207"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96807266"
|
||||
inkscape:cx="186.96944"
|
||||
inkscape:cy="370.32344"
|
||||
inkscape:window-width="1728"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="33"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_2" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<style
|
||||
id="style1">.cls-1{fill:#474746;}.cls-2{fill:#3a3a3a;}.cls-3{fill:#202121;}.cls-4{fill:#dbdcdd;}.cls-5{fill:#cbcbcc;}.cls-11,.cls-16,.cls-18,.cls-6{fill:none;}.cls-11,.cls-16,.cls-6{stroke:#1d1d1b;stroke-linecap:round;stroke-linejoin:round;}.cls-6{stroke-width:0.95px;}.cls-7{fill:#e5e2e5;}.cls-8{fill:#444e85;}.cls-21,.cls-22,.cls-9{fill:#1d1d1b;}.cls-10{fill:#d1b066;}.cls-11{stroke-width:1.3px;}.cls-12{fill:#d8b258;}.cls-13{fill:#b28e45;}.cls-14{fill:#a6a9af;}.cls-15{fill:#d1c854;}.cls-16{stroke-width:1.01px;}.cls-17{fill:#fff;}.cls-18{stroke:#fff;stroke-miterlimit:10;}.cls-19{fill:#ba9c61;}.cls-20{fill:#715c34;}.cls-21{font-size:13.29px;}.cls-21,.cls-22{font-family:LucidaConsole, Lucida Console;}.cls-22{font-size:8.44px;}</style>
|
||||
</defs>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
transform="translate(-106.20269,-157.045)">
|
||||
<path
|
||||
class="cls-1" fill="#474746"
|
||||
d="m 396.14,293.93 v 311.36 a 22,22 0 0 1 -22,22 H 128.65 a 22,22 0 0 1 -22,-22 v -225.7 a 22,22 0 0 1 22,-22 h 69.14 a 22,22 0 0 0 22,-22 v -41.6 a 22,22 0 0 1 22,-22 h 132.32 a 22,22 0 0 1 22.03,21.94 z"
|
||||
id="path1" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 358.92,384 a 23.67,23.67 0 0 1 23.64,23.64 v 176.6 a 23.67,23.67 0 0 1 -23.64,23.64 H 147.31 A 23.67,23.67 0 0 1 123.67,584.24 V 407.61 A 23.67,23.67 0 0 1 147.31,384 h 211.61 m 0,-5 H 147.31 a 28.64,28.64 0 0 0 -28.64,28.64 v 176.6 a 28.65,28.65 0 0 0 28.64,28.64 h 211.61 a 28.64,28.64 0 0 0 28.64,-28.64 V 407.61 A 28.64,28.64 0 0 0 358.92,379 Z"
|
||||
id="path2" />
|
||||
<rect
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
x="237.61"
|
||||
y="284.09"
|
||||
width="139.52"
|
||||
height="79.300003"
|
||||
rx="8.3900003"
|
||||
id="rect2" />
|
||||
<rect
|
||||
class="cls-3" fill="#202121"
|
||||
x="244.67999"
|
||||
y="289.66"
|
||||
width="125.32"
|
||||
height="67.370003"
|
||||
rx="2"
|
||||
id="rect3" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="M 151.55,352.34 H 194 a 3.83,3.83 0 0 1 3.83,3.83 v 1.39 h -50.11 v -1.39 a 3.83,3.83 0 0 1 3.83,-3.83 z"
|
||||
id="path3" />
|
||||
<path
|
||||
class="cls-1" fill="#474746"
|
||||
d="m 240,272 a 3,3 0 0 1 3,-2.69 h 24.48 a 3,3 0 0 1 3,2.61 L 240.25,272"
|
||||
id="path4" />
|
||||
<path
|
||||
class="cls-4" fill="#dbdcdd"
|
||||
d="m 353.11,172.61 a 54.23,54.23 0 0 0 -32.38,-10.67 h -49 a 54.49,54.49 0 0 0 -54.49,54.48 V 282 h -5.21 v -70 a 54.49,54.49 0 0 1 54.49,-54.48 h 49 a 54.24,54.24 0 0 1 37.59,15.09 z"
|
||||
id="path5" />
|
||||
<path
|
||||
class="cls-5" fill="#cbcbcc"
|
||||
d="m 370,212 v 51.84 h -21.49 l -9.24,-46.17 V 209 A 19.5,19.5 0 0 0 319.79,189.52 H 260.87 A 19.48,19.48 0 0 0 241.39,209 v 60.74 c -1.23,0.56 -0.86,2.13 -0.86,2.13 l -6.91,1.75 c -5.66,1.43 -7.63,4.78 -9.44,7 -1.81,2.22 -2,1.41 -2,1.41 h -4.89 v -65.61 a 54.49,54.49 0 0 1 54.49,-54.48 h 49 a 54.23,54.23 0 0 1 32.38,10.67 v 0 A 54.3,54.3 0 0 1 370,212 Z"
|
||||
id="path6" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 396.42,323.74 h 1.21 a 1.46,1.46 0 0 1 1.46,1.46 v 2.17 a 1.46,1.46 0 0 1 -1.46,1.46 h -1.21 z"
|
||||
id="path7" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="M 329.33,586.79 H 198 A 24.51,24.51 0 0 1 173.52,562.31 V 429 A 24.5,24.5 0 0 1 198,404.51 H 329.33 A 24.49,24.49 0 0 1 353.8,429 v 133.31 a 24.5,24.5 0 0 1 -24.47,24.48 z m -128.81,-27 H 326.8 V 431.51 H 200.52 Z"
|
||||
id="path8" />
|
||||
<rect
|
||||
class="cls-7" fill="#e5e2e5"
|
||||
x="187.02"
|
||||
y="418.01001"
|
||||
width="153.28"
|
||||
height="155.27"
|
||||
rx="10.97"
|
||||
id="rect8" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 166.62,386.56 a 12.85,12.85 0 1 0 12.86,12.85 12.85,12.85 0 0 0 -12.86,-12.85 z m 0,21 a 8.15,8.15 0 1 1 8.15,-8.15 8.14,8.14 0 0 1 -8.15,8.15 z"
|
||||
id="path9" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 174.77,399.41 a 8.15,8.15 0 1 1 -8.15,-8.15 8.15,8.15 0 0 1 8.15,8.15 z"
|
||||
id="path10" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 359.9,386.56 a 12.85,12.85 0 1 0 12.86,12.85 12.85,12.85 0 0 0 -12.86,-12.85 z m 0,21 a 8.15,8.15 0 1 1 8.15,-8.15 8.14,8.14 0 0 1 -8.15,8.15 z"
|
||||
id="path11" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 368.05,399.41 a 8.15,8.15 0 1 1 -8.15,-8.15 8.15,8.15 0 0 1 8.15,8.15 z"
|
||||
id="path12" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 359.9,579.56 a 12.86,12.86 0 1 0 12.86,12.85 12.85,12.85 0 0 0 -12.86,-12.85 z m 0,21 a 8.15,8.15 0 1 1 8.15,-8.15 8.14,8.14 0 0 1 -8.15,8.15 z"
|
||||
id="path13" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 368.05,592.41 a 8.15,8.15 0 1 1 -8.15,-8.14 8.15,8.15 0 0 1 8.15,8.14 z"
|
||||
id="path14" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 166.62,579.56 a 12.86,12.86 0 1 0 12.86,12.85 12.86,12.86 0 0 0 -12.86,-12.85 z m 0,21 a 8.15,8.15 0 1 1 8.15,-8.15 8.14,8.14 0 0 1 -8.15,8.15 z"
|
||||
id="path15" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 174.77,592.41 a 8.15,8.15 0 1 1 -8.15,-8.14 8.15,8.15 0 0 1 8.15,8.14 z"
|
||||
id="path16" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 253.12,315.37 h 5.46 v 15.27 h 8.51 v 5 h -14 z"
|
||||
id="path17" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 269.79,315.37 h 5.48 v 20.25 h -5.48 z"
|
||||
id="path18" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 279.18,315.37 h 5.46 v 15.27 h 8.52 v 5 h -14 z"
|
||||
id="path19" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 292,315.37 h 6 l 3.56,6.84 3.56,-6.84 h 6 l -6.87,11.77 v 8.48 h -5.47 v -8.48 z"
|
||||
id="path20" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 320.33,328.29 v -4.21 h 8.44 v 8.63 a 16.91,16.91 0 0 1 -4.28,2.58 13,13 0 0 1 -4.42,0.68 9.6,9.6 0 0 1 -5.13,-1.23 8,8 0 0 1 -3.07,-3.66 13.45,13.45 0 0 1 -1.09,-5.58 13.15,13.15 0 0 1 1.19,-5.77 8.3,8.3 0 0 1 3.5,-3.72 10,10 0 0 1 4.83,-1 11.86,11.86 0 0 1 4.38,0.61 5.69,5.69 0 0 1 2.41,1.88 8.8,8.8 0 0 1 1.44,3.24 l -5.27,1.08 a 3.24,3.24 0 0 0 -1.1,-1.76 3.15,3.15 0 0 0 -2,-0.6 3.36,3.36 0 0 0 -2.86,1.43 7.54,7.54 0 0 0 -1.07,4.52 c 0,2.19 0.36,3.76 1.08,4.7 a 3.57,3.57 0 0 0 3,1.41 5.11,5.11 0 0 0 1.75,-0.31 9.16,9.16 0 0 0 1.91,-1 v -1.91 z"
|
||||
id="path21" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 331.32,325.51 c 0,-3.3 0.81,-5.88 2.41,-7.72 a 8.46,8.46 0 0 1 6.72,-2.76 8.55,8.55 0 0 1 6.79,2.71 q 2.38,2.72 2.39,7.61 a 14.13,14.13 0 0 1 -1,5.82 8.19,8.19 0 0 1 -3,3.54 8.93,8.93 0 0 1 -4.91,1.26 10,10 0 0 1 -4.94,-1.09 8,8 0 0 1 -3.18,-3.45 12.89,12.89 0 0 1 -1.28,-5.92 z m 5.46,0 a 7.49,7.49 0 0 0 1,4.41 3.44,3.44 0 0 0 5.43,0 q 1,-1.31 1,-4.71 a 6.89,6.89 0 0 0 -1,-4.17 3.25,3.25 0 0 0 -2.73,-1.32 3.13,3.13 0 0 0 -2.65,1.34 7.6,7.6 0 0 0 -1.05,4.48 z"
|
||||
id="path22" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 356,322.64 v -4.46 h 1.89 a 3.22,3.22 0 0 1 1,0.12 1,1 0 0 1 0.52,0.43 1.3,1.3 0 0 1 0.2,0.7 1.18,1.18 0 0 1 -0.3,0.83 1.44,1.44 0 0 1 -0.88,0.42 2,2 0 0 1 0.48,0.37 5,5 0 0 1 0.51,0.72 l 0.55,0.87 h -1.08 l -0.65,-1 a 8,8 0 0 0 -0.47,-0.65 0.82,0.82 0 0 0 -0.28,-0.19 1.54,1.54 0 0 0 -0.45,-0.05 h -0.18 v 1.86 z m 0.9,-2.57 h 0.66 a 3.91,3.91 0 0 0 0.81,-0.05 0.51,0.51 0 0 0 0.26,-0.19 0.59,0.59 0 0 0 0.09,-0.34 0.52,0.52 0 0 0 -0.12,-0.36 0.62,0.62 0 0 0 -0.34,-0.18 h -1.36 z"
|
||||
id="path23" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 357.66,326.28 a 5.71,5.71 0 1 1 5.71,-5.71 5.71,5.71 0 0 1 -5.71,5.71 z m 0,-10.12 a 4.42,4.42 0 1 0 4.41,4.41 4.41,4.41 0 0 0 -4.41,-4.41 z"
|
||||
id="path24" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 135.34,431.15 h 25.89 v 5.73 h -21.49 v 14.25 h -4.4 z"
|
||||
id="path25" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 135.34,455 h 26.1 v 5.73 h -26.1 z"
|
||||
id="path26" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 135.34,466.39 h 25.89 v 5.73 h -21.49 v 14.25 h -4.4 z"
|
||||
id="path27" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 135.34,494.27 h 11 l 15.11,-10.4 v 6.72 l -10.32,6.68 10.32,6.55 v 6.6 L 146.29,500 h -10.95 z"
|
||||
id="path28" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 144.94,526.48 h 4.4 v 12.35 h -10.4 a 15.78,15.78 0 0 1 -2.82,-5.22 20.31,20.31 0 0 1 -1.22,-6.92 16.61,16.61 0 0 1 1.72,-7.77 11.34,11.34 0 0 1 4.91,-5 14.83,14.83 0 0 1 6.95,-1.67 14,14 0 0 1 7.25,1.86 12.12,12.12 0 0 1 4.86,5.44 15.74,15.74 0 0 1 1.3,6.8 q 0,5.28 -2,8.26 a 9.16,9.16 0 0 1 -5.64,3.82 l -1,-5.69 a 5.51,5.51 0 0 0 3,-2.26 7.25,7.25 0 0 0 1.11,-4.13 8.14,8.14 0 0 0 -2.19,-6 q -2.19,-2.21 -6.49,-2.21 c -3.1,0 -5.43,0.75 -7,2.24 a 7.82,7.82 0 0 0 -2.33,5.89 10.72,10.72 0 0 0 0.65,3.61 12.93,12.93 0 0 0 1.58,3.11 h 3.31 z"
|
||||
id="path29" />
|
||||
<path
|
||||
class="cls-9" fill="#1d1d1b"
|
||||
d="m 148.23,543 a 15.54,15.54 0 0 1 6.7,1.3 12.53,12.53 0 0 1 3.58,2.64 11.41,11.41 0 0 1 2.35,3.67 17,17 0 0 1 1,6.12 q 0,6.27 -3.58,10 -3.58,3.73 -9.95,3.77 -6.33,0 -9.89,-3.74 -3.56,-3.74 -3.57,-10 0,-6.33 3.55,-10.07 3.55,-3.74 9.81,-3.69 z m 0.18,5.91 c -2.95,0 -5.2,0.74 -6.72,2.23 a 8.12,8.12 0 0 0 0,11.28 q 2.28,2.19 6.81,2.19 4.53,0 6.7,-2.14 a 8.43,8.43 0 0 0 0,-11.39 q -2.28,-2.22 -6.79,-2.22 z"
|
||||
id="path30" />
|
||||
<path
|
||||
class="cls-1" fill="#474746"
|
||||
d="m 384.27,274.39 v -3.8 a 6.7,6.7 0 0 0 -6.71,-6.71 h -73.09 a 6.7,6.7 0 0 0 -6.71,6.71 v 1.31 c 0,0 74.61,0 76.35,0 0.32,0 6.89,-0.2 10.16,2.49 z"
|
||||
id="path67" />
|
||||
<path
|
||||
class="cls-15" fill="#d1c854"
|
||||
d="m 223.32,282 a 21.84,21.84 0 0 0 -3.5,11.91 v 41.6 a 21.88,21.88 0 0 1 -3,11 L 204.7,339.3 a 8,8 0 0 1 -3.9,-6.86 V 290 a 8,8 0 0 1 8,-8 z"
|
||||
id="path68" />
|
||||
<path
|
||||
class="cls-2" fill="#3a3a3a"
|
||||
d="m 374.11,275.9 a 18,18 0 0 1 18,18 v 311.39 a 18.05,18.05 0 0 1 -18,18 H 128.65 a 18.05,18.05 0 0 1 -18,-18 v -225.7 a 18,18 0 0 1 18,-18 h 69.14 a 26.05,26.05 0 0 0 26,-26 v -41.6 a 18,18 0 0 1 18,-18 h 132.32 m 0,-4 H 241.85 a 22,22 0 0 0 -22,22 v 41.6 a 22,22 0 0 1 -22,22 h -69.2 a 22,22 0 0 0 -22,22 v 225.7 a 22,22 0 0 0 22,22 h 245.46 a 22,22 0 0 0 22,-22 V 293.93 a 22,22 0 0 0 -22,-22 z"
|
||||
id="path69" />
|
||||
</g>
|
||||
<g
|
||||
id="Layer_7"
|
||||
data-name="Layer 7"
|
||||
transform="translate(-106,-157.045)">
|
||||
<path
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
d="m 396.14,293.93 v 311.35 a 22,22 0 0 1 -22,22 H 128.65 a 22,22 0 0 1 -22,-22 V 379.59 a 22,22 0 0 1 22,-22 h 69.14 a 22,22 0 0 0 22,-22 v -41.66 a 22,22 0 0 1 22,-22 h 132.32 a 22,22 0 0 1 22.03,22 z"
|
||||
id="path70" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="118.67"
|
||||
y="378.97"
|
||||
width="268.88"
|
||||
height="233.91"
|
||||
rx="28.639999"
|
||||
id="rect70" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="237.61"
|
||||
y="284.09"
|
||||
width="139.52"
|
||||
height="79.300003"
|
||||
rx="8.3900003"
|
||||
id="rect71" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="244.67999"
|
||||
y="289.66"
|
||||
width="125.32"
|
||||
height="67.370003"
|
||||
rx="2"
|
||||
id="rect72" />
|
||||
<path
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
d="M 151.55,352.34 H 194 a 3.83,3.83 0 0 1 3.83,3.83 v 1.39 h -50.11 v -1.39 a 3.83,3.83 0 0 1 3.83,-3.83 z"
|
||||
id="path72" />
|
||||
<path
|
||||
class="cls-16" stroke-width="1.01px"
|
||||
d="m 240,272 a 3,3 0 0 1 3,-2.69 h 24.48 a 3,3 0 0 1 3,2.61"
|
||||
id="path73" />
|
||||
<rect
|
||||
class="cls-10" fill="#d1b066"
|
||||
x="322.39001"
|
||||
y="253.42999"
|
||||
width="37.77"
|
||||
height="10.45"
|
||||
id="rect73" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,250.81 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path74" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,248.28 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path75" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,245.75 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path76" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,243.22 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path77" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,240.69 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path78" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,238.17 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.09 1,-0.63 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path79" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,235.64 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path80" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,233.11 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path81" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,230.58 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path82" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,228.05 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path83" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,225.52 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path84" />
|
||||
<path
|
||||
class="cls-10" fill="#d1b066"
|
||||
d="m 328.2,223 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.09 1,-0.63 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path85" />
|
||||
<rect
|
||||
class="cls-10" fill="#d1b066"
|
||||
x="328.20001"
|
||||
y="217.71001"
|
||||
width="26.27"
|
||||
height="4.9099998"
|
||||
id="rect85" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="322.63"
|
||||
y="253.42999"
|
||||
width="18.77"
|
||||
height="10.45"
|
||||
id="rect86" />
|
||||
<rect
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
x="341.39999"
|
||||
y="253.42999"
|
||||
width="18.77"
|
||||
height="10.45"
|
||||
id="rect87" />
|
||||
<rect
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
x="328.20001"
|
||||
y="217.71001"
|
||||
width="26.27"
|
||||
height="5.29"
|
||||
id="rect88" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,250.81 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path88" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,248.28 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path89" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,245.75 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path90" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,243.22 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path91" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,240.69 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path92" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,238.17 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.09 1,-0.63 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path93" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,235.64 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path94" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,233.11 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.63 26.39,0.1 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path95" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,230.58 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path96" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,228.05 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path97" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,225.52 -1,0.64 a 0.68,0.68 0 0 0 0,1.16 l 1,0.64 26.39,0.09 1,-0.64 a 0.68,0.68 0 0 0 0,-1.15 l -1,-0.64"
|
||||
id="path98" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 328.2,223 -1,0.64 a 0.67,0.67 0 0 0 0,1.15 l 1,0.64 26.39,0.09 1,-0.63 a 0.69,0.69 0 0 0 0,-1.16 l -1,-0.64"
|
||||
id="path99" />
|
||||
<path
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
d="m 384.27,274.39 v 0 -3.79 a 6.7,6.7 0 0 0 -6.71,-6.71 h -73.09 a 6.7,6.7 0 0 0 -6.71,6.71 v 1.31"
|
||||
id="path100" />
|
||||
<path
|
||||
class="cls-11" stroke-width="1.3px"
|
||||
d="m 223.32,282 h -14.53 a 8,8 0 0 0 -8,8 v 42.43 a 8,8 0 0 0 3.9,6.86 l 12.16,7.25"
|
||||
id="path101" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 212.06,282 v -70 a 54.49,54.49 0 0 1 54.49,-54.48 h 49 A 54.48,54.48 0 0 1 370,212 v 51.84"
|
||||
id="path102" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="M 241.39,269.77 V 209 a 19.48,19.48 0 0 1 19.48,-19.48 h 58.92 A 19.48,19.48 0 0 1 339.27,209 v 8.68"
|
||||
id="path103" />
|
||||
<path
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
d="m 396.42,323.74 h 1.21 a 1.46,1.46 0 0 1 1.46,1.46 v 2.17 a 1.46,1.46 0 0 1 -1.46,1.46 h -1.21 z"
|
||||
id="path104" />
|
||||
<rect
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
x="187.02"
|
||||
y="418.01001"
|
||||
width="153.28"
|
||||
height="155.27"
|
||||
rx="10.97"
|
||||
id="rect104" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="166.62"
|
||||
cy="399.41"
|
||||
r="8.1499996"
|
||||
id="circle140" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="166.62"
|
||||
cy="399.41"
|
||||
r="12.85"
|
||||
id="circle141" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="359.89999"
|
||||
cy="399.41"
|
||||
r="8.1499996"
|
||||
id="circle142" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="359.89999"
|
||||
cy="399.41"
|
||||
r="12.85"
|
||||
id="circle143" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="359.89999"
|
||||
cy="592.40997"
|
||||
r="8.1499996"
|
||||
id="circle144" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="359.89999"
|
||||
cy="592.40997"
|
||||
r="12.85"
|
||||
id="circle145" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="166.62"
|
||||
cy="592.40997"
|
||||
r="8.1499996"
|
||||
id="circle146" />
|
||||
<circle
|
||||
class="cls-6" stroke-width="0.95px"
|
||||
cx="166.62"
|
||||
cy="592.40997"
|
||||
r="12.85"
|
||||
id="circle147" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 253.12,315.37 h 5.46 v 15.27 h 8.51 v 5 h -14 z"
|
||||
id="path147" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 269.79,315.37 h 5.48 v 20.25 h -5.48 z"
|
||||
id="path148" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 279.18,315.37 h 5.46 v 15.27 h 8.52 v 5 h -14 z"
|
||||
id="path149" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 292,315.37 h 6 l 3.56,6.84 3.56,-6.84 h 6 l -6.87,11.77 v 8.48 h -5.47 v -8.48 z"
|
||||
id="path150" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 320.33,328.29 v -4.21 h 8.44 v 8.63 a 16.91,16.91 0 0 1 -4.28,2.58 13,13 0 0 1 -4.42,0.68 9.6,9.6 0 0 1 -5.13,-1.23 8,8 0 0 1 -3.07,-3.66 13.45,13.45 0 0 1 -1.09,-5.58 13.15,13.15 0 0 1 1.19,-5.77 8.3,8.3 0 0 1 3.5,-3.72 10,10 0 0 1 4.83,-1 11.86,11.86 0 0 1 4.38,0.61 5.69,5.69 0 0 1 2.41,1.88 8.8,8.8 0 0 1 1.44,3.24 l -5.27,1.08 a 3.24,3.24 0 0 0 -1.1,-1.76 3.15,3.15 0 0 0 -2,-0.6 3.36,3.36 0 0 0 -2.86,1.43 7.54,7.54 0 0 0 -1.07,4.52 c 0,2.19 0.36,3.76 1.08,4.7 a 3.57,3.57 0 0 0 3,1.41 5.11,5.11 0 0 0 1.75,-0.31 9.16,9.16 0 0 0 1.91,-1 v -1.91 z"
|
||||
id="path151" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 331.32,325.51 c 0,-3.3 0.81,-5.88 2.41,-7.72 a 8.46,8.46 0 0 1 6.72,-2.76 8.55,8.55 0 0 1 6.79,2.71 q 2.38,2.72 2.39,7.61 a 14.13,14.13 0 0 1 -1,5.82 8.19,8.19 0 0 1 -3,3.54 8.93,8.93 0 0 1 -4.91,1.26 10,10 0 0 1 -4.94,-1.09 8,8 0 0 1 -3.18,-3.45 12.89,12.89 0 0 1 -1.28,-5.92 z m 5.46,0 a 7.49,7.49 0 0 0 1,4.41 3.44,3.44 0 0 0 5.43,0 q 1,-1.31 1,-4.71 a 6.89,6.89 0 0 0 -1,-4.17 3.25,3.25 0 0 0 -2.73,-1.32 3.13,3.13 0 0 0 -2.65,1.34 7.6,7.6 0 0 0 -1.05,4.48 z"
|
||||
id="path152" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 356,322.64 v -4.46 h 1.89 a 3.22,3.22 0 0 1 1,0.12 1,1 0 0 1 0.52,0.43 1.3,1.3 0 0 1 0.2,0.7 1.18,1.18 0 0 1 -0.3,0.83 1.44,1.44 0 0 1 -0.88,0.42 2,2 0 0 1 0.48,0.37 5,5 0 0 1 0.51,0.72 l 0.55,0.87 h -1.08 l -0.65,-1 a 8,8 0 0 0 -0.47,-0.65 0.82,0.82 0 0 0 -0.28,-0.19 1.54,1.54 0 0 0 -0.45,-0.05 h -0.18 v 1.86 z m 0.9,-2.57 h 0.66 a 3.91,3.91 0 0 0 0.81,-0.05 0.51,0.51 0 0 0 0.26,-0.19 0.59,0.59 0 0 0 0.09,-0.34 0.52,0.52 0 0 0 -0.12,-0.36 0.62,0.62 0 0 0 -0.34,-0.18 h -1.36 z"
|
||||
id="path153" />
|
||||
<path
|
||||
class="cls-8" fill="#444e85"
|
||||
d="m 357.66,326.28 a 5.71,5.71 0 1 1 5.71,-5.71 5.71,5.71 0 0 1 -5.71,5.71 z m 0,-10.12 a 4.42,4.42 0 1 0 4.41,4.41 4.41,4.41 0 0 0 -4.41,-4.41 z"
|
||||
id="path154" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 135.34,431.15 h 25.89 v 5.73 h -21.49 v 14.25 h -4.4 z"
|
||||
id="path155" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 135.34,455 h 26.1 v 5.73 h -26.1 z"
|
||||
id="path156" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 135.34,466.39 h 25.89 v 5.73 h -21.49 v 14.25 h -4.4 z"
|
||||
id="path157" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 135.34,494.27 h 11 l 15.11,-10.4 v 6.72 l -10.32,6.68 10.32,6.55 v 6.6 L 146.29,500 h -10.95 z"
|
||||
id="path158" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 144.94,526.48 h 4.4 v 12.35 h -10.4 a 15.78,15.78 0 0 1 -2.82,-5.22 20.31,20.31 0 0 1 -1.22,-6.92 16.61,16.61 0 0 1 1.72,-7.77 11.34,11.34 0 0 1 4.91,-5 14.83,14.83 0 0 1 6.95,-1.67 14,14 0 0 1 7.25,1.86 12.12,12.12 0 0 1 4.86,5.44 15.74,15.74 0 0 1 1.3,6.8 q 0,5.28 -2,8.26 a 9.16,9.16 0 0 1 -5.64,3.82 l -1,-5.69 a 5.51,5.51 0 0 0 3,-2.26 7.25,7.25 0 0 0 1.11,-4.13 8.14,8.14 0 0 0 -2.19,-6 q -2.19,-2.21 -6.49,-2.21 c -3.1,0 -5.43,0.75 -7,2.24 a 7.82,7.82 0 0 0 -2.33,5.89 10.72,10.72 0 0 0 0.65,3.61 12.93,12.93 0 0 0 1.58,3.11 h 3.31 z"
|
||||
id="path159" />
|
||||
<path
|
||||
class="cls-3" fill="#202121"
|
||||
d="m 148.23,543 a 15.54,15.54 0 0 1 6.7,1.3 12.53,12.53 0 0 1 3.58,2.64 11.41,11.41 0 0 1 2.35,3.67 17,17 0 0 1 1,6.12 q 0,6.27 -3.58,10 -3.58,3.73 -9.95,3.77 -6.33,0 -9.89,-3.74 -3.56,-3.74 -3.57,-10 0,-6.33 3.55,-10.07 3.55,-3.74 9.81,-3.69 z m 0.18,5.91 c -2.95,0 -5.2,0.74 -6.72,2.23 a 8.12,8.12 0 0 0 0,11.28 q 2.28,2.19 6.81,2.19 4.53,0 6.7,-2.14 a 8.43,8.43 0 0 0 0,-11.39 q -2.28,-2.22 -6.79,-2.22 z"
|
||||
id="path160" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 78 KiB |
1
neode-ui/public/assets/img/mesh-devices/tbeam.svg
Normal file
|
After Width: | Height: | Size: 123 KiB |
109
neode-ui/public/assets/img/mesh-devices/thinknode_m1.svg
Normal file
@ -0,0 +1,109 @@
|
||||
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg105" sodipodi:docname="thinknode_m1.svg" inkscape:version="1.4 (e7c3feb1, 2024-10-09)" viewBox="397.31 77.24 361 863.17">
|
||||
<sodipodi:namedview id="namedview105" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="4.066786" inkscape:cx="564.94244" inkscape:cy="741.49463" inkscape:window-width="1472" inkscape:window-height="890" inkscape:window-x="0" inkscape:window-y="38" inkscape:window-maximized="1" inkscape:current-layer="Layer_3"/>
|
||||
<defs id="defs1">
|
||||
<style id="style1">.cls-1{fill:#353535;}.cls-2{fill:#262626;}.cls-3{fill:#cccccb;}.cls-4{fill:#2b2b2b;}.cls-5{fill:#f05043;}.cls-6{fill:#3d3d3d;}.cls-7{fill:#231f20;}.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}</style>
|
||||
</defs>
|
||||
<g id="Layer_3" data-name="Layer 3">
|
||||
<path class="cls-1" fill="#353535" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path1"/>
|
||||
<rect class="cls-2" fill="#262626" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect1"/>
|
||||
<rect class="cls-1" fill="#353535" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect2"/>
|
||||
<rect class="cls-3" fill="#cccccb" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect3"/>
|
||||
<path class="cls-1" fill="#353535" d="M507.38,77.74H472.16a7,7,0,0,0-7,7V359.93L452.2,396.26v39.91H561V396.26l-13.3-36V84.15a6.41,6.41,0,0,0-6.41-6.41Z" id="path3"/>
|
||||
<rect class="cls-2" fill="#262626" x="454.25" y="436.17" width="104.38" height="3.65" id="rect4"/>
|
||||
<polygon class="cls-1" fill="#353535" points="442.68 449.91 448.16 440.69 562.98 440.69 570.51 449.91 442.68 449.91" id="polygon4"/>
|
||||
<rect class="cls-1" fill="#353535" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.8" id="rect5"/>
|
||||
<path class="cls-2" fill="#262626" d="M611.2,356v-5.48a3.13,3.13,0,0,1,3.13-3.13h86.35a3.13,3.13,0,0,1,3.13,3.13V356Z" id="path5"/>
|
||||
<rect class="cls-2" fill="#262626" x="611.07" y="416.61" width="92.74" height="23.22" id="rect6"/>
|
||||
<polygon class="cls-1" fill="#353535" points="592.99 449.91 598.47 440.69 713.42 440.69 720.82 449.91 592.99 449.91" id="polygon6"/>
|
||||
<rect class="cls-2" fill="#262626" x="751.94" y="555.13" width="5.87" height="47.48" id="rect7"/>
|
||||
<path class="cls-2" fill="#262626" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path7"/>
|
||||
<path class="cls-2" fill="#262626" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path8"/>
|
||||
<path class="cls-4" fill="#2b2b2b" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path9"/>
|
||||
<rect class="cls-2" fill="#262626" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect9"/>
|
||||
<circle class="cls-5" fill="#f05043" cx="518.72" cy="853.89" r="5.48" id="circle9"/>
|
||||
<circle class="cls-1" fill="#353535" cx="640.14" cy="853.89" r="5.48" id="circle10"/>
|
||||
<circle class="cls-1" fill="#353535" cx="541.83" cy="853.89" r="5.48" id="circle11"/>
|
||||
<circle class="cls-1" fill="#353535" cx="567.67" cy="853.89" r="5.48" id="circle12"/>
|
||||
<circle class="cls-1" fill="#353535" cx="593.51" cy="853.89" r="5.48" id="circle13"/>
|
||||
<circle class="cls-1" fill="#353535" cx="616.82" cy="853.89" r="5.48" id="circle14"/>
|
||||
<path class="cls-4" fill="#2b2b2b" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path14"/>
|
||||
<path class="cls-4" fill="#2b2b2b" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path15"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path16"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path17"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path18"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path19"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path20"/>
|
||||
<path class="cls-6" fill="#3d3d3d" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path21"/>
|
||||
<path class="cls-1" fill="#353535" d="M477,924.32h-3V903.09h-7.46v-2.65h17.9v2.65H477Z" id="path51"/>
|
||||
<path class="cls-1" fill="#353535" d="M490.59,906.36c0,.43,0,.86,0,1.31s-.07.85-.12,1.2h.2a5.17,5.17,0,0,1,1.44-1.54,7.08,7.08,0,0,1,1.94-.92,7.81,7.81,0,0,1,2.21-.31,8.61,8.61,0,0,1,3.63.68,4.62,4.62,0,0,1,2.19,2.13,8.22,8.22,0,0,1,.73,3.74v11.67h-2.9V912.85a4.72,4.72,0,0,0-1-3.24,3.92,3.92,0,0,0-3.05-1.07,5.65,5.65,0,0,0-3.14.75,4.07,4.07,0,0,0-1.62,2.21,11.17,11.17,0,0,0-.49,3.56v9.26h-2.94V898.91h2.94Z" id="path52"/>
|
||||
<path class="cls-1" fill="#353535" d="M509.82,899.67a1.73,1.73,0,0,1,1.19.46,2.17,2.17,0,0,1,0,2.82,1.74,1.74,0,0,1-1.19.47,1.77,1.77,0,0,1-1.24-.47,2.24,2.24,0,0,1,0-2.82A1.76,1.76,0,0,1,509.82,899.67Zm1.44,6.73v17.92h-2.94V906.4Z" id="path53"/>
|
||||
<path class="cls-1" fill="#353535" d="M525.58,906.06a6.8,6.8,0,0,1,4.85,1.56c1.09,1,1.63,2.71,1.63,5v11.67h-2.91V912.85a4.67,4.67,0,0,0-1-3.24,3.88,3.88,0,0,0-3-1.07c-2,0-3.36.56-4.11,1.67a8.54,8.54,0,0,0-1.14,4.82v9.29H517V906.4h2.37l.44,2.44h.16a5.68,5.68,0,0,1,1.49-1.56,6.41,6.41,0,0,1,2-.92A8.13,8.13,0,0,1,525.58,906.06Z" id="path54"/>
|
||||
<path class="cls-1" fill="#353535" d="M540.53,912.18c0,.36,0,.83,0,1.41s-.07,1.08-.09,1.5h.14l.6-.77.82-1c.28-.34.52-.63.72-.85l5.72-6.05h3.44l-7.26,7.66,7.76,10.26h-3.54L542.57,916l-2,1.78v6.58h-2.91V898.91h2.91Z" id="path55"/>
|
||||
<path class="cls-1" fill="#353535" d="M574.81,924.32H571.3l-12.78-19.83h-.13c0,.51.08,1.12.11,1.82s.07,1.45.1,2.24,0,1.61,0,2.43v13.34h-2.77V900.44h3.48l12.74,19.77h.13c0-.36-.05-.89-.08-1.6s-.07-1.5-.1-2.35,0-1.62,0-2.34V900.44h2.81Z" id="path56"/>
|
||||
<path class="cls-1" fill="#353535" d="M596.48,915.33a12.32,12.32,0,0,1-.58,4,8.32,8.32,0,0,1-1.67,2.93,7,7,0,0,1-2.65,1.82,9.31,9.31,0,0,1-3.46.62,8.63,8.63,0,0,1-3.28-.62,7.29,7.29,0,0,1-2.61-1.82,8.57,8.57,0,0,1-1.72-2.93,11.76,11.76,0,0,1-.62-4,11.43,11.43,0,0,1,1-5,7.12,7.12,0,0,1,2.87-3.14,8.76,8.76,0,0,1,4.45-1.09,8.4,8.4,0,0,1,4.3,1.09,7.45,7.45,0,0,1,2.91,3.14A11,11,0,0,1,596.48,915.33Zm-13.54,0a10.93,10.93,0,0,0,.55,3.66,4.82,4.82,0,0,0,1.72,2.39,5.69,5.69,0,0,0,5.95,0,4.78,4.78,0,0,0,1.73-2.39,10.93,10.93,0,0,0,.55-3.66,10.36,10.36,0,0,0-.57-3.65,4.84,4.84,0,0,0-1.72-2.32,5,5,0,0,0-3-.82,4.5,4.5,0,0,0-4,1.8A8.79,8.79,0,0,0,582.94,915.33Z" id="path57"/>
|
||||
<path class="cls-1" fill="#353535" d="M607.49,924.66a6.67,6.67,0,0,1-5.35-2.33c-1.34-1.54-2-3.86-2-6.94s.67-5.4,2-7a6.74,6.74,0,0,1,5.37-2.36,7.71,7.71,0,0,1,2.44.35,6.37,6.37,0,0,1,1.81,1,6.58,6.58,0,0,1,1.3,1.34h.2c0-.29-.06-.72-.11-1.29s-.09-1-.09-1.36v-7.15H616v25.41h-2.38l-.43-2.4h-.14a6.51,6.51,0,0,1-1.3,1.38,5.9,5.9,0,0,1-1.82,1A7.4,7.4,0,0,1,607.49,924.66Zm.46-2.44c1.9,0,3.23-.52,4-1.56a7.84,7.84,0,0,0,1.16-4.7v-.53a9.84,9.84,0,0,0-1.11-5.14c-.73-1.19-2.09-1.79-4.08-1.79a4,4,0,0,0-3.56,1.89,9.46,9.46,0,0,0-1.19,5.07,9,9,0,0,0,1.19,5A4,4,0,0,0,608,922.22Z" id="path58"/>
|
||||
<path class="cls-1" fill="#353535" d="M628.62,906.06a7.53,7.53,0,0,1,4,1,6.62,6.62,0,0,1,2.54,2.82,9.69,9.69,0,0,1,.89,4.27v1.77H623.74a6.75,6.75,0,0,0,1.56,4.63,5.42,5.42,0,0,0,4.16,1.59,12.58,12.58,0,0,0,3-.32,16.78,16.78,0,0,0,2.72-.92v2.58a13.84,13.84,0,0,1-2.71.89,16.15,16.15,0,0,1-3.17.28,9.46,9.46,0,0,1-4.5-1,7.15,7.15,0,0,1-3-3.09,10.61,10.61,0,0,1-1.09-5,12,12,0,0,1,1-5,7.33,7.33,0,0,1,6.94-4.38Zm0,2.41a4.26,4.26,0,0,0-3.33,1.36,6.34,6.34,0,0,0-1.45,3.76h9.13a7.05,7.05,0,0,0-.47-2.68,3.94,3.94,0,0,0-1.42-1.79A4.33,4.33,0,0,0,628.59,908.47Z" id="path59"/>
|
||||
<path class="cls-1" fill="#353535" d="M639.36,913h8.1v2.74h-8.1Z" id="path60"/>
|
||||
<path class="cls-1" fill="#353535" d="M662.87,924.32,655,903.39h-.13c0,.44.08,1,.12,1.7s.06,1.45.08,2.26,0,1.65,0,2.49v14.48h-2.77V900.44h4.45L664.15,920h.13l7.49-19.57h4.42v23.88h-3V909.64c0-.78,0-1.55,0-2.32s.06-1.5.1-2.18.08-1.25.1-1.72h-.13l-8,20.9Z" id="path61"/>
|
||||
<path class="cls-1" fill="#353535" d="M688.16,924.32V909.41c0-.63,0-1.3,0-2s0-1.42.07-2.1,0-1.27,0-1.76c-.35.38-.66.69-.92.92s-.6.54-1,.92l-2.41,2-1.57-2,6.26-4.89H691v23.88Z" id="path62"/>
|
||||
<path class="cls-1" fill="#353535" d="M502.55,894.19l-2.22-2.37a14.1,14.1,0,0,1,18.94-.33l-2.13,2.44a10.9,10.9,0,0,0-7.16-2.69A10.78,10.78,0,0,0,502.55,894.19Z" id="path63"/>
|
||||
<path class="cls-1" fill="#353535" d="M506.38,897.85l-2.4-2.18a8.08,8.08,0,0,1,11.61-.39l-2.24,2.33a4.85,4.85,0,0,0-7,.24Z" id="path64"/>
|
||||
</g>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M472.55,77.74h68.09a7.43,7.43,0,0,1,7.43,7.43V360.26a0,0,0,0,1,0,0h-83a0,0,0,0,1,0,0V85.17A7.43,7.43,0,0,1,472.55,77.74Z" id="path65"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="465.12" y1="123.91" x2="548.07" y2="123.91" id="line65"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="465.12" y1="149.74" x2="548.07" y2="149.74" id="line66"/>
|
||||
<polyline class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" points="465.12 360.26 452.2 396.26 452.2 436.17 560.99 436.17 560.99 396.26 548.07 360.26" id="polyline66"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="452.2" y1="396.26" x2="560.98" y2="396.26" id="line67"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M449.69,440.17H562a3.26,3.26,0,0,1,2.56,1.55l5.93,8.19H442.68l4-7.49A3.65,3.65,0,0,1,449.69,440.17Z" id="path67"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M600,440.17H712.33a3.24,3.24,0,0,1,2.56,1.55l5.93,8.19H593l4-7.49A3.65,3.65,0,0,1,600,440.17Z" id="path68"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="454.45" y1="436.17" x2="454.45" y2="439.83" id="line68"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="558.64" y1="436.17" x2="558.64" y2="439.83" id="line69"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.87" id="rect69"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="611.07" y1="416.61" x2="611.07" y2="439.83" id="line70"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="703.81" y1="416.61" x2="703.81" y2="439.83" id="line71"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M614.2,347.35h86.48a3.13,3.13,0,0,1,3.13,3.13V356a0,0,0,0,1,0,0H611.07a0,0,0,0,1,0,0v-5.48A3.13,3.13,0,0,1,614.2,347.35Z" id="path71"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="570.51" y1="449.91" x2="592.99" y2="449.91" id="line72"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path72"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect72"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect73"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect74"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path74"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path75"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path76"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path77"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path78"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path79"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="751.94" y="555.13" width="5.87" height="47.48" id="rect79"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path80"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path81"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path82"/>
|
||||
<rect class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect82"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="518.72" cy="853.89" r="5.48" id="circle82"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="640.14" cy="853.89" r="5.48" id="circle83"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="541.83" cy="853.89" r="5.48" id="circle84"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="567.67" cy="853.89" r="5.48" id="circle85"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="593.51" cy="853.89" r="5.48" id="circle86"/>
|
||||
<circle class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" cx="616.82" cy="853.89" r="5.48" id="circle87"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="430.68" y1="572.74" x2="430.68" y2="602.61" id="line87"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="424.42" y1="595.43" x2="424.42" y2="578.11" id="line88"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="438.21" y1="595.43" x2="438.21" y2="578.11" id="line89"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="430.68" y1="644.74" x2="430.68" y2="674.61" id="line90"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="424.42" y1="667.43" x2="424.42" y2="650.11" id="line91"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="438.21" y1="667.43" x2="438.21" y2="650.11" id="line92"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="430.68" y1="716.74" x2="430.68" y2="746.61" id="line93"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="424.42" y1="739.43" x2="424.42" y2="722.11" id="line94"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="438.21" y1="739.43" x2="438.21" y2="722.11" id="line95"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="730.03" y1="572.74" x2="730.03" y2="602.61" id="line96"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="723.77" y1="595.43" x2="723.77" y2="578.11" id="line97"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="737.56" y1="595.43" x2="737.56" y2="578.11" id="line98"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="730.03" y1="644.74" x2="730.03" y2="674.61" id="line99"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="723.77" y1="667.43" x2="723.77" y2="650.11" id="line100"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="737.56" y1="667.43" x2="737.56" y2="650.11" id="line101"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="730.03" y1="716.74" x2="730.03" y2="746.61" id="line102"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="723.77" y1="739.43" x2="723.77" y2="722.11" id="line103"/>
|
||||
<line class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" x1="737.56" y1="739.43" x2="737.56" y2="722.11" id="line104"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path104"/>
|
||||
<path class="cls-8" fill="none" stroke="#000" stroke-miterlimit="10" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path105"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
264
neode-ui/public/assets/img/mesh-devices/unknown-new.svg
Normal file
@ -0,0 +1,264 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 233.57587 210"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg22"
|
||||
sodipodi:docname="unknown-new.svg"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
width="233.57587"
|
||||
height="210"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview22"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="2.6448247"
|
||||
inkscape:cx="116.45384"
|
||||
inkscape:cy="98.116143"
|
||||
inkscape:window-width="1728"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="33"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg22" />
|
||||
<!-- Antenna -->
|
||||
<!-- Radio body - main housing with gradient -->
|
||||
<defs
|
||||
id="defs6">
|
||||
<linearGradient
|
||||
id="bodyGrad"
|
||||
x1="29.580399"
|
||||
y1="34.651325"
|
||||
x2="207.06279"
|
||||
y2="212.13371"
|
||||
gradientTransform="matrix(0.84515425,0,0,1.183216,16.289544,-41)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0%"
|
||||
style="stop-color:#22222a"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="100%"
|
||||
style="stop-color:#16161b"
|
||||
id="stop3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="screenGrad"
|
||||
x1="36.829224"
|
||||
y1="105.71089"
|
||||
x2="36.829224"
|
||||
y2="201.38222"
|
||||
gradientTransform="matrix(1.1811273,0,0,0.84664878,16.289544,-41)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0%"
|
||||
style="stop-color:#0c1a12"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="100%"
|
||||
style="stop-color:#061008"
|
||||
id="stop5" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
id="glow"
|
||||
x="-0.8"
|
||||
y="-0.8"
|
||||
width="2.6"
|
||||
height="2.6">
|
||||
<feGaussianBlur
|
||||
stdDeviation="2"
|
||||
result="coloredBlur"
|
||||
id="feGaussianBlur5" />
|
||||
<feMerge
|
||||
id="feMerge6">
|
||||
<feMergeNode
|
||||
in="coloredBlur"
|
||||
id="feMergeNode5" />
|
||||
<feMergeNode
|
||||
in="SourceGraphic"
|
||||
id="feMergeNode6" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect
|
||||
x="46.289543"
|
||||
y="5"
|
||||
width="140"
|
||||
height="200"
|
||||
rx="14"
|
||||
fill="url(#bodyGrad)"
|
||||
stroke="#67ea94"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="rect6"
|
||||
style="fill:url(#bodyGrad)" />
|
||||
<!-- Inner bezel -->
|
||||
<rect
|
||||
x="54.289543"
|
||||
y="13"
|
||||
width="124"
|
||||
height="184"
|
||||
rx="10"
|
||||
fill="none"
|
||||
stroke="#2a2a32"
|
||||
stroke-width="1"
|
||||
id="rect7" />
|
||||
<!-- Top speaker grille -->
|
||||
<rect
|
||||
x="71.289543"
|
||||
y="21"
|
||||
width="90"
|
||||
height="20"
|
||||
rx="3"
|
||||
fill="#0a0a0f"
|
||||
id="rect8" />
|
||||
<g
|
||||
stroke="#333340"
|
||||
stroke-width="1"
|
||||
id="g10"
|
||||
transform="translate(16.289544,-41)">
|
||||
<line
|
||||
x1="62"
|
||||
y1="68"
|
||||
x2="138"
|
||||
y2="68"
|
||||
id="line8" />
|
||||
<line
|
||||
x1="62"
|
||||
y1="72"
|
||||
x2="138"
|
||||
y2="72"
|
||||
id="line9" />
|
||||
<line
|
||||
x1="62"
|
||||
y1="76"
|
||||
x2="138"
|
||||
y2="76"
|
||||
id="line10" />
|
||||
</g>
|
||||
<!-- Screen area with glow effect -->
|
||||
<rect
|
||||
x="60.289543"
|
||||
y="49"
|
||||
width="112"
|
||||
height="80"
|
||||
rx="6"
|
||||
fill="url(#screenGrad)"
|
||||
stroke="#67ea94"
|
||||
stroke-width="1"
|
||||
opacity="0.8"
|
||||
id="rect10"
|
||||
style="fill:url(#screenGrad)" />
|
||||
<!-- Meshtastic logo centered in screen - using actual logo paths -->
|
||||
<g
|
||||
transform="matrix(0.95,0,0,0.95,69.289544,64)"
|
||||
filter="url(#glow)"
|
||||
id="g14">
|
||||
<g
|
||||
transform="matrix(0.802386,0,0,0.460028,-421.748,-122.127)"
|
||||
id="g13">
|
||||
<g
|
||||
transform="matrix(0.579082,0,0,1.01004,460.975,-39.6867)"
|
||||
id="g11">
|
||||
<path
|
||||
d="m 250.908,330.267 -57.782,84.738 -12.188,-8.311 63.864,-93.657 c 1.372,-2.013 3.651,-3.218 6.087,-3.221 2.437,-0.002 4.717,1.199 6.093,3.21 l 64.012,93.51 -12.173,8.333 z"
|
||||
fill="#67ea94"
|
||||
id="path10" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.582378,0,0,1.01579,485.019,-211.182)"
|
||||
id="g12">
|
||||
<path
|
||||
d="m 87.642,581.398 67.115,-98.421 -12.119,-8.264 -67.115,98.421 z"
|
||||
fill="#67ea94"
|
||||
id="path11" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<!-- Question mark with glow -->
|
||||
<!-- Control area background -->
|
||||
<rect
|
||||
x="60.289543"
|
||||
y="137"
|
||||
width="112"
|
||||
height="60"
|
||||
rx="6"
|
||||
fill="#0f0f14"
|
||||
id="rect14" />
|
||||
<!-- D-pad style navigation -->
|
||||
<g
|
||||
transform="translate(88.289544,167)"
|
||||
id="g17">
|
||||
<circle
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="22"
|
||||
fill="#1a1a20"
|
||||
stroke="#2a2a32"
|
||||
stroke-width="1"
|
||||
id="circle14" />
|
||||
<path
|
||||
d="M -6,-16 H 6 V -6 H -6 Z"
|
||||
fill="#2a2a32"
|
||||
rx="1"
|
||||
id="path14" />
|
||||
<path
|
||||
d="M -6,6 H 6 V 16 H -6 Z"
|
||||
fill="#2a2a32"
|
||||
rx="1"
|
||||
id="path15" />
|
||||
<path
|
||||
d="M -16,-6 H -6 V 6 h -10 z"
|
||||
fill="#2a2a32"
|
||||
rx="1"
|
||||
id="path16" />
|
||||
<path
|
||||
d="M 6,-6 H 16 V 6 H 6 Z"
|
||||
fill="#2a2a32"
|
||||
rx="1"
|
||||
id="path17" />
|
||||
<circle
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="5"
|
||||
fill="#3a3a42"
|
||||
id="circle17" />
|
||||
</g>
|
||||
<!-- Action button (glowing green) -->
|
||||
<circle
|
||||
cx="144.28955"
|
||||
cy="167"
|
||||
r="14"
|
||||
fill="#1a2a1f"
|
||||
stroke="#67ea94"
|
||||
stroke-width="2"
|
||||
id="circle18" />
|
||||
<circle
|
||||
cx="144.28955"
|
||||
cy="167"
|
||||
r="8"
|
||||
fill="#67ea94"
|
||||
filter="url(#glow)"
|
||||
id="circle19" />
|
||||
<!-- Status LED -->
|
||||
<circle
|
||||
cx="170.26186"
|
||||
cy="25.547974"
|
||||
r="3"
|
||||
fill="#67ea94"
|
||||
opacity="0.8"
|
||||
filter="url(#glow)"
|
||||
id="circle20" />
|
||||
<!-- Side buttons -->
|
||||
<!-- USB port indication at bottom -->
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 101 KiB |
@ -37,6 +37,9 @@
|
||||
<!-- PWA Install Prompt (Install app, not just Add to Home Screen) -->
|
||||
<PWAInstallPrompt />
|
||||
|
||||
<!-- Global "mesh device detected" setup flow (fires on any page) -->
|
||||
<MeshDeviceSetupModal />
|
||||
|
||||
<!-- Global persistent audio player (bottom bar) -->
|
||||
<GlobalAudioPlayer />
|
||||
|
||||
@ -88,6 +91,8 @@ import ToastStack from './components/ToastStack.vue'
|
||||
import Screensaver from './components/Screensaver.vue'
|
||||
import HelpGuideModal from './components/HelpGuideModal.vue'
|
||||
import GlobalAudioPlayer from './components/GlobalAudioPlayer.vue'
|
||||
import MeshDeviceSetupModal from './components/mesh/MeshDeviceSetupModal.vue'
|
||||
import { useMeshStore } from './stores/mesh'
|
||||
|
||||
import { useControllerNav } from '@/composables/useControllerNav'
|
||||
import { playKeyboardTypingSound } from '@/composables/useLoginSounds'
|
||||
@ -360,6 +365,9 @@ function onVisibilityChange() {
|
||||
|
||||
onMounted(async () => {
|
||||
syncKioskSafeArea()
|
||||
// Light app-wide mesh poll so a freshly plugged-in radio surfaces the
|
||||
// setup modal on any page (the Mesh view's own poll takes over there).
|
||||
useMeshStore().startGlobalDetection()
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
window.addEventListener('keydown', onKeyDown, true)
|
||||
window.addEventListener('mousemove', onUserActivity)
|
||||
|
||||
240
neode-ui/src/components/mesh/MeshDeviceSetupModal.vue
Normal file
@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="show"
|
||||
:title="step === 1 ? 'Mesh Device Detected' : 'Set Up Your Mesh Radio'"
|
||||
max-width="max-w-lg"
|
||||
content-class="max-h-[90vh] overflow-y-auto"
|
||||
@close="dismiss"
|
||||
>
|
||||
<!-- Step 1: detection + hardware graphic -->
|
||||
<div v-if="step === 1" class="text-center">
|
||||
<div class="mx-auto my-2 w-44 h-44 relative">
|
||||
<!-- pulsing signal waves behind the board image -->
|
||||
<svg viewBox="0 0 160 160" class="absolute inset-0 w-full h-full" aria-hidden="true">
|
||||
<g class="mesh-detect-waves" stroke="rgba(251,146,60,0.5)" fill="none" stroke-width="2.5" stroke-linecap="round">
|
||||
<path d="M130 44 a30 30 0 0 1 0 28" />
|
||||
<path d="M138 36 a42 42 0 0 1 0 44" />
|
||||
<path d="M146 28 a54 54 0 0 1 0 60" />
|
||||
</g>
|
||||
</svg>
|
||||
<!-- actual board image (vendored from the Meshtastic web flasher) -->
|
||||
<img
|
||||
:src="deviceImage.image"
|
||||
:alt="deviceImage.label"
|
||||
class="relative w-full h-full object-contain drop-shadow-[0_8px_24px_rgba(251,146,60,0.25)]"
|
||||
@error="imageFailed = true"
|
||||
v-if="!imageFailed"
|
||||
/>
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center text-5xl">📡</div>
|
||||
</div>
|
||||
<p class="text-white text-base font-medium">{{ deviceImage.label }}</p>
|
||||
<p class="text-white/60 text-xs mt-1">
|
||||
{{ deviceImage.exact ? 'Detected' : 'Detected LoRa radio' }} on
|
||||
<span class="font-mono text-orange-300">{{ devicePath }}</span>
|
||||
</p>
|
||||
<p class="text-white/50 text-xs mt-2">
|
||||
Connect it to join the off-grid mesh — chat, block headers, and payments
|
||||
keep flowing even without internet.
|
||||
</p>
|
||||
<div class="flex gap-2 mt-6">
|
||||
<button class="flex-1 glass-button px-4 py-2 rounded-lg text-sm" @click="dismiss">
|
||||
Not Now
|
||||
</button>
|
||||
<button class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium" @click="step = 2">
|
||||
Set Up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: configuration with presets -->
|
||||
<div v-else>
|
||||
<p class="text-white/60 text-xs mb-4">
|
||||
Archipelago presets are pre-selected — confirm the region and you're on the mesh.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Region — the meaning adapts to the firmware: Meshtastic takes a
|
||||
region enum directly; MeshCore/RNode take raw RF params owned by
|
||||
the firmware/daemon, so the region only drives displayed guidance. -->
|
||||
<div>
|
||||
<label class="block text-sm text-white/80 mb-1">LoRa region / frequency plan</label>
|
||||
<select v-model="form.region" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60">
|
||||
<option value="">Keep the radio's current region</option>
|
||||
<option v-for="r in LORA_REGIONS" :key="r.code" :value="r.code">{{ r.label }}</option>
|
||||
</select>
|
||||
<p v-if="suggestedRegion && form.region === suggestedRegion.code" class="text-[11px] text-green-400/80 mt-1">
|
||||
Suggested from your node's location
|
||||
</p>
|
||||
<p v-if="selectedRegion && selectedRegion.dutyCyclePct < 100" class="text-[11px] text-amber-400/80 mt-1">
|
||||
{{ selectedRegion.code }} limits airtime to {{ selectedRegion.dutyCyclePct }}% duty cycle ({{ selectedRegion.band }} MHz) — the radio enforces this automatically.
|
||||
</p>
|
||||
<p v-if="effectiveKind === 'meshcore' && meshcorePlan" class="text-[11px] text-sky-300/80 mt-1">
|
||||
MeshCore community plan for {{ selectedRegion?.code }}: {{ meshcorePlan.freqMhz }} MHz, {{ meshcorePlan.bwKhz }} kHz, SF{{ meshcorePlan.sf }}, CR4/{{ meshcorePlan.cr }} — set on the radio via its MeshCore app if it differs.
|
||||
</p>
|
||||
<p v-else-if="effectiveKind === 'meshcore' && selectedRegion" class="text-[11px] text-sky-300/80 mt-1">
|
||||
MeshCore radios keep their flashed RF settings — make sure the radio is on the {{ selectedRegion.band }} MHz band for {{ selectedRegion.code }}.
|
||||
</p>
|
||||
<p v-if="effectiveKind === 'reticulum'" class="text-[11px] text-sky-300/80 mt-1">
|
||||
RNode radio parameters (frequency / bandwidth / SF / CR) are managed by the Reticulum daemon's interface config on this node.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Node name -->
|
||||
<div>
|
||||
<label class="block text-sm text-white/80 mb-1">Name on the mesh</label>
|
||||
<input v-model="form.name" maxlength="24" placeholder="e.g. basement-node"
|
||||
class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60" />
|
||||
</div>
|
||||
|
||||
<!-- Channel (Meshtastic secondary channel / MeshCore group channel;
|
||||
Reticulum has no channel concept — IFAC lives in the daemon config) -->
|
||||
<div v-if="effectiveKind !== 'reticulum'">
|
||||
<label class="block text-sm text-white/80 mb-1">Channel</label>
|
||||
<input v-model="form.channel" maxlength="11"
|
||||
class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60" />
|
||||
<p class="text-[11px] text-white/40 mt-1">
|
||||
Archipelago nodes find each other on the "archipelago" channel; the public default channel stays active for interop.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Firmware (advanced) -->
|
||||
<div>
|
||||
<button class="text-xs text-orange-300 hover:text-orange-200" @click="showAdvanced = !showAdvanced">
|
||||
{{ showAdvanced ? '▾' : '▸' }} Advanced
|
||||
</button>
|
||||
<div v-if="showAdvanced" class="mt-2">
|
||||
<label class="block text-sm text-white/80 mb-1">Radio firmware</label>
|
||||
<select v-model="form.deviceKind" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60">
|
||||
<option value="auto">Auto-detect (Meshcore → Meshtastic → RNode)</option>
|
||||
<option value="meshcore">MeshCore</option>
|
||||
<option value="meshtastic">Meshtastic</option>
|
||||
<option value="reticulum">Reticulum / RNode</option>
|
||||
</select>
|
||||
<p class="text-[11px] text-white/40 mt-1">
|
||||
Pin this if you know what's flashed on the board — auto-detect probes each firmware in order.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="text-xs text-red-400 mt-3">{{ error }}</p>
|
||||
|
||||
<div class="flex gap-2 mt-6">
|
||||
<button class="glass-button px-4 py-2 rounded-lg text-sm" @click="step = 1">Back</button>
|
||||
<button
|
||||
class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50"
|
||||
:disabled="connecting"
|
||||
@click="connect"
|
||||
>
|
||||
{{ connecting ? 'Connecting…' : 'Connect to Mesh' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BaseModal from '@/components/BaseModal.vue'
|
||||
import { useMeshStore } from '@/stores/mesh'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { LORA_REGIONS, regionByCode, suggestRegionFromLatLon, meshcorePlanFor } from '@/utils/loraRegions'
|
||||
import { resolveMeshDeviceImage } from '@/utils/meshDeviceImages'
|
||||
|
||||
const mesh = useMeshStore()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const step = ref<1 | 2>(1)
|
||||
const showAdvanced = ref(false)
|
||||
const connecting = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const devicePath = computed(() => mesh.undismissedDetectedDevices[0] ?? '')
|
||||
const show = computed(() => !!devicePath.value)
|
||||
const imageFailed = ref(false)
|
||||
const deviceImage = computed(() =>
|
||||
resolveMeshDeviceImage(
|
||||
mesh.status?.detected_device_info?.find(d => d.path === devicePath.value)
|
||||
)
|
||||
)
|
||||
|
||||
const suggestedRegion = computed(() => {
|
||||
const info = appStore.serverInfo as { lat?: number | null; lon?: number | null } | undefined
|
||||
if (info?.lat != null && info?.lon != null) {
|
||||
return suggestRegionFromLatLon(info.lat, info.lon)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
region: '',
|
||||
name: '',
|
||||
channel: 'archipelago',
|
||||
deviceKind: 'auto',
|
||||
})
|
||||
|
||||
const selectedRegion = computed(() => regionByCode(form.value.region))
|
||||
// The firmware whose options we surface: the explicit pin wins, else the
|
||||
// last detected/connected type, else meshtastic-style (where presets apply).
|
||||
const effectiveKind = computed(() => {
|
||||
if (form.value.deviceKind !== 'auto') return form.value.deviceKind
|
||||
const t = (mesh.status?.device_type ?? '').toLowerCase()
|
||||
return t === 'meshcore' || t === 'meshtastic' || t === 'reticulum' ? t : 'auto'
|
||||
})
|
||||
const meshcorePlan = computed(() => meshcorePlanFor(form.value.region))
|
||||
|
||||
// (Re)apply presets each time a new device surfaces the modal
|
||||
watch(show, (visible) => {
|
||||
if (visible) {
|
||||
step.value = 1
|
||||
error.value = ''
|
||||
imageFailed.value = false
|
||||
form.value.region = suggestedRegion.value?.code ?? mesh.status?.lora_region ?? ''
|
||||
form.value.name = mesh.status?.self_advert_name ?? appStore.serverName ?? ''
|
||||
form.value.channel = mesh.status?.channel_name || 'archipelago'
|
||||
form.value.deviceKind = mesh.status?.device_kind ?? 'auto'
|
||||
}
|
||||
})
|
||||
|
||||
function dismiss() {
|
||||
if (devicePath.value) mesh.dismissDetectedDevice(devicePath.value)
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
connecting.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const path = devicePath.value
|
||||
await mesh.configure({
|
||||
enabled: true,
|
||||
device_path: path,
|
||||
channel_name: form.value.channel.trim() || 'archipelago',
|
||||
...(form.value.name.trim() ? { advert_name: form.value.name.trim() } : {}),
|
||||
...(form.value.region ? { lora_region: form.value.region } : {}),
|
||||
device_kind: form.value.deviceKind,
|
||||
})
|
||||
mesh.dismissDetectedDevice(path)
|
||||
void router.push('/mesh')
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to configure the mesh radio'
|
||||
} finally {
|
||||
connecting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mesh-detect-waves path {
|
||||
animation: mesh-wave-pulse 2.2s ease-out infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
.mesh-detect-waves path:nth-child(2) { animation-delay: 0.35s; }
|
||||
.mesh-detect-waves path:nth-child(3) { animation-delay: 0.7s; }
|
||||
@keyframes mesh-wave-pulse {
|
||||
0% { opacity: 0; }
|
||||
25% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
@ -23,6 +23,31 @@ export interface MeshStatus {
|
||||
receive_block_headers?: boolean
|
||||
/** Operator-configured LoRa region (e.g. "EU_868"), for the Device tab. */
|
||||
region?: string | null
|
||||
/** Persisted config: LoRa region the driver programs on fresh radios. */
|
||||
lora_region?: string | null
|
||||
/** Persisted config: firmware pin (meshcore|meshtastic|reticulum), null = auto-detect. */
|
||||
device_kind?: string | null
|
||||
/** USB identity per detected port (for the setup modal's board image). */
|
||||
detected_device_info?: Array<{
|
||||
path: string
|
||||
vid?: string | null
|
||||
pid?: string | null
|
||||
product?: string | null
|
||||
manufacturer?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
/** Params accepted by mesh.configure (superset of the status fields). */
|
||||
export interface MeshConfigureParams {
|
||||
enabled?: boolean
|
||||
device_path?: string
|
||||
channel_name?: string
|
||||
broadcast_identity?: boolean
|
||||
advert_name?: string
|
||||
announce_block_headers?: boolean
|
||||
receive_block_headers?: boolean
|
||||
lora_region?: string
|
||||
device_kind?: string
|
||||
}
|
||||
|
||||
export interface MeshPeer {
|
||||
@ -234,6 +259,33 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Global device-detection (for the app-wide "device detected" modal) ──
|
||||
// Ports the user dismissed the setup modal for, persisted per browser.
|
||||
const DETECT_DISMISS_KEY = 'archipelago.mesh.detect-dismissed.v1'
|
||||
const dismissedDetectedPaths = ref<Set<string>>(new Set(
|
||||
JSON.parse(localStorage.getItem(DETECT_DISMISS_KEY) || '[]') as string[]
|
||||
))
|
||||
const undismissedDetectedDevices = computed(() => {
|
||||
const s = status.value
|
||||
if (!s || s.device_connected || s.enabled) return []
|
||||
return (s.detected_devices || []).filter(p => !dismissedDetectedPaths.value.has(p))
|
||||
})
|
||||
function dismissDetectedDevice(path: string) {
|
||||
dismissedDetectedPaths.value.add(path)
|
||||
dismissedDetectedPaths.value = new Set(dismissedDetectedPaths.value)
|
||||
localStorage.setItem(DETECT_DISMISS_KEY, JSON.stringify([...dismissedDetectedPaths.value]))
|
||||
}
|
||||
let globalDetectTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** App-wide light poll so the detected-device modal works on every page
|
||||
* (the Mesh view's own 5s poll takes over while it is mounted). */
|
||||
function startGlobalDetection(intervalMs = 15000) {
|
||||
if (globalDetectTimer) return
|
||||
void fetchStatus()
|
||||
globalDetectTimer = setInterval(() => {
|
||||
if (document.visibilityState === 'visible') void fetchStatus()
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
async function fetchPeers() {
|
||||
try {
|
||||
const res = await rpcClient.call<{ peers: MeshPeer[]; count: number }>({
|
||||
@ -455,12 +507,12 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function configure(config: Partial<MeshStatus>) {
|
||||
async function configure(config: MeshConfigureParams) {
|
||||
try {
|
||||
error.value = null
|
||||
await rpcClient.call<{ configured: boolean }>({
|
||||
method: 'mesh.configure',
|
||||
params: config,
|
||||
params: config as Record<string, unknown>,
|
||||
})
|
||||
await fetchStatus()
|
||||
} catch (err: unknown) {
|
||||
@ -851,6 +903,9 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
blockHeaders,
|
||||
latestBlockHeight,
|
||||
fetchStatus,
|
||||
undismissedDetectedDevices,
|
||||
dismissDetectedDevice,
|
||||
startGlobalDetection,
|
||||
fetchPeers,
|
||||
fetchMessages,
|
||||
sendMessage,
|
||||
|
||||
102
neode-ui/src/utils/loraRegions.ts
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* LoRa regional frequency plans — the canonical region list the backend's
|
||||
* Meshtastic driver can program (core/.../mesh/meshtastic.rs region table),
|
||||
* annotated with band / duty-cycle / power constraints from the Meshtastic
|
||||
* firmware `regions[]` table so the UI can surface legality hints.
|
||||
*
|
||||
* Also used to derive suggested radio parameters for MeshCore and RNode
|
||||
* presets (those firmwares take raw frequency/BW/SF/CR, not a region enum).
|
||||
*/
|
||||
|
||||
export interface LoraRegion {
|
||||
/** Wire value for mesh.configure { lora_region } */
|
||||
code: string
|
||||
label: string
|
||||
/** Frequency range, MHz (display) */
|
||||
band: string
|
||||
/** Regional duty-cycle cap, percent (100 = none) */
|
||||
dutyCyclePct: number
|
||||
/** Max TX power, dBm */
|
||||
maxPowerDbm: number
|
||||
}
|
||||
|
||||
export const LORA_REGIONS: LoraRegion[] = [
|
||||
{ code: 'US', label: 'United States / Americas (915 MHz)', band: '902–928', dutyCyclePct: 100, maxPowerDbm: 30 },
|
||||
{ code: 'EU_868', label: 'Europe (868 MHz)', band: '869.4–869.65', dutyCyclePct: 10, maxPowerDbm: 27 },
|
||||
{ code: 'EU_433', label: 'Europe (433 MHz)', band: '433–434', dutyCyclePct: 10, maxPowerDbm: 10 },
|
||||
{ code: 'ANZ', label: 'Australia / New Zealand (915 MHz)', band: '915–928', dutyCyclePct: 100, maxPowerDbm: 30 },
|
||||
{ code: 'ANZ_433', label: 'Australia / New Zealand (433 MHz)', band: '433.05–434.79', dutyCyclePct: 100, maxPowerDbm: 14 },
|
||||
{ code: 'NZ_865', label: 'New Zealand (865 MHz)', band: '864–868', dutyCyclePct: 100, maxPowerDbm: 36 },
|
||||
{ code: 'CN', label: 'China (470 MHz)', band: '470–510', dutyCyclePct: 100, maxPowerDbm: 19 },
|
||||
{ code: 'JP', label: 'Japan (920 MHz)', band: '920.5–923.5', dutyCyclePct: 100, maxPowerDbm: 13 },
|
||||
{ code: 'KR', label: 'South Korea (920 MHz)', band: '920–923', dutyCyclePct: 100, maxPowerDbm: 23 },
|
||||
{ code: 'TW', label: 'Taiwan (920 MHz)', band: '920–925', dutyCyclePct: 100, maxPowerDbm: 27 },
|
||||
{ code: 'RU', label: 'Russia (868 MHz)', band: '868.7–869.2', dutyCyclePct: 100, maxPowerDbm: 20 },
|
||||
{ code: 'IN', label: 'India (865 MHz)', band: '865–867', dutyCyclePct: 100, maxPowerDbm: 30 },
|
||||
{ code: 'TH', label: 'Thailand (920 MHz)', band: '920–925', dutyCyclePct: 10, maxPowerDbm: 27 },
|
||||
{ code: 'UA_868', label: 'Ukraine (868 MHz)', band: '868–868.6', dutyCyclePct: 1, maxPowerDbm: 14 },
|
||||
{ code: 'UA_433', label: 'Ukraine (433 MHz)', band: '433–434.7', dutyCyclePct: 10, maxPowerDbm: 10 },
|
||||
{ code: 'MY_919', label: 'Malaysia (919 MHz)', band: '919–924', dutyCyclePct: 100, maxPowerDbm: 27 },
|
||||
{ code: 'MY_433', label: 'Malaysia (433 MHz)', band: '433–435', dutyCyclePct: 100, maxPowerDbm: 20 },
|
||||
{ code: 'SG_923', label: 'Singapore (923 MHz)', band: '917–925', dutyCyclePct: 100, maxPowerDbm: 20 },
|
||||
{ code: 'PH_915', label: 'Philippines (915 MHz)', band: '915–918', dutyCyclePct: 100, maxPowerDbm: 24 },
|
||||
{ code: 'PH_868', label: 'Philippines (868 MHz)', band: '868–869.4', dutyCyclePct: 100, maxPowerDbm: 14 },
|
||||
{ code: 'PH_433', label: 'Philippines (433 MHz)', band: '433–434.7', dutyCyclePct: 100, maxPowerDbm: 10 },
|
||||
{ code: 'LORA_24', label: 'Worldwide (2.4 GHz, SX1280 only)', band: '2400–2483.5', dutyCyclePct: 100, maxPowerDbm: 10 },
|
||||
]
|
||||
|
||||
export function regionByCode(code: string | null | undefined): LoraRegion | undefined {
|
||||
if (!code) return undefined
|
||||
return LORA_REGIONS.find(r => r.code === code.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Coarse lat/lon → LoRa region suggestion. Bounding boxes, most-specific
|
||||
* first — good enough to preselect the dropdown; the user confirms.
|
||||
*/
|
||||
export function suggestRegionFromLatLon(lat: number, lon: number): LoraRegion | undefined {
|
||||
const boxes: Array<{ code: string; latMin: number; latMax: number; lonMin: number; lonMax: number }> = [
|
||||
{ code: 'JP', latMin: 24, latMax: 46, lonMin: 123, lonMax: 146 },
|
||||
{ code: 'KR', latMin: 33, latMax: 39, lonMin: 124, lonMax: 132 },
|
||||
{ code: 'TW', latMin: 21.5, latMax: 25.5, lonMin: 119.5, lonMax: 122.5 },
|
||||
{ code: 'PH_915', latMin: 4, latMax: 21, lonMin: 116, lonMax: 127 },
|
||||
{ code: 'SG_923', latMin: 1, latMax: 1.6, lonMin: 103.5, lonMax: 104.2 },
|
||||
{ code: 'MY_919', latMin: 0.8, latMax: 7.5, lonMin: 99, lonMax: 119.5 },
|
||||
{ code: 'TH', latMin: 5.5, latMax: 20.5, lonMin: 97, lonMax: 106 },
|
||||
{ code: 'IN', latMin: 6, latMax: 36, lonMin: 68, lonMax: 97.5 },
|
||||
{ code: 'CN', latMin: 18, latMax: 54, lonMin: 73, lonMax: 135 },
|
||||
{ code: 'UA_868', latMin: 44, latMax: 52.5, lonMin: 22, lonMax: 40.5 },
|
||||
{ code: 'RU', latMin: 41, latMax: 82, lonMin: 27, lonMax: 180 },
|
||||
{ code: 'ANZ', latMin: -48, latMax: -9, lonMin: 112, lonMax: 180 },
|
||||
{ code: 'EU_868', latMin: 34, latMax: 72, lonMin: -25, lonMax: 45 },
|
||||
// Americas
|
||||
{ code: 'US', latMin: -56, latMax: 72, lonMin: -170, lonMax: -30 },
|
||||
]
|
||||
for (const b of boxes) {
|
||||
if (lat >= b.latMin && lat <= b.latMax && lon >= b.lonMin && lon <= b.lonMax) {
|
||||
return regionByCode(b.code)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Community MeshCore radio plan for a region, where one is well established.
|
||||
* MeshCore has no region enum — the firmware takes raw freq/BW/SF/CR — so
|
||||
* these are display-level suggestions (EU is the verified community default;
|
||||
* other regions show the legal band and defer to the local community plan). */
|
||||
export interface MeshcorePlan {
|
||||
freqMhz: number
|
||||
bwKhz: number
|
||||
sf: number
|
||||
cr: number
|
||||
}
|
||||
|
||||
export const MESHCORE_PLANS: Record<string, MeshcorePlan> = {
|
||||
EU_868: { freqMhz: 869.525, bwKhz: 250, sf: 11, cr: 5 },
|
||||
EU_433: { freqMhz: 433.65, bwKhz: 250, sf: 11, cr: 5 },
|
||||
}
|
||||
|
||||
export function meshcorePlanFor(code: string | null | undefined): MeshcorePlan | undefined {
|
||||
if (!code) return undefined
|
||||
return MESHCORE_PLANS[code.toUpperCase()]
|
||||
}
|
||||
90
neode-ui/src/utils/meshDeviceImages.ts
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Best-effort mapping from a detected serial port's USB identity to a board
|
||||
* image (SVGs vendored from the Meshtastic web-flasher, GPL-3.0 —
|
||||
* github.com/meshtastic/web-flasher, public/img/devices/).
|
||||
*
|
||||
* Native-USB boards (T-Deck, RAK4631, T1000-E…) report their name in the USB
|
||||
* product string, so those match precisely. UART-bridge boards only identify
|
||||
* the bridge chip (CP2102/CH340), so vid:pid picks the most common board for
|
||||
* that chip and the label stays honest ("LoRa radio").
|
||||
*/
|
||||
|
||||
export interface DetectedDeviceInfo {
|
||||
path: string
|
||||
vid?: string | null
|
||||
pid?: string | null
|
||||
product?: string | null
|
||||
manufacturer?: string | null
|
||||
}
|
||||
|
||||
export interface MeshDeviceImage {
|
||||
/** Path under public/ */
|
||||
image: string
|
||||
/** Human name shown in the modal */
|
||||
label: string
|
||||
/** True when the match is exact (product string), false for chip-level guesses */
|
||||
exact: boolean
|
||||
}
|
||||
|
||||
const IMG = '/assets/img/mesh-devices'
|
||||
|
||||
/** product-string keyword → image (checked in order, case-insensitive) */
|
||||
const PRODUCT_MATCHES: Array<{ match: RegExp; image: string; label: string }> = [
|
||||
{ match: /t-?deck/i, image: `${IMG}/t-deck.svg`, label: 'LILYGO T-Deck' },
|
||||
{ match: /t-?echo\s*plus/i, image: `${IMG}/t-echo_plus.svg`, label: 'LILYGO T-Echo Plus' },
|
||||
{ match: /t-?echo/i, image: `${IMG}/t-echo.svg`, label: 'LILYGO T-Echo' },
|
||||
{ match: /t-?beam\s*s3/i, image: `${IMG}/tbeam-s3-core.svg`, label: 'LILYGO T-Beam S3' },
|
||||
{ match: /t-?beam/i, image: `${IMG}/tbeam.svg`, label: 'LILYGO T-Beam' },
|
||||
{ match: /t3.?s3/i, image: `${IMG}/tlora-t3s3-v1.svg`, label: 'LILYGO T3-S3' },
|
||||
{ match: /t-?lora|tlora/i, image: `${IMG}/tlora-v2-1-1_6.svg`, label: 'LILYGO T-LoRa' },
|
||||
{ match: /rak.?4631|wisblock/i, image: `${IMG}/rak4631.svg`, label: 'RAK WisBlock 4631' },
|
||||
{ match: /wismesh|wistap/i, image: `${IMG}/rak-wismeshtap.svg`, label: 'RAK WisMesh Tap' },
|
||||
{ match: /t1000|tracker.?t1000/i, image: `${IMG}/tracker-t1000-e.svg`, label: 'Seeed Card Tracker T1000-E' },
|
||||
{ match: /xiao/i, image: `${IMG}/seeed-xiao-s3.svg`, label: 'Seeed XIAO S3' },
|
||||
{ match: /wio.?tracker|wm1110/i, image: `${IMG}/wio-tracker-wm1110.svg`, label: 'Seeed Wio Tracker' },
|
||||
{ match: /station\s*g2/i, image: `${IMG}/station-g2.svg`, label: 'B&Q Station G2' },
|
||||
{ match: /nano\s*g2/i, image: `${IMG}/nano-g2-ultra.svg`, label: 'B&Q Nano G2 Ultra' },
|
||||
{ match: /t114/i, image: `${IMG}/heltec-mesh-node-t114.svg`, label: 'Heltec Mesh Node T114' },
|
||||
{ match: /wireless\s*stick|wsl/i, image: `${IMG}/heltec-wsl-v3.svg`, label: 'Heltec Wireless Stick Lite V3' },
|
||||
{ match: /heltec.*v4/i, image: `${IMG}/heltec_v4.svg`, label: 'Heltec V4' },
|
||||
{ match: /heltec/i, image: `${IMG}/heltec-v3.svg`, label: 'Heltec LoRa 32 V3' },
|
||||
{ match: /thinknode/i, image: `${IMG}/thinknode_m1.svg`, label: 'Elecrow ThinkNode' },
|
||||
{ match: /pico/i, image: `${IMG}/rpipicow.svg`, label: 'Raspberry Pi Pico' },
|
||||
{ match: /rnode/i, image: `${IMG}/diy.svg`, label: 'RNode' },
|
||||
]
|
||||
|
||||
/** vid:pid → most-likely board (bridge chips = chip-level guess) */
|
||||
const VIDPID_MATCHES: Record<string, { image: string; label: string; exact: boolean }> = {
|
||||
// Silicon Labs CP210x — Heltec V3 family ships this bridge
|
||||
'10c4:ea60': { image: `${IMG}/heltec-v3.svg`, label: 'LoRa radio (CP2102 serial)', exact: false },
|
||||
// WCH CH340 — most T-Beam / T-LoRa boards
|
||||
'1a86:7523': { image: `${IMG}/tbeam.svg`, label: 'LoRa radio (CH340 serial)', exact: false },
|
||||
'1a86:55d4': { image: `${IMG}/tbeam.svg`, label: 'LoRa radio (CH9102 serial)', exact: false },
|
||||
// Espressif native USB (S3 boards: T3-S3, T-Deck without product str)
|
||||
'303a:1001': { image: `${IMG}/tlora-t3s3-v1.svg`, label: 'ESP32-S3 LoRa board', exact: false },
|
||||
// RAK4631 nRF52 native USB
|
||||
'239a:8029': { image: `${IMG}/rak4631.svg`, label: 'RAK WisBlock 4631', exact: true },
|
||||
// Raspberry Pi (Pico W)
|
||||
'2e8a:0005': { image: `${IMG}/rpipicow.svg`, label: 'Raspberry Pi Pico', exact: false },
|
||||
}
|
||||
|
||||
const FALLBACK: MeshDeviceImage = {
|
||||
image: `${IMG}/unknown-new.svg`,
|
||||
label: 'LoRa mesh radio',
|
||||
exact: false,
|
||||
}
|
||||
|
||||
export function resolveMeshDeviceImage(info: DetectedDeviceInfo | undefined): MeshDeviceImage {
|
||||
if (!info) return FALLBACK
|
||||
const product = `${info.manufacturer ?? ''} ${info.product ?? ''}`.trim()
|
||||
if (product) {
|
||||
for (const m of PRODUCT_MATCHES) {
|
||||
if (m.match.test(product)) return { image: m.image, label: m.label, exact: true }
|
||||
}
|
||||
}
|
||||
if (info.vid && info.pid) {
|
||||
const hit = VIDPID_MATCHES[`${info.vid}:${info.pid}`.toLowerCase()]
|
||||
if (hit) return hit
|
||||
}
|
||||
return FALLBACK
|
||||
}
|
||||
@ -40,13 +40,7 @@ const sendError = ref('')
|
||||
const broadcasting = ref(false)
|
||||
const configuring = ref(false)
|
||||
const connectingDevice = ref<string | null>(null)
|
||||
// Onboarding modal (#6): guides a first-time connect for a freshly-detected,
|
||||
// not-yet-connected device — a friendlier wrapper around the same Connect
|
||||
// action the "Detected USB devices" list already offers, not a new setup
|
||||
// engine. `onboardingDismissed` remembers paths the user closed without
|
||||
// connecting, so it doesn't reappear every poll tick for the same device.
|
||||
const showOnboardingModal = ref(false)
|
||||
const onboardingDismissed = ref<Set<string>>(new Set())
|
||||
// Device-detected onboarding now lives in the global MeshDeviceSetupModal (App.vue).
|
||||
const chatScrollEl = ref<HTMLElement | null>(null)
|
||||
const messageInputRef = ref<HTMLInputElement | null>(null)
|
||||
const mobileShowChat = ref(false)
|
||||
@ -1041,35 +1035,12 @@ async function handleToggleEnabled() {
|
||||
async function handleConnectDevice(devicePath: string) {
|
||||
connectingDevice.value = devicePath
|
||||
try {
|
||||
await mesh.configure({ enabled: true, device_path: devicePath } as Partial<import('@/stores/mesh').MeshStatus>)
|
||||
showOnboardingModal.value = false
|
||||
await mesh.configure({ enabled: true, device_path: devicePath })
|
||||
} finally {
|
||||
connectingDevice.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const undismissedDetectedDevices = computed(() =>
|
||||
(mesh.status?.detected_devices ?? []).filter((d) => !onboardingDismissed.value.has(d))
|
||||
)
|
||||
|
||||
function dismissOnboarding() {
|
||||
for (const d of undismissedDetectedDevices.value) onboardingDismissed.value.add(d)
|
||||
showOnboardingModal.value = false
|
||||
}
|
||||
|
||||
// Pop the onboarding modal the moment a device is detected but not yet
|
||||
// connected — same trigger condition the inline "Detected USB devices" list
|
||||
// already uses (mesh.status.detected_devices non-empty + not connected),
|
||||
// just surfaced as a guided prompt instead of requiring the user to notice
|
||||
// the collapsed Device card.
|
||||
watch(
|
||||
() => [mesh.status?.device_connected, undismissedDetectedDevices.value.length] as const,
|
||||
([connected, count]) => {
|
||||
if (!connected && count > 0) showOnboardingModal.value = true
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function signalBars(rssi: number | null, snr: number | null = null): number {
|
||||
if (rssi !== null) {
|
||||
if (rssi > -60) return 4
|
||||
@ -2448,31 +2419,9 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Onboarding modal (#6): guided first-connect prompt for a freshly
|
||||
detected, not-yet-connected mesh device — wraps the same Connect
|
||||
action the inline "Detected USB devices" list already offers. -->
|
||||
<div v-if="showOnboardingModal" class="mesh-transport-modal-backdrop" @click.self="dismissOnboarding">
|
||||
<div class="glass-card mesh-transport-modal">
|
||||
<h3 class="mesh-transport-title">📡 Mesh Device Found</h3>
|
||||
<p class="mesh-transport-sub">
|
||||
A radio was detected but isn't connected yet. Connect it to start using off-grid mesh chat.
|
||||
</p>
|
||||
<div class="mesh-transport-options">
|
||||
<button
|
||||
v-for="dev in undismissedDetectedDevices"
|
||||
:key="dev"
|
||||
class="mesh-transport-option"
|
||||
:disabled="connectingDevice !== null"
|
||||
@click="handleConnectDevice(dev)"
|
||||
>
|
||||
<span class="mesh-transport-icon">📡</span>
|
||||
<span class="mesh-transport-label">{{ dev }}</span>
|
||||
<span class="mesh-transport-meta">{{ connectingDevice === dev ? 'Connecting…' : 'Connect' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="mesh-transport-cancel" @click="dismissOnboarding">Not now</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The "mesh device detected" setup flow is now the global
|
||||
MeshDeviceSetupModal mounted in App.vue (fires on every page,
|
||||
board image + region presets), so no local modal here. -->
|
||||
|
||||
<MediaLightbox
|
||||
:items="lightboxItems"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useMeshStore } from '@/stores/mesh'
|
||||
import { LORA_REGIONS, regionByCode, meshcorePlanFor } from '@/utils/loraRegions'
|
||||
|
||||
const mesh = useMeshStore()
|
||||
|
||||
@ -18,6 +19,64 @@ async function handleReboot() {
|
||||
rebooting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Editable settings (persisted via mesh.configure) ──
|
||||
const form = ref({
|
||||
region: '',
|
||||
deviceKind: 'auto',
|
||||
channel: 'archipelago',
|
||||
name: '',
|
||||
broadcastIdentity: true,
|
||||
})
|
||||
const saving = ref(false)
|
||||
const saveError = ref<string | null>(null)
|
||||
const saveDone = ref(false)
|
||||
let seeded = false
|
||||
|
||||
// Seed the form once from status (don't clobber in-progress edits on poll)
|
||||
watch(
|
||||
() => mesh.status,
|
||||
(s) => {
|
||||
if (!s || seeded) return
|
||||
seeded = true
|
||||
form.value.region = s.lora_region ?? ''
|
||||
form.value.deviceKind = s.device_kind ?? 'auto'
|
||||
form.value.channel = s.channel_name || 'archipelago'
|
||||
form.value.name = s.self_advert_name ?? ''
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const selectedRegion = computed(() => regionByCode(form.value.region))
|
||||
const deviceType = computed(() => mesh.status?.device_type ?? 'unknown')
|
||||
// Firmware whose options apply: explicit pin wins, else the connected type.
|
||||
const effectiveKind = computed(() => {
|
||||
if (form.value.deviceKind !== 'auto') return form.value.deviceKind
|
||||
const t = deviceType.value.toLowerCase()
|
||||
return t === 'meshcore' || t === 'meshtastic' || t === 'reticulum' ? t : 'auto'
|
||||
})
|
||||
const meshcorePlan = computed(() => meshcorePlanFor(form.value.region))
|
||||
|
||||
async function saveSettings() {
|
||||
saving.value = true
|
||||
saveError.value = null
|
||||
saveDone.value = false
|
||||
try {
|
||||
await mesh.configure({
|
||||
lora_region: form.value.region,
|
||||
device_kind: form.value.deviceKind,
|
||||
channel_name: form.value.channel.trim() || 'archipelago',
|
||||
...(form.value.name.trim() ? { advert_name: form.value.name.trim() } : {}),
|
||||
broadcast_identity: form.value.broadcastIdentity,
|
||||
})
|
||||
saveDone.value = true
|
||||
setTimeout(() => { saveDone.value = false }, 3000)
|
||||
} catch (e) {
|
||||
saveError.value = e instanceof Error ? e.message : 'Failed to save mesh settings'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -39,7 +98,7 @@ async function handleReboot() {
|
||||
<span class="mesh-stat-value">{{ mesh.status.self_advert_name ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="mesh-stat">
|
||||
<span class="mesh-stat-label">Region</span>
|
||||
<span class="mesh-stat-label">Region (radio)</span>
|
||||
<span class="mesh-stat-value">{{ mesh.status.region ?? 'Not set' }}</span>
|
||||
</div>
|
||||
<div class="mesh-stat">
|
||||
@ -48,7 +107,77 @@ async function handleReboot() {
|
||||
</div>
|
||||
<div class="mesh-stat">
|
||||
<span class="mesh-stat-label">Type</span>
|
||||
<span class="mesh-stat-value">{{ mesh.status.device_type === 'unknown' ? '—' : mesh.status.device_type }}</span>
|
||||
<span class="mesh-stat-value">{{ deviceType === 'unknown' ? '—' : deviceType }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Radio settings -->
|
||||
<div class="mt-5 pt-4 border-t border-white/10">
|
||||
<h4 class="text-sm font-semibold text-white mb-3">Radio Settings</h4>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">LoRa region / frequency plan</label>
|
||||
<select v-model="form.region" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60">
|
||||
<option value="">Keep the radio's current region</option>
|
||||
<option v-for="r in LORA_REGIONS" :key="r.code" :value="r.code">{{ r.label }}</option>
|
||||
</select>
|
||||
<p v-if="selectedRegion && selectedRegion.dutyCyclePct < 100" class="text-[11px] text-amber-400/80 mt-1">
|
||||
{{ selectedRegion.code }}: {{ selectedRegion.dutyCyclePct }}% duty-cycle limit, {{ selectedRegion.band }} MHz, max {{ selectedRegion.maxPowerDbm }} dBm
|
||||
</p>
|
||||
<p v-if="effectiveKind === 'meshtastic' || effectiveKind === 'auto'" class="text-[11px] text-white/40 mt-1">
|
||||
Applied to fresh (region-unset) Meshtastic radios; a radio that already has a region keeps it.
|
||||
</p>
|
||||
<p v-else-if="effectiveKind === 'meshcore' && meshcorePlan" class="text-[11px] text-sky-300/80 mt-1">
|
||||
MeshCore community plan for {{ selectedRegion?.code }}: {{ meshcorePlan.freqMhz }} MHz, {{ meshcorePlan.bwKhz }} kHz, SF{{ meshcorePlan.sf }}, CR4/{{ meshcorePlan.cr }} — the radio's flashed RF settings apply; adjust via a MeshCore client if they differ.
|
||||
</p>
|
||||
<p v-else-if="effectiveKind === 'meshcore'" class="text-[11px] text-sky-300/80 mt-1">
|
||||
MeshCore radios keep their flashed RF settings — verify the radio matches your region's band{{ selectedRegion ? ` (${selectedRegion.band} MHz)` : '' }}.
|
||||
</p>
|
||||
<p v-else-if="effectiveKind === 'reticulum'" class="text-[11px] text-sky-300/80 mt-1">
|
||||
RNode RF parameters are managed by the Reticulum daemon's interface config on this node.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Radio firmware</label>
|
||||
<select v-model="form.deviceKind" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60">
|
||||
<option value="auto">Auto-detect (Meshcore → Meshtastic → RNode)</option>
|
||||
<option value="meshcore">MeshCore</option>
|
||||
<option value="meshtastic">Meshtastic</option>
|
||||
<option value="reticulum">Reticulum / RNode</option>
|
||||
</select>
|
||||
<p class="text-[11px] text-white/40 mt-1">
|
||||
Pin the flashed firmware so no other protocol's probe bytes touch the port.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="effectiveKind !== 'reticulum'">
|
||||
<label class="block text-xs text-white/60 mb-1">Channel</label>
|
||||
<input v-model="form.channel" maxlength="11" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Name on the mesh</label>
|
||||
<input v-model="form.name" maxlength="24" placeholder="node name" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60" />
|
||||
</div>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 mt-3 text-sm text-white/80 cursor-pointer">
|
||||
<input v-model="form.broadcastIdentity" type="checkbox" class="h-4 w-4 accent-orange-500" />
|
||||
Periodically broadcast this node's identity on the mesh
|
||||
</label>
|
||||
|
||||
<p v-if="effectiveKind === 'auto'" class="text-[11px] text-white/40 mt-3">
|
||||
Options adapt to the detected firmware: region + channel program Meshtastic radios;
|
||||
MeshCore and RNode own their RF parameters in firmware/daemon config; name and identity apply to all.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-3 mt-4">
|
||||
<button
|
||||
class="glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50"
|
||||
:disabled="saving"
|
||||
@click="saveSettings"
|
||||
>
|
||||
{{ saving ? 'Saving…' : 'Save Settings' }}
|
||||
</button>
|
||||
<span v-if="saveDone" class="text-xs text-green-400">Saved — applies on next radio session</span>
|
||||
<span v-if="saveError" class="text-xs text-red-400">{{ saveError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||