|
|
|
|
@ -0,0 +1,591 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
"""
|
|
|
|
|
生成 draw.io (diagrams.net) 可用的 UML 图 XML 文件
|
|
|
|
|
包含 6 张图:类图、顺序图、组件图、用例图、活动图、部署图
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
import uuid
|
|
|
|
|
import html
|
|
|
|
|
|
|
|
|
|
def make_mxfile(pages):
|
|
|
|
|
root = ET.Element("mxfile")
|
|
|
|
|
root.set("host", "app.diagrams.net")
|
|
|
|
|
root.set("modified", "2026-05-19T00:00:00.000Z")
|
|
|
|
|
root.set("agent", "PythonScript")
|
|
|
|
|
root.set("version", "24.0.0")
|
|
|
|
|
root.set("type", "device")
|
|
|
|
|
root.set("pages", str(len(pages)))
|
|
|
|
|
for i, (name, graph_model) in enumerate(pages):
|
|
|
|
|
diag = ET.SubElement(root, "diagram")
|
|
|
|
|
diag.set("name", name)
|
|
|
|
|
diag.set("id", str(uuid.uuid4()))
|
|
|
|
|
diag.append(graph_model)
|
|
|
|
|
return root
|
|
|
|
|
|
|
|
|
|
def make_graph_model(cells, w=1200, h=900):
|
|
|
|
|
gm = ET.Element("mxGraphModel")
|
|
|
|
|
gm.set("dx", "1434")
|
|
|
|
|
gm.set("dy", "780")
|
|
|
|
|
gm.set("grid", "1")
|
|
|
|
|
gm.set("gridSize", "10")
|
|
|
|
|
gm.set("guides", "1")
|
|
|
|
|
gm.set("tooltips", "1")
|
|
|
|
|
gm.set("connect", "1")
|
|
|
|
|
gm.set("arrows", "1")
|
|
|
|
|
gm.set("fold", "1")
|
|
|
|
|
gm.set("page", "1")
|
|
|
|
|
gm.set("pageScale", "1")
|
|
|
|
|
gm.set("pageWidth", str(w))
|
|
|
|
|
gm.set("pageHeight", str(h))
|
|
|
|
|
gm.set("math", "0")
|
|
|
|
|
gm.set("shadow", "0")
|
|
|
|
|
root = ET.SubElement(gm, "root")
|
|
|
|
|
ET.SubElement(root, "mxCell", {"id":"0"})
|
|
|
|
|
ET.SubElement(root, "mxCell", {"id":"1", "parent":"0"})
|
|
|
|
|
for cell in cells:
|
|
|
|
|
root.append(cell)
|
|
|
|
|
return gm
|
|
|
|
|
|
|
|
|
|
def cell_style(shape, extras=""):
|
|
|
|
|
base = {
|
|
|
|
|
"class": "swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;",
|
|
|
|
|
"class_attr": "text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;",
|
|
|
|
|
"actor": "shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;",
|
|
|
|
|
"usecase": "ellipse;whiteSpace=wrap;html=1;",
|
|
|
|
|
"boundary": "shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;",
|
|
|
|
|
"rectangle": "rounded=0;whiteSpace=wrap;html=1;",
|
|
|
|
|
"rounded": "rounded=1;whiteSpace=wrap;html=1;",
|
|
|
|
|
"lifeline": "shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;outlineConnect=0;",
|
|
|
|
|
"activation": "shape=umlDestroy;whiteSpace=wrap;html=1;strokeWidth=3;",
|
|
|
|
|
"activation2": "rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;",
|
|
|
|
|
"component": "shape=component;align=left;spacingLeft=36;verticalAlign=top;whiteSpace=wrap;html=1;",
|
|
|
|
|
"interface": "shape=providedRequiredInterface;verticalAlign=top;spacingTop=0;whiteSpace=wrap;html=1;",
|
|
|
|
|
"start": "ellipse;whiteSpace=wrap;html=1;fillColor=#000000;",
|
|
|
|
|
"end": "ellipse;whiteSpace=wrap;html=1;fillColor=#000000;strokeColor=#ff0000;",
|
|
|
|
|
"activity": "rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;",
|
|
|
|
|
"decision": "rhombus;whiteSpace=wrap;html=1;fillColor=#ffffcc;strokeColor=#b3b3b3;",
|
|
|
|
|
"node": "shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;",
|
|
|
|
|
"artifact": "shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;",
|
|
|
|
|
"arrow": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;",
|
|
|
|
|
"dashed_arrow": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;",
|
|
|
|
|
"open_arrow": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;",
|
|
|
|
|
"diamond": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=diamondThin;endFill=1;",
|
|
|
|
|
"async": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;",
|
|
|
|
|
"message": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;verticalAlign=bottom;",
|
|
|
|
|
"return": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;verticalAlign=bottom;",
|
|
|
|
|
"text": "text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;",
|
|
|
|
|
"title": "text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1",
|
|
|
|
|
}
|
|
|
|
|
s = base.get(shape, shape)
|
|
|
|
|
if extras:
|
|
|
|
|
s += extras
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
def add_cell(cells, cid, parent="1", style="", value="", x=0, y=0, w=0, h=0, source=None, target=None, edge=False):
|
|
|
|
|
attrs = {"id": str(cid), "parent": str(parent), "style": style, "value": value}
|
|
|
|
|
if edge:
|
|
|
|
|
attrs["edge"] = "1"
|
|
|
|
|
if source: attrs["source"] = str(source)
|
|
|
|
|
if target: attrs["target"] = str(target)
|
|
|
|
|
else:
|
|
|
|
|
attrs["vertex"] = "1"
|
|
|
|
|
if x is not None: attrs["x"] = str(x)
|
|
|
|
|
if y is not None: attrs["y"] = str(y)
|
|
|
|
|
if w is not None: attrs["width"] = str(w)
|
|
|
|
|
if h is not None: attrs["height"] = str(h)
|
|
|
|
|
cell = ET.Element("mxCell", attrs)
|
|
|
|
|
if edge:
|
|
|
|
|
geo = ET.SubElement(cell, "mxGeometry", {"relative":"1", "as":"geometry"})
|
|
|
|
|
if source and target:
|
|
|
|
|
ET.SubElement(geo, "Array", {"as":"points"})
|
|
|
|
|
else:
|
|
|
|
|
geo = ET.SubElement(cell, "mxGeometry", {"x":str(x), "y":str(y), "width":str(w), "height":str(h), "as":"geometry"})
|
|
|
|
|
cells.append(cell)
|
|
|
|
|
return cid
|
|
|
|
|
|
|
|
|
|
_class_id_counter = 1000
|
|
|
|
|
def add_uml_class(cells, cid, name, attrs, methods, x, y, w=200, h=None):
|
|
|
|
|
global _class_id_counter
|
|
|
|
|
line_h = 18
|
|
|
|
|
attr_h = len(attrs) * line_h if attrs else line_h
|
|
|
|
|
meth_h = len(methods) * line_h if methods else line_h
|
|
|
|
|
sep = 6
|
|
|
|
|
total_h = 26 + attr_h + sep + meth_h + 10
|
|
|
|
|
if h and h > total_h:
|
|
|
|
|
total_h = h
|
|
|
|
|
# class box
|
|
|
|
|
add_cell(cells, cid, "1", cell_style("class"), name, x, y, w, total_h)
|
|
|
|
|
# separator line
|
|
|
|
|
_class_id_counter += 1
|
|
|
|
|
add_cell(cells, f"{cid}_sep1", cid, cell_style("rectangle","fillColor=none;strokeColor=none;"), "", 0, 26, w, 0)
|
|
|
|
|
# attrs
|
|
|
|
|
ay = 26
|
|
|
|
|
for i, a in enumerate(attrs):
|
|
|
|
|
_class_id_counter += 1
|
|
|
|
|
add_cell(cells, f"{cid}_attr{i}", cid, cell_style("class_attr"), a, 0, ay, w, line_h)
|
|
|
|
|
ay += line_h
|
|
|
|
|
if not attrs:
|
|
|
|
|
_class_id_counter += 1
|
|
|
|
|
add_cell(cells, f"{cid}_attr0", cid, cell_style("class_attr"), "", 0, ay, w, line_h)
|
|
|
|
|
ay += line_h
|
|
|
|
|
# separator
|
|
|
|
|
_class_id_counter += 1
|
|
|
|
|
add_cell(cells, f"{cid}_sep2", cid, cell_style("rectangle","fillColor=none;strokeColor=none;"), "", 0, ay, w, 0)
|
|
|
|
|
ay += sep
|
|
|
|
|
# methods
|
|
|
|
|
for i, m in enumerate(methods):
|
|
|
|
|
_class_id_counter += 1
|
|
|
|
|
add_cell(cells, f"{cid}_meth{i}", cid, cell_style("class_attr"), m, 0, ay, w, line_h)
|
|
|
|
|
ay += line_h
|
|
|
|
|
if not methods:
|
|
|
|
|
_class_id_counter += 1
|
|
|
|
|
add_cell(cells, f"{cid}_meth0", cid, cell_style("class_attr"), "", 0, ay, w, line_h)
|
|
|
|
|
return cid
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# 图1:类图 — 声源分析模块
|
|
|
|
|
# ============================================================
|
|
|
|
|
def build_class_diagram():
|
|
|
|
|
cells = []
|
|
|
|
|
# title
|
|
|
|
|
add_cell(cells, "title1", "1", cell_style("title"),
|
|
|
|
|
"图1 类图 — 声源分析模块核心类结构(静态视角)", 20, 10, 600, 30)
|
|
|
|
|
|
|
|
|
|
# Pipeline (center)
|
|
|
|
|
add_uml_class(cells, "Pipeline", "Pipeline",
|
|
|
|
|
["-impl_: Impl*"],
|
|
|
|
|
["+Process(audio): AcousticFrame", "+FromYaml(path): PipelineConfig", "+Reset()", "+Config(): const PipelineConfig&"],
|
|
|
|
|
420, 80, 260, 160)
|
|
|
|
|
|
|
|
|
|
# AudioBuffer
|
|
|
|
|
add_uml_class(cells, "AudioBuffer", "AudioBuffer",
|
|
|
|
|
["-capacity_frames_: size_t", "-num_channels_: size_t", "-buffer_: vector<float>", "-head_, tail_, size_: size_t"],
|
|
|
|
|
["+Push(samples): size_t", "+Pop(n): vector<float>", "+Get(off,n): vector<float>", "+Size(): size_t", "+Clear()"],
|
|
|
|
|
40, 80, 200, 170)
|
|
|
|
|
|
|
|
|
|
# FeatureExtractor
|
|
|
|
|
add_uml_class(cells, "FeatureExtractor", "FeatureExtractor",
|
|
|
|
|
["-impl_: Impl*"],
|
|
|
|
|
["+MelSpectrogram(audio): MatrixXf", "+MelSpectrogramMultiChannel(audio,n): vector<MatrixXf>"],
|
|
|
|
|
40, 300, 220, 110)
|
|
|
|
|
|
|
|
|
|
# GunshotClassifier
|
|
|
|
|
add_uml_class(cells, "GunshotClassifier", "GunshotClassifier",
|
|
|
|
|
["-session_: Ort::Session*", "-env_: Ort::Env*", "-labels_: vector<string>"],
|
|
|
|
|
["+Predict(mel): pair<string,float>", "+Labels(): const vector<string>&"],
|
|
|
|
|
300, 300, 220, 110)
|
|
|
|
|
|
|
|
|
|
# GccPhatLocalizer
|
|
|
|
|
add_uml_class(cells, "GccPhatLocalizer", "GccPhatLocalizer",
|
|
|
|
|
["-mic_config_: MicArrayConfig", "-sample_rate_: int", "-max_tdoa_: float"],
|
|
|
|
|
["+Localize(audio_mat): pair<float,float>"],
|
|
|
|
|
560, 300, 200, 90)
|
|
|
|
|
|
|
|
|
|
# DistanceEstimator
|
|
|
|
|
add_uml_class(cells, "DistanceEstimator", "DistanceEstimator",
|
|
|
|
|
["-config_: DistanceConfig", "-kalman_state_: float"],
|
|
|
|
|
["+ComputeSpl(audio): float", "+Estimate(spl,label): float", "+UpdateKalman(d): float", "+Reset()"],
|
|
|
|
|
800, 300, 220, 110)
|
|
|
|
|
|
|
|
|
|
# ThreatTracker
|
|
|
|
|
add_uml_class(cells, "ThreatTracker", "ThreatTracker",
|
|
|
|
|
["-min_interval_: float", "-history_: vector<AcousticThreat>"],
|
|
|
|
|
["+Update(threats): vector<AcousticThreat>", "+Reset()"],
|
|
|
|
|
800, 80, 200, 90)
|
|
|
|
|
|
|
|
|
|
# AcousticNode (ROS)
|
|
|
|
|
add_uml_class(cells, "AcousticNode", "AcousticNode (ROS层)",
|
|
|
|
|
["-nh_, pnh_: NodeHandle", "-pipeline_: Pipeline*", "-source_type_: string"],
|
|
|
|
|
["+run()", "-on_mic_array_audio(msg)", "-process_wav_source()", "-load_params()"],
|
|
|
|
|
40, 480, 240, 120)
|
|
|
|
|
|
|
|
|
|
# WavFileSource
|
|
|
|
|
add_uml_class(cells, "WavFileSource", "WavFileSource",
|
|
|
|
|
["-file_path_: string", "-sample_rate_: int", "-num_channels_: int"],
|
|
|
|
|
["+open(): bool", "+read(audio,n): size_t", "+num_channels(): int"],
|
|
|
|
|
340, 480, 200, 100)
|
|
|
|
|
|
|
|
|
|
# Types
|
|
|
|
|
add_uml_class(cells, "AcousticThreat", "AcousticThreat (struct)",
|
|
|
|
|
["+timestamp: Timestamp", "+threat_id: string", "+sound_type: string", "+confidence: float", "+azimuth: float", "+elevation: float", "+distance: float"],
|
|
|
|
|
[],
|
|
|
|
|
600, 480, 220, 130)
|
|
|
|
|
|
|
|
|
|
# PipelineConfig
|
|
|
|
|
add_uml_class(cells, "PipelineConfig", "PipelineConfig (struct)",
|
|
|
|
|
["+sample_rate: uint32_t", "+chunk_duration: float", "+n_mels: uint32_t", "+classifier: ClassifierConfig", "+mic_array: MicArrayConfig", "+distance: DistanceConfig"],
|
|
|
|
|
[],
|
|
|
|
|
860, 480, 220, 130)
|
|
|
|
|
|
|
|
|
|
# Relationships (edges)
|
|
|
|
|
# Pipeline --diamond--> components
|
|
|
|
|
add_cell(cells, "e1", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="AudioBuffer")
|
|
|
|
|
add_cell(cells, "e2", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="FeatureExtractor")
|
|
|
|
|
add_cell(cells, "e3", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="GunshotClassifier")
|
|
|
|
|
add_cell(cells, "e4", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="GccPhatLocalizer")
|
|
|
|
|
add_cell(cells, "e5", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="DistanceEstimator")
|
|
|
|
|
add_cell(cells, "e6", "1", cell_style("diamond"), "", edge=True, source="Pipeline", target="ThreatTracker")
|
|
|
|
|
|
|
|
|
|
# AcousticNode --> Pipeline
|
|
|
|
|
add_cell(cells, "e7", "1", cell_style("open_arrow"), "", edge=True, source="AcousticNode", target="Pipeline")
|
|
|
|
|
|
|
|
|
|
# AcousticNode --> WavFileSource
|
|
|
|
|
add_cell(cells, "e8", "1", cell_style("open_arrow"), "", edge=True, source="AcousticNode", target="WavFileSource")
|
|
|
|
|
|
|
|
|
|
# Pipeline --> PipelineConfig (dependency)
|
|
|
|
|
add_cell(cells, "e9", "1", cell_style("dashed_arrow"), "uses", edge=True, source="Pipeline", target="PipelineConfig")
|
|
|
|
|
|
|
|
|
|
# ThreatTracker --> AcousticThreat
|
|
|
|
|
add_cell(cells, "e10", "1", cell_style("open_arrow"), "", edge=True, source="ThreatTracker", target="AcousticThreat")
|
|
|
|
|
|
|
|
|
|
# Labels for relationships
|
|
|
|
|
add_cell(cells, "l1", "1", cell_style("text"), "组合", 230, 90, 50, 20)
|
|
|
|
|
add_cell(cells, "l2", "1", cell_style("text"), "组合", 230, 310, 50, 20)
|
|
|
|
|
add_cell(cells, "l3", "1", cell_style("text"), "组合", 520, 310, 50, 20)
|
|
|
|
|
add_cell(cells, "l4", "1", cell_style("text"), "组合", 780, 310, 50, 20)
|
|
|
|
|
add_cell(cells, "l5", "1", cell_style("text"), "组合", 780, 100, 50, 20)
|
|
|
|
|
|
|
|
|
|
return make_graph_model(cells, w=1200, h=700)
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# 图2:顺序图 — Pipeline::Process 调用链
|
|
|
|
|
# ============================================================
|
|
|
|
|
def build_sequence_diagram():
|
|
|
|
|
cells = []
|
|
|
|
|
add_cell(cells, "title2", "1", cell_style("title"),
|
|
|
|
|
"图2 顺序图 — Pipeline::Process 音频处理调用链(动态视角)", 20, 10, 700, 30)
|
|
|
|
|
|
|
|
|
|
# Lifelines
|
|
|
|
|
lx = 60
|
|
|
|
|
lifelines = [
|
|
|
|
|
("AcousticNode", lx),
|
|
|
|
|
("Pipeline", lx+180),
|
|
|
|
|
("AudioBuffer", lx+340),
|
|
|
|
|
("FeatureExtractor", lx+500),
|
|
|
|
|
("GunshotClassifier", lx+660),
|
|
|
|
|
("GccPhatLocalizer", lx+820),
|
|
|
|
|
("DistanceEstimator", lx+980),
|
|
|
|
|
("ThreatTracker", lx+1140),
|
|
|
|
|
("ThreatPublisher", lx+1300),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for name, x in lifelines:
|
|
|
|
|
add_cell(cells, f"ll_{name}", "1", cell_style("lifeline"), name, x, 60, 100, 520)
|
|
|
|
|
|
|
|
|
|
# Messages
|
|
|
|
|
y = 100
|
|
|
|
|
def msg(cid, src, tgt, text, ypos, dashed=False):
|
|
|
|
|
style = cell_style("return") if dashed else cell_style("message")
|
|
|
|
|
add_cell(cells, cid, "1", style, text, edge=True, source=f"ll_{src}", target=f"ll_{tgt}")
|
|
|
|
|
# label
|
|
|
|
|
add_cell(cells, f"{cid}_lab", "1", cell_style("text"), text,
|
|
|
|
|
(lifelines_dict[src] + lifelines_dict[tgt])//2 - 60, ypos-15, 120, 20)
|
|
|
|
|
|
|
|
|
|
lifelines_dict = {name:x for name,x in lifelines}
|
|
|
|
|
|
|
|
|
|
msg("m1", "AcousticNode", "Pipeline", "Process(audio_samples)", y)
|
|
|
|
|
y += 50
|
|
|
|
|
msg("m2", "Pipeline", "AudioBuffer", "Push(samples)", y)
|
|
|
|
|
y += 40
|
|
|
|
|
msg("m3", "Pipeline", "AudioBuffer", "Get(offset, chunk)", y)
|
|
|
|
|
y += 40
|
|
|
|
|
msg("m4", "Pipeline", "FeatureExtractor", "MelSpectrogramMultiChannel(...)", y)
|
|
|
|
|
y += 40
|
|
|
|
|
msg("m5", "Pipeline", "GunshotClassifier", "Predict(avg_mel)", y)
|
|
|
|
|
y += 40
|
|
|
|
|
msg("m6", "Pipeline", "GccPhatLocalizer", "Localize(audio_mat)", y)
|
|
|
|
|
y += 40
|
|
|
|
|
msg("m7", "Pipeline", "DistanceEstimator", "Estimate(spl, label)", y)
|
|
|
|
|
y += 40
|
|
|
|
|
msg("m8", "Pipeline", "ThreatTracker", "Update(threat)", y)
|
|
|
|
|
y += 40
|
|
|
|
|
msg("m9", "Pipeline", "AcousticNode", "return AcousticFrame", y, dashed=True)
|
|
|
|
|
y += 40
|
|
|
|
|
msg("m10", "AcousticNode", "ThreatPublisher", "Publish(frame)", y)
|
|
|
|
|
|
|
|
|
|
# activation bars (simplified as small rectangles)
|
|
|
|
|
for name, x in lifelines:
|
|
|
|
|
add_cell(cells, f"act_{name}", "1", cell_style("activation2"), "", x+40, 100, 20, y-60)
|
|
|
|
|
|
|
|
|
|
# note
|
|
|
|
|
add_cell(cells, "note1", "1", cell_style("boundary"),
|
|
|
|
|
"设计思路:每个调用都是同步阻塞调用,数据沿调用链逐层传递。这种设计确保了单线程内数据一致性,简化了实时系统的并发控制。",
|
|
|
|
|
40, y+40, 400, 60)
|
|
|
|
|
|
|
|
|
|
return make_graph_model(cells, w=1500, h=700)
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# 图3:组件图 — 声源分析模块分层
|
|
|
|
|
# ============================================================
|
|
|
|
|
def build_component_diagram():
|
|
|
|
|
cells = []
|
|
|
|
|
add_cell(cells, "title3", "1", cell_style("title"),
|
|
|
|
|
"图3 组件图 — 声源分析模块分层组件结构", 20, 10, 600, 30)
|
|
|
|
|
|
|
|
|
|
# Core component
|
|
|
|
|
add_cell(cells, "comp_core", "1", cell_style("component"),
|
|
|
|
|
"«component»\nacoustic_analyzer_core\n\n• AudioBuffer\n• FeatureExtractor\n• GunshotClassifier\n• GccPhatLocalizer\n• DistanceEstimator\n• ThreatTracker\n• Pipeline",
|
|
|
|
|
80, 80, 220, 200)
|
|
|
|
|
|
|
|
|
|
# IO component
|
|
|
|
|
add_cell(cells, "comp_io", "1", cell_style("component"),
|
|
|
|
|
"«component»\nacoustic_analyzer_io\n\n• AudioSource (interface)\n• WavFileSource\n• MobilePhoneSource",
|
|
|
|
|
80, 320, 220, 120)
|
|
|
|
|
|
|
|
|
|
# ROS component
|
|
|
|
|
add_cell(cells, "comp_ros", "1", cell_style("component"),
|
|
|
|
|
"«component»\nacoustic_analyzer_ros\n\n• AcousticNode\n• ThreatPublisher",
|
|
|
|
|
400, 80, 200, 100)
|
|
|
|
|
|
|
|
|
|
# External libraries
|
|
|
|
|
add_cell(cells, "ext_onnx", "1", cell_style("artifact"),
|
|
|
|
|
"«library»\nONNX Runtime", 400, 220, 140, 60)
|
|
|
|
|
add_cell(cells, "ext_eigen", "1", cell_style("artifact"),
|
|
|
|
|
"«library»\nEigen 3.4.0", 400, 300, 140, 60)
|
|
|
|
|
add_cell(cells, "ext_yaml", "1", cell_style("artifact"),
|
|
|
|
|
"«library»\nyaml-cpp", 400, 380, 140, 60)
|
|
|
|
|
add_cell(cells, "ext_ros", "1", cell_style("artifact"),
|
|
|
|
|
"«framework»\nROS (roscpp)", 680, 80, 140, 60)
|
|
|
|
|
|
|
|
|
|
# Dependencies
|
|
|
|
|
add_cell(cells, "d1", "1", cell_style("open_arrow"), "", edge=True, source="comp_ros", target="comp_core")
|
|
|
|
|
add_cell(cells, "d2", "1", cell_style("open_arrow"), "", edge=True, source="comp_ros", target="comp_io")
|
|
|
|
|
add_cell(cells, "d3", "1", cell_style("dashed_arrow"), "uses", edge=True, source="comp_core", target="ext_onnx")
|
|
|
|
|
add_cell(cells, "d4", "1", cell_style("dashed_arrow"), "uses", edge=True, source="comp_core", target="ext_eigen")
|
|
|
|
|
add_cell(cells, "d5", "1", cell_style("dashed_arrow"), "uses", edge=True, source="comp_core", target="ext_yaml")
|
|
|
|
|
add_cell(cells, "d6", "1", cell_style("dashed_arrow"), "uses", edge=True, source="comp_ros", target="ext_ros")
|
|
|
|
|
|
|
|
|
|
# Interface port
|
|
|
|
|
add_cell(cells, "iface1", "1", cell_style("interface"), "IAcousticSource", 300, 350, 40, 30)
|
|
|
|
|
add_cell(cells, "d7", "1", cell_style("open_arrow"), "", edge=True, source="comp_io", target="iface1")
|
|
|
|
|
add_cell(cells, "d8", "1", cell_style("open_arrow"), "", edge=True, source="iface1", target="comp_core")
|
|
|
|
|
|
|
|
|
|
# note
|
|
|
|
|
add_cell(cells, "note3", "1", cell_style("boundary"),
|
|
|
|
|
"设计思路:core 层仅依赖第三方数学库(Eigen/ONNX),完全不依赖 ROS 和 yaml-cpp。\n这使得核心算法可以在 Windows/Linux 上独立编译测试,实现跨平台复用。",
|
|
|
|
|
680, 200, 320, 70)
|
|
|
|
|
|
|
|
|
|
return make_graph_model(cells, w=1100, h=520)
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# 图4:用例图 — 单兵终端APP
|
|
|
|
|
# ============================================================
|
|
|
|
|
def build_usecase_diagram():
|
|
|
|
|
cells = []
|
|
|
|
|
add_cell(cells, "title4", "1", cell_style("title"),
|
|
|
|
|
"图4 用例图 — 单兵终端APP功能需求(用户视角)", 20, 10, 600, 30)
|
|
|
|
|
|
|
|
|
|
# Actor
|
|
|
|
|
add_cell(cells, "actor", "1", cell_style("actor"), "前线士兵", 60, 200, 40, 80)
|
|
|
|
|
|
|
|
|
|
# System boundary
|
|
|
|
|
add_cell(cells, "boundary", "1", cell_style("rectangle","fillColor=#f5f5f5;strokeColor=#666;"),
|
|
|
|
|
"单兵终端APP", 150, 80, 500, 420)
|
|
|
|
|
|
|
|
|
|
usecases = [
|
|
|
|
|
("UC1", "登录认证", 260, 120),
|
|
|
|
|
("UC2", "上报物资需求", 400, 120),
|
|
|
|
|
("UC3", "选择投放点", 260, 200),
|
|
|
|
|
("UC4", "查看任务状态", 400, 200),
|
|
|
|
|
("UC5", "查看无人机状态", 260, 280),
|
|
|
|
|
("UC6", "实时位置上报", 400, 280),
|
|
|
|
|
("UC7", "SOS一键求救", 260, 360),
|
|
|
|
|
("UC8", "服务器配置", 400, 360),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for uid, label, x, y in usecases:
|
|
|
|
|
add_cell(cells, uid, "1", cell_style("usecase"), label, x, y, 120, 50)
|
|
|
|
|
|
|
|
|
|
# Include / Extend relationships
|
|
|
|
|
add_cell(cells, "inc1", "1", cell_style("dashed_arrow"), "«include»", edge=True, source="UC2", target="UC3")
|
|
|
|
|
add_cell(cells, "ext1", "1", cell_style("dashed_arrow"), "«extend»", edge=True, source="UC7", target="UC6")
|
|
|
|
|
|
|
|
|
|
# Actor connections
|
|
|
|
|
for uid in ["UC1","UC2","UC3","UC4","UC5","UC6","UC7","UC8"]:
|
|
|
|
|
add_cell(cells, f"a_{uid}", "1", cell_style("arrow"), "", edge=True, source="actor", target=uid)
|
|
|
|
|
|
|
|
|
|
# note
|
|
|
|
|
add_cell(cells, "note4", "1", cell_style("boundary"),
|
|
|
|
|
"设计思路:用例图从用户视角描述了系统功能边界。\n"+
|
|
|
|
|
"「上报物资需求」包含「选择投放点」(必须),\n"+
|
|
|
|
|
"「SOS求救」扩展「实时位置上报」(自动触发位置发送)。",
|
|
|
|
|
700, 120, 280, 80)
|
|
|
|
|
|
|
|
|
|
return make_graph_model(cells, w=1100, h=600)
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# 图5:活动图 — 物资需求上报流程
|
|
|
|
|
# ============================================================
|
|
|
|
|
def build_activity_diagram():
|
|
|
|
|
cells = []
|
|
|
|
|
add_cell(cells, "title5", "1", cell_style("title"),
|
|
|
|
|
"图5 活动图 — 物资需求上报业务流程(行为视角)", 20, 10, 600, 30)
|
|
|
|
|
|
|
|
|
|
y = 60
|
|
|
|
|
# start
|
|
|
|
|
add_cell(cells, "a_start", "1", cell_style("start"), "", 400, y, 20, 20)
|
|
|
|
|
y += 40
|
|
|
|
|
add_cell(cells, "a_login", "1", cell_style("activity"), "登录认证", 360, y, 100, 40)
|
|
|
|
|
y += 60
|
|
|
|
|
add_cell(cells, "a_home", "1", cell_style("activity"), "进入首页", 360, y, 100, 40)
|
|
|
|
|
y += 60
|
|
|
|
|
add_cell(cells, "a_dec1", "1", cell_style("decision"), "选择功能", 360, y, 100, 50)
|
|
|
|
|
y += 70
|
|
|
|
|
|
|
|
|
|
# Branch: submit demand
|
|
|
|
|
add_cell(cells, "a_type", "1", cell_style("activity"), "选择物资类型", 180, y, 120, 40)
|
|
|
|
|
add_cell(cells, "a_demand", "1", cell_style("activity"), "输入数量/紧急程度", 340, y, 140, 40)
|
|
|
|
|
add_cell(cells, "a_drop", "1", cell_style("activity"), "查看推荐投放点", 520, y, 140, 40)
|
|
|
|
|
add_cell(cells, "a_map", "1", cell_style("activity"), "地图选点/搜索", 700, y, 140, 40)
|
|
|
|
|
|
|
|
|
|
# Edges from decision
|
|
|
|
|
add_cell(cells, "e_d1", "1", cell_style("arrow"), "上报需求", edge=True, source="a_dec1", target="a_type")
|
|
|
|
|
add_cell(cells, "e_d2", "1", cell_style("arrow"), "", edge=True, source="a_type", target="a_demand")
|
|
|
|
|
add_cell(cells, "e_d3", "1", cell_style("arrow"), "", edge=True, source="a_demand", target="a_drop")
|
|
|
|
|
add_cell(cells, "e_d4", "1", cell_style("arrow"), "", edge=True, source="a_drop", target="a_map")
|
|
|
|
|
|
|
|
|
|
y += 60
|
|
|
|
|
add_cell(cells, "a_confirm", "1", cell_style("decision"), "确认提交?", 360, y, 100, 50)
|
|
|
|
|
add_cell(cells, "e_d5", "1", cell_style("arrow"), "", edge=True, source="a_map", target="a_confirm")
|
|
|
|
|
|
|
|
|
|
y += 70
|
|
|
|
|
add_cell(cells, "a_submit", "1", cell_style("activity"), "调用 API.postDemand()", 340, y, 140, 40)
|
|
|
|
|
add_cell(cells, "a_cancel", "1", cell_style("activity"), "返回修改", 560, y, 100, 40)
|
|
|
|
|
add_cell(cells, "e_yes", "1", cell_style("arrow"), "是", edge=True, source="a_confirm", target="a_submit")
|
|
|
|
|
add_cell(cells, "e_no", "1", cell_style("arrow"), "否", edge=True, source="a_confirm", target="a_cancel")
|
|
|
|
|
add_cell(cells, "e_back", "1", cell_style("arrow"), "", edge=True, source="a_cancel", target="a_type")
|
|
|
|
|
|
|
|
|
|
y += 60
|
|
|
|
|
add_cell(cells, "a_dec2", "1", cell_style("decision"), "提交成功?", 360, y, 100, 50)
|
|
|
|
|
add_cell(cells, "e_d6", "1", cell_style("arrow"), "", edge=True, source="a_submit", target="a_dec2")
|
|
|
|
|
|
|
|
|
|
y += 70
|
|
|
|
|
add_cell(cells, "a_success", "1", cell_style("activity"), "显示成功提示\n跳转首页", 180, y, 140, 50)
|
|
|
|
|
add_cell(cells, "a_fail", "1", cell_style("activity"), "显示错误提示\n支持重试", 520, y, 140, 50)
|
|
|
|
|
add_cell(cells, "e_ok", "1", cell_style("arrow"), "成功", edge=True, source="a_dec2", target="a_success")
|
|
|
|
|
add_cell(cells, "e_err", "1", cell_style("arrow"), "失败", edge=True, source="a_dec2", target="a_fail")
|
|
|
|
|
|
|
|
|
|
y += 70
|
|
|
|
|
# merge
|
|
|
|
|
add_cell(cells, "a_merge", "1", cell_style("activity"), "结束", 360, y, 100, 40)
|
|
|
|
|
add_cell(cells, "e_m1", "1", cell_style("arrow"), "", edge=True, source="a_success", target="a_merge")
|
|
|
|
|
add_cell(cells, "e_m2", "1", cell_style("arrow"), "", edge=True, source="a_fail", target="a_merge")
|
|
|
|
|
|
|
|
|
|
y += 60
|
|
|
|
|
add_cell(cells, "a_end", "1", cell_style("end"), "", 400, y, 20, 20)
|
|
|
|
|
add_cell(cells, "e_end", "1", cell_style("arrow"), "", edge=True, source="a_merge", target="a_end")
|
|
|
|
|
|
|
|
|
|
# Swimlane labels
|
|
|
|
|
add_cell(cells, "sw1", "1", cell_style("text","fontStyle=1;fontSize=12;"), "【士兵操作】", 40, 80, 100, 20)
|
|
|
|
|
add_cell(cells, "sw2", "1", cell_style("text","fontStyle=1;fontSize=12;"), "【APP处理】", 40, 200, 100, 20)
|
|
|
|
|
add_cell(cells, "sw3", "1", cell_style("text","fontStyle=1;fontSize=12;"), "【后端交互】", 40, 400, 100, 20)
|
|
|
|
|
|
|
|
|
|
# note
|
|
|
|
|
add_cell(cells, "note5", "1", cell_style("boundary"),
|
|
|
|
|
"设计思路:活动图展示了物资需求上报的完整业务流程。\n"+
|
|
|
|
|
"关键设计:① 投放点选择支持「列表推荐」和「地图自由选点」两种模式;\n"+
|
|
|
|
|
"② API 调用失败时返回模拟数据(Mock),保证演示可用性;\n"+
|
|
|
|
|
"③ 全流程有明确的成功/失败分支和回退路径。",
|
|
|
|
|
40, 520, 420, 80)
|
|
|
|
|
|
|
|
|
|
return make_graph_model(cells, w=900, h=700)
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# 图6:部署图 — 系统物理部署
|
|
|
|
|
# ============================================================
|
|
|
|
|
def build_deployment_diagram():
|
|
|
|
|
cells = []
|
|
|
|
|
add_cell(cells, "title6", "1", cell_style("title"),
|
|
|
|
|
"图6 部署图 — 智途投送系统物理部署拓扑", 20, 10, 600, 30)
|
|
|
|
|
|
|
|
|
|
# Node 1: Soldier Phone
|
|
|
|
|
add_cell(cells, "node_phone", "1", cell_style("node"),
|
|
|
|
|
"«device»\n士兵手机\nAndroid 12+", 60, 80, 180, 120)
|
|
|
|
|
add_cell(cells, "art_app", "1", cell_style("artifact"),
|
|
|
|
|
"单兵终端APP\n(Capacitor/WebView)", 80, 130, 140, 50)
|
|
|
|
|
|
|
|
|
|
# Node 2: Server
|
|
|
|
|
add_cell(cells, "node_server", "1", cell_style("node"),
|
|
|
|
|
"«device»\n后方指挥所服务器\nUbuntu 22.04", 340, 80, 220, 140)
|
|
|
|
|
add_cell(cells, "art_flask", "1", cell_style("artifact"),
|
|
|
|
|
"Flask 后端\n(Python 3.10)", 360, 130, 180, 40)
|
|
|
|
|
add_cell(cells, "art_web", "1", cell_style("artifact"),
|
|
|
|
|
"Web 监控界面\n(HTML/JS/Leaflet)", 360, 180, 180, 30)
|
|
|
|
|
|
|
|
|
|
# Node 3: UAV
|
|
|
|
|
add_cell(cells, "node_uav", "1", cell_style("node"),
|
|
|
|
|
"«device»\n无人机机载计算机\nUbuntu 20.04 + ROS Noetic", 660, 80, 240, 180)
|
|
|
|
|
add_cell(cells, "art_ros", "1", cell_style("artifact"),
|
|
|
|
|
"ROS 节点网络\n(roscore + 多节点)", 680, 130, 200, 40)
|
|
|
|
|
add_cell(cells, "art_acoustic", "1", cell_style("artifact"),
|
|
|
|
|
"声源分析模块\n(C++17 / ONNX)", 680, 180, 200, 40)
|
|
|
|
|
add_cell(cells, "art_other", "1", cell_style("artifact"),
|
|
|
|
|
"视觉/热成像/路径规划节点", 680, 230, 200, 30)
|
|
|
|
|
|
|
|
|
|
# Communication links
|
|
|
|
|
add_cell(cells, "c1", "1", cell_style("arrow"),
|
|
|
|
|
"HTTP/REST\n(4G/WiFi)", edge=True, source="node_phone", target="node_server")
|
|
|
|
|
add_cell(cells, "c2", "1", cell_style("arrow"),
|
|
|
|
|
"ROS Topic\n(WebSocket/局域网)", edge=True, source="node_server", target="node_uav")
|
|
|
|
|
add_cell(cells, "c3", "1", cell_style("dashed_arrow"),
|
|
|
|
|
"rosbridge\n(可选直连)", edge=True, source="node_phone", target="node_uav")
|
|
|
|
|
|
|
|
|
|
# Hardware inside UAV
|
|
|
|
|
add_cell(cells, "hw_mic", "1", cell_style("rectangle","fillColor=#e0e0e0;"),
|
|
|
|
|
"麦克风阵列", 700, 300, 80, 40)
|
|
|
|
|
add_cell(cells, "hw_cam", "1", cell_style("rectangle","fillColor=#e0e0e0;"),
|
|
|
|
|
"可见光相机", 800, 300, 80, 40)
|
|
|
|
|
add_cell(cells, "hw_gps", "1", cell_style("rectangle","fillColor=#e0e0e0;"),
|
|
|
|
|
"GPS/IMU", 700, 360, 80, 40)
|
|
|
|
|
|
|
|
|
|
add_cell(cells, "c_hw1", "1", cell_style("arrow"), "", edge=True, source="hw_mic", target="node_uav")
|
|
|
|
|
add_cell(cells, "c_hw2", "1", cell_style("arrow"), "", edge=True, source="hw_cam", target="node_uav")
|
|
|
|
|
add_cell(cells, "c_hw3", "1", cell_style("arrow"), "", edge=True, source="hw_gps", target="node_uav")
|
|
|
|
|
|
|
|
|
|
# note
|
|
|
|
|
add_cell(cells, "note6", "1", cell_style("boundary"),
|
|
|
|
|
"设计思路:部署图展示了系统的物理分布和通信路径。\n"+
|
|
|
|
|
"关键设计:① 单兵APP通过 4G/WiFi 与后方服务器通信,不直接依赖无人机;\n"+
|
|
|
|
|
"② 服务器作为「消息中转站」,解耦了前线士兵与无人机控制;\n"+
|
|
|
|
|
"③ 无人机机载端运行 Ubuntu+ROS,通过局域网与机载传感器直连。",
|
|
|
|
|
60, 400, 520, 80)
|
|
|
|
|
|
|
|
|
|
return make_graph_model(cells, w=1000, h=560)
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Main
|
|
|
|
|
# ============================================================
|
|
|
|
|
pages = [
|
|
|
|
|
("01-类图-声源分析模块", build_class_diagram()),
|
|
|
|
|
("02-顺序图-Pipeline调用链", build_sequence_diagram()),
|
|
|
|
|
("03-组件图-声源分析分层", build_component_diagram()),
|
|
|
|
|
("04-用例图-单兵终端APP", build_usecase_diagram()),
|
|
|
|
|
("05-活动图-物资需求上报", build_activity_diagram()),
|
|
|
|
|
("06-部署图-系统物理拓扑", build_deployment_diagram()),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
root = make_mxfile(pages)
|
|
|
|
|
|
|
|
|
|
# Pretty print
|
|
|
|
|
def indent(elem, level=0):
|
|
|
|
|
i = "\n" + level*" "
|
|
|
|
|
if len(elem):
|
|
|
|
|
if not elem.text or not elem.text.strip():
|
|
|
|
|
elem.text = i + " "
|
|
|
|
|
if not elem.tail or not elem.tail.strip():
|
|
|
|
|
elem.tail = i
|
|
|
|
|
for child in elem:
|
|
|
|
|
indent(child, level+1)
|
|
|
|
|
if not child.tail or not child.tail.strip():
|
|
|
|
|
child.tail = i
|
|
|
|
|
else:
|
|
|
|
|
if level and (not elem.tail or not elem.tail.strip()):
|
|
|
|
|
elem.tail = i
|
|
|
|
|
|
|
|
|
|
indent(root)
|
|
|
|
|
|
|
|
|
|
tree = ET.ElementTree(root)
|
|
|
|
|
tree.write("uml_diagrams.drawio", encoding="utf-8", xml_declaration=True)
|
|
|
|
|
print("Generated: uml_diagrams.drawio (6 pages)")
|