Seed public segments and emit signal-conflict alarms
Extends ensure_default_templates to also seed the 5 公共段 (前端码车 / 前端放车 / 前端摆渡 / 窑尾摆渡 / 卸砖 / 回车) and their shared resource declarations (transfer_front / transfer_tail / unload_position / return_line / robot_arm) so operators have the full skeleton to bind equipment + signals against. Engine now runs a station signal-conflict check during the run-halt phase: any station whose presence and vacancy are both true with Good quality emits ops.alarm.signal_conflict + segment.fault_locked and transitions the segment to Faulted. Closes the final P8 alarm type from design doc §8.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a7f5c85032
commit
972938a8e6
|
|
@ -72,14 +72,15 @@ pub async fn run() {
|
|||
.expect("Failed to connect enabled sources");
|
||||
|
||||
if crate::seed::enabled_via_env() {
|
||||
match crate::seed::ensure_kiln_templates(&platform.pool).await {
|
||||
match crate::seed::ensure_default_templates(&platform.pool).await {
|
||||
Ok(report) => tracing::info!(
|
||||
"Seeded kiln templates (stations={}, segments={}, steps={})",
|
||||
"Seeded default templates (stations={}, segments={}, steps={}, resources={})",
|
||||
report.stations_inserted,
|
||||
report.segments_inserted,
|
||||
report.steps_inserted
|
||||
report.steps_inserted,
|
||||
report.resources_inserted
|
||||
),
|
||||
Err(err) => tracing::error!("Seed kiln templates failed: {}", err),
|
||||
Err(err) => tracing::error!("Seed default templates failed: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -242,6 +242,32 @@ async fn tick(
|
|||
runtime.state,
|
||||
SegmentState::Executing | SegmentState::Confirming | SegmentState::Resetting
|
||||
) {
|
||||
// Signal-conflict detection runs first: an impossible station state
|
||||
// means a sensor or wiring fault, which the engine should not
|
||||
// continue past regardless of how interlocks evaluate.
|
||||
let stations_referenced = collect_referenced_stations(steps, interlocks);
|
||||
if let Err((station_id, message)) =
|
||||
interlock::check_station_signal_conflicts(&stations_referenced, ctx, monitor)
|
||||
{
|
||||
let _ = state.event_manager.send(AppEvent::AlarmSignalConflict {
|
||||
segment_id: segment.id,
|
||||
message: message.clone(),
|
||||
});
|
||||
let _ = state.event_manager.send(AppEvent::SegmentFaultLocked {
|
||||
segment_id: segment.id,
|
||||
message: message.clone(),
|
||||
});
|
||||
tracing::warn!(
|
||||
"Engine: segment {} signal conflict on station {}: {}",
|
||||
segment.id,
|
||||
station_id,
|
||||
message
|
||||
);
|
||||
runtime.state = SegmentState::Faulted;
|
||||
runtime.fault_message = Some(message);
|
||||
return Some(runtime);
|
||||
}
|
||||
|
||||
let run_halt: Vec<&SegmentInterlock> = interlocks
|
||||
.iter()
|
||||
.filter(|i| i.applies_to == "run_halt")
|
||||
|
|
@ -600,6 +626,19 @@ async fn tick(
|
|||
}
|
||||
}
|
||||
|
||||
/// Collect distinct station ids touched by either a step's `target_station_id`
|
||||
/// or an interlock's `station_id`. Used for cross-cutting station health checks.
|
||||
fn collect_referenced_stations(
|
||||
steps: &[SegmentStep],
|
||||
interlocks: &[SegmentInterlock],
|
||||
) -> Vec<Uuid> {
|
||||
let mut ids: Vec<Uuid> = steps.iter().filter_map(|s| s.target_station_id).collect();
|
||||
ids.extend(interlocks.iter().filter_map(|i| i.station_id));
|
||||
ids.sort();
|
||||
ids.dedup();
|
||||
ids
|
||||
}
|
||||
|
||||
fn next_sequential(steps: &[SegmentStep], current: i32) -> Option<i32> {
|
||||
steps
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -238,6 +238,43 @@ pub fn evaluate_all(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Check each station for impossible signal states (design doc §8.1
|
||||
/// `ops.alarm.signal_conflict`). Currently flags any station whose `presence`
|
||||
/// and `vacancy` are both logically true at the same time with Good quality.
|
||||
///
|
||||
/// Returns `Ok(())` when all stations are consistent. Returns
|
||||
/// `Err((station_id, reason))` for the first station with a conflict.
|
||||
pub fn check_station_signal_conflicts(
|
||||
station_ids: &[Uuid],
|
||||
ctx: &InterlockContext,
|
||||
monitor: &HashMap<Uuid, PointMonitorInfo>,
|
||||
) -> Result<(), (Uuid, String)> {
|
||||
for station_id in station_ids {
|
||||
let Some(roles) = ctx.station_role_points.get(station_id) else {
|
||||
continue;
|
||||
};
|
||||
let presence = roles
|
||||
.get("presence")
|
||||
.and_then(|(pid, invert)| read_logical_bool(monitor, *pid, *invert));
|
||||
let vacancy = roles
|
||||
.get("vacancy")
|
||||
.and_then(|(pid, invert)| read_logical_bool(monitor, *pid, *invert));
|
||||
match (presence, vacancy) {
|
||||
(Some(true), Some(true)) => {
|
||||
return Err((
|
||||
*station_id,
|
||||
format!(
|
||||
"station {} reports presence=true and vacancy=true simultaneously",
|
||||
station_id
|
||||
),
|
||||
));
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -363,6 +400,41 @@ mod tests {
|
|||
assert!(evaluate(&rule, &ctx, &monitor).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn station_signal_conflict_flags_simultaneous_presence_and_vacancy() {
|
||||
let station_id = Uuid::new_v4();
|
||||
let presence_pid = Uuid::new_v4();
|
||||
let vacancy_pid = Uuid::new_v4();
|
||||
|
||||
let mut roles = HashMap::new();
|
||||
roles.insert("presence".to_string(), (presence_pid, false));
|
||||
roles.insert("vacancy".to_string(), (vacancy_pid, false));
|
||||
let mut station_role_points = HashMap::new();
|
||||
station_role_points.insert(station_id, roles);
|
||||
let ctx = InterlockContext {
|
||||
equipment_role_points: HashMap::new(),
|
||||
station_role_points,
|
||||
};
|
||||
|
||||
let mut monitor = HashMap::new();
|
||||
monitor.insert(presence_pid, monitor_entry(presence_pid, true, true));
|
||||
monitor.insert(vacancy_pid, monitor_entry(vacancy_pid, true, true));
|
||||
|
||||
let err = check_station_signal_conflicts(&[station_id], &ctx, &monitor)
|
||||
.expect_err("conflict should surface");
|
||||
assert_eq!(err.0, station_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn station_signal_conflict_ignores_missing_signals() {
|
||||
let station_id = Uuid::new_v4();
|
||||
let ctx = InterlockContext {
|
||||
equipment_role_points: HashMap::new(),
|
||||
station_role_points: HashMap::new(),
|
||||
};
|
||||
assert!(check_station_signal_conflicts(&[station_id], &ctx, &HashMap::new()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_all_returns_first_failure() {
|
||||
let pid_ok = Uuid::new_v4();
|
||||
|
|
|
|||
|
|
@ -1,22 +1,29 @@
|
|||
//! Kiln template seed (design doc §12 P5).
|
||||
//! Default segment templates (design doc §12 P5 + P7).
|
||||
//!
|
||||
//! Inserts skeleton stations + segments + steps for 1 号 / 2 号 dry-kiln lines
|
||||
//! so operators can start binding equipment / signals through the CRUD APIs
|
||||
//! instead of authoring every row by hand. Idempotent: re-running is a no-op
|
||||
//! when the codes already exist.
|
||||
//! Idempotently inserts the skeleton stations, segments, steps, and shared
|
||||
//! resource declarations operators need before they can wire equipment and
|
||||
//! signal bindings through the CRUD APIs. Re-running is a no-op once codes
|
||||
//! exist.
|
||||
//!
|
||||
//! Only the *control flow* is seeded; equipment / station-signal bindings stay
|
||||
//! operator-managed because the field config differs per site.
|
||||
//! Coverage:
|
||||
//!
|
||||
//! - 6 dry-kiln segments (infeed / step / outfeed × 2 kilns).
|
||||
//! - 6 public segments (front load / front release / front transfer /
|
||||
//! tail transfer / unload / return) wiring kiln 1 + kiln 2 with shared
|
||||
//! resource keys (`transfer_front`, `transfer_tail`, `unload_position`,
|
||||
//! `return_line`, `robot_arm`).
|
||||
//!
|
||||
//! Equipment / station-signal bindings stay operator-managed because the
|
||||
//! field config differs per site.
|
||||
|
||||
use sqlx::PgPool;
|
||||
|
||||
/// Insert kiln-1 / kiln-2 templates. Returns the number of rows freshly
|
||||
/// inserted (segments + stations + steps).
|
||||
pub async fn ensure_kiln_templates(pool: &PgPool) -> Result<TemplateReport, sqlx::Error> {
|
||||
/// Insert kiln + public segment templates. Returns counts so callers can log.
|
||||
pub async fn ensure_default_templates(pool: &PgPool) -> Result<TemplateReport, sqlx::Error> {
|
||||
let mut report = TemplateReport::default();
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
for station in kiln_template_stations() {
|
||||
for station in default_template_stations() {
|
||||
let inserted = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO station (code, name, line_code, segment_code, station_type, enabled, description)
|
||||
|
|
@ -35,7 +42,7 @@ pub async fn ensure_kiln_templates(pool: &PgPool) -> Result<TemplateReport, sqlx
|
|||
report.stations_inserted += inserted.rows_affected();
|
||||
}
|
||||
|
||||
for segment in kiln_template_segments() {
|
||||
for segment in default_template_segments() {
|
||||
let inserted = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO process_segment (
|
||||
|
|
@ -64,6 +71,23 @@ pub async fn ensure_kiln_templates(pool: &PgPool) -> Result<TemplateReport, sqlx
|
|||
.await?;
|
||||
let Some(segment_id) = segment_id else { continue };
|
||||
|
||||
// Resource declarations are idempotent at the row level (UNIQUE
|
||||
// constraint). Insert each declared key.
|
||||
for resource_key in default_segment_resources(segment.code) {
|
||||
let inserted = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO segment_resource (segment_id, resource_key)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.bind(segment_id)
|
||||
.bind(resource_key)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
report.resources_inserted += inserted.rows_affected();
|
||||
}
|
||||
|
||||
// Only seed steps for an empty segment. Once an operator owns the
|
||||
// step list, the seed mustn't trample it.
|
||||
let existing_step_count: i64 =
|
||||
|
|
@ -108,6 +132,7 @@ pub struct TemplateReport {
|
|||
pub stations_inserted: u64,
|
||||
pub segments_inserted: u64,
|
||||
pub steps_inserted: u64,
|
||||
pub resources_inserted: u64,
|
||||
}
|
||||
|
||||
struct StationTemplate {
|
||||
|
|
@ -119,47 +144,60 @@ struct StationTemplate {
|
|||
description: &'static str,
|
||||
}
|
||||
|
||||
fn default_template_stations() -> Vec<StationTemplate> {
|
||||
let mut out = Vec::new();
|
||||
out.extend(kiln_template_stations());
|
||||
out.extend(public_template_stations());
|
||||
out
|
||||
}
|
||||
|
||||
fn kiln_template_stations() -> Vec<StationTemplate> {
|
||||
let lines = [("KILN_1", "1 号"), ("KILN_2", "2 号")];
|
||||
let kinds: &[(&str, &str, &str)] = &[
|
||||
("dry_in", "干燥窑进口", "DRY_INFEED"),
|
||||
("dry_step", "干燥窑内", "DRY_STEP"),
|
||||
("dry_out", "干燥窑出口", "DRY_OUTFEED"),
|
||||
let dry: &[(&'static str, &'static str, &'static str, &'static str)] = &[
|
||||
// (line, code, name, station_type)
|
||||
("KILN_1", "ST-DRY1-IN", "1 号干燥窑进口位", "dry_in"),
|
||||
("KILN_1", "ST-DRY1-STEP", "1 号干燥窑内位", "dry_step"),
|
||||
("KILN_1", "ST-DRY1-OUT", "1 号干燥窑出口位", "dry_out"),
|
||||
("KILN_2", "ST-DRY2-IN", "2 号干燥窑进口位", "dry_in"),
|
||||
("KILN_2", "ST-DRY2-STEP", "2 号干燥窑内位", "dry_step"),
|
||||
("KILN_2", "ST-DRY2-OUT", "2 号干燥窑出口位", "dry_out"),
|
||||
];
|
||||
let mut out = Vec::with_capacity(lines.len() * kinds.len());
|
||||
for (line_code, line_name) in lines {
|
||||
for (kind, kind_name, segment_code) in kinds {
|
||||
let code = match (line_code, *kind) {
|
||||
("KILN_1", "dry_in") => "ST-DRY1-IN",
|
||||
("KILN_1", "dry_step") => "ST-DRY1-STEP",
|
||||
("KILN_1", "dry_out") => "ST-DRY1-OUT",
|
||||
("KILN_2", "dry_in") => "ST-DRY2-IN",
|
||||
("KILN_2", "dry_step") => "ST-DRY2-STEP",
|
||||
("KILN_2", "dry_out") => "ST-DRY2-OUT",
|
||||
_ => continue,
|
||||
};
|
||||
let name: &'static str = match (line_code, *kind) {
|
||||
("KILN_1", "dry_in") => "1 号干燥窑进口位",
|
||||
("KILN_1", "dry_step") => "1 号干燥窑内位",
|
||||
("KILN_1", "dry_out") => "1 号干燥窑出口位",
|
||||
("KILN_2", "dry_in") => "2 号干燥窑进口位",
|
||||
("KILN_2", "dry_step") => "2 号干燥窑内位",
|
||||
("KILN_2", "dry_out") => "2 号干燥窑出口位",
|
||||
_ => continue,
|
||||
};
|
||||
// Description is interned via &'static str by matching above; no need to format.
|
||||
let _ = (line_name, kind_name); // documentation-only context
|
||||
out.push(StationTemplate {
|
||||
dry.iter()
|
||||
.map(|(line, code, name, station_type)| StationTemplate {
|
||||
code,
|
||||
name,
|
||||
line_code,
|
||||
line_code: line,
|
||||
segment_code: match *station_type {
|
||||
"dry_in" => "DRY_INFEED",
|
||||
"dry_step" => "DRY_STEP",
|
||||
"dry_out" => "DRY_OUTFEED",
|
||||
_ => "DRY",
|
||||
},
|
||||
station_type,
|
||||
description: "Seeded by ensure_default_templates",
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn public_template_stations() -> Vec<StationTemplate> {
|
||||
let stations: &[(&'static str, &'static str, &'static str, &'static str)] = &[
|
||||
// (code, name, segment_code, station_type)
|
||||
("ST-FRONT-LOAD", "前端码车位", "FRONT_LOAD", "load"),
|
||||
("ST-FRONT-TRANSFER", "前端摆渡接车位", "FRONT_TRANSFER", "transfer"),
|
||||
("ST-TAIL-TRANSFER", "窑尾摆渡接车位", "TAIL_TRANSFER", "transfer"),
|
||||
("ST-UNLOAD", "卸砖机位", "UNLOAD", "unload"),
|
||||
("ST-RETURN-IN", "回车线入口位", "RETURN", "return"),
|
||||
];
|
||||
stations
|
||||
.iter()
|
||||
.map(|(code, name, segment_code, station_type)| StationTemplate {
|
||||
code,
|
||||
name,
|
||||
line_code: "COMMON",
|
||||
segment_code,
|
||||
station_type: kind,
|
||||
description: "Seeded by ensure_kiln_templates",
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
station_type,
|
||||
description: "Seeded by ensure_default_templates",
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct SegmentTemplate {
|
||||
|
|
@ -172,65 +210,74 @@ struct SegmentTemplate {
|
|||
description: &'static str,
|
||||
}
|
||||
|
||||
fn kiln_template_segments() -> Vec<SegmentTemplate> {
|
||||
fn default_template_segments() -> Vec<SegmentTemplate> {
|
||||
let mut out = Vec::new();
|
||||
for (line_code, label) in [("KILN_1", "1 号"), ("KILN_2", "2 号")] {
|
||||
out.push(SegmentTemplate {
|
||||
code: if line_code == "KILN_1" {
|
||||
"SEG-DRY1-INFEED"
|
||||
} else {
|
||||
"SEG-DRY2-INFEED"
|
||||
},
|
||||
name: if line_code == "KILN_1" {
|
||||
"1 号干燥窑进口段"
|
||||
} else {
|
||||
"2 号干燥窑进口段"
|
||||
},
|
||||
segment_type: "kiln_infeed",
|
||||
line_code,
|
||||
priority: 10,
|
||||
mode: "disabled",
|
||||
description: "Seeded skeleton; bind equipment + station signals to enable.",
|
||||
});
|
||||
out.push(SegmentTemplate {
|
||||
code: if line_code == "KILN_1" {
|
||||
"SEG-DRY1-STEP"
|
||||
} else {
|
||||
"SEG-DRY2-STEP"
|
||||
},
|
||||
name: if line_code == "KILN_1" {
|
||||
"1 号干燥窑内前移段"
|
||||
} else {
|
||||
"2 号干燥窑内前移段"
|
||||
},
|
||||
segment_type: "kiln_step",
|
||||
line_code,
|
||||
priority: 5,
|
||||
mode: "disabled",
|
||||
description: "Seeded skeleton; bind equipment + station signals to enable.",
|
||||
});
|
||||
out.push(SegmentTemplate {
|
||||
code: if line_code == "KILN_1" {
|
||||
"SEG-DRY1-OUTFEED"
|
||||
} else {
|
||||
"SEG-DRY2-OUTFEED"
|
||||
},
|
||||
name: if line_code == "KILN_1" {
|
||||
"1 号干燥窑出口段"
|
||||
} else {
|
||||
"2 号干燥窑出口段"
|
||||
},
|
||||
segment_type: "kiln_outfeed",
|
||||
line_code,
|
||||
priority: 10,
|
||||
mode: "disabled",
|
||||
description: "Seeded skeleton; bind equipment + station signals to enable.",
|
||||
});
|
||||
let _ = label;
|
||||
}
|
||||
out.extend(kiln_template_segments());
|
||||
out.extend(public_template_segments());
|
||||
out
|
||||
}
|
||||
|
||||
fn kiln_template_segments() -> Vec<SegmentTemplate> {
|
||||
let entries: &[(&'static str, &'static str, &'static str, &'static str, i32)] = &[
|
||||
// (line, code, name, segment_type, priority)
|
||||
("KILN_1", "SEG-DRY1-INFEED", "1 号干燥窑进口段", "kiln_infeed", 10),
|
||||
("KILN_1", "SEG-DRY1-STEP", "1 号干燥窑内前移段", "kiln_step", 5),
|
||||
("KILN_1", "SEG-DRY1-OUTFEED", "1 号干燥窑出口段", "kiln_outfeed", 10),
|
||||
("KILN_2", "SEG-DRY2-INFEED", "2 号干燥窑进口段", "kiln_infeed", 10),
|
||||
("KILN_2", "SEG-DRY2-STEP", "2 号干燥窑内前移段", "kiln_step", 5),
|
||||
("KILN_2", "SEG-DRY2-OUTFEED", "2 号干燥窑出口段", "kiln_outfeed", 10),
|
||||
];
|
||||
entries
|
||||
.iter()
|
||||
.map(|(line, code, name, segment_type, priority)| SegmentTemplate {
|
||||
code,
|
||||
name,
|
||||
segment_type,
|
||||
line_code: line,
|
||||
priority: *priority,
|
||||
mode: "disabled",
|
||||
description: "Seeded skeleton; bind equipment + station signals to enable.",
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn public_template_segments() -> Vec<SegmentTemplate> {
|
||||
let entries: &[(&'static str, &'static str, &'static str, i32)] = &[
|
||||
// (code, name, segment_type, priority)
|
||||
("SEG-FRONT-LOAD", "前端码车位进车段", "front_load", 10),
|
||||
("SEG-FRONT-RELEASE", "前端码车位放车段", "front_release", 10),
|
||||
("SEG-FRONT-TRANSFER", "前端摆渡分配段", "front_transfer", 8),
|
||||
("SEG-TAIL-TRANSFER", "窑尾摆渡接车段", "tail_transfer", 8),
|
||||
("SEG-UNLOAD", "卸砖机位段", "unload", 6),
|
||||
("SEG-RETURN", "回车线入口段", "return", 4),
|
||||
];
|
||||
entries
|
||||
.iter()
|
||||
.map(|(code, name, segment_type, priority)| SegmentTemplate {
|
||||
code,
|
||||
name,
|
||||
segment_type,
|
||||
line_code: "COMMON",
|
||||
priority: *priority,
|
||||
mode: "disabled",
|
||||
description: "Seeded skeleton; bind equipment + station signals to enable.",
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Shared-resource keys per public segment (design doc §7).
|
||||
/// Returns `[]` for segments that don't claim a public resource.
|
||||
pub fn default_segment_resources(segment_code: &str) -> Vec<&'static str> {
|
||||
match segment_code {
|
||||
"SEG-FRONT-LOAD" | "SEG-FRONT-RELEASE" => vec!["robot_arm"],
|
||||
"SEG-FRONT-TRANSFER" => vec!["transfer_front"],
|
||||
"SEG-TAIL-TRANSFER" => vec!["transfer_tail"],
|
||||
"SEG-UNLOAD" => vec!["unload_position"],
|
||||
"SEG-RETURN" => vec!["return_line"],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StepTemplate {
|
||||
pub step_no: i32,
|
||||
|
|
@ -241,7 +288,7 @@ pub struct StepTemplate {
|
|||
pub description: &'static str,
|
||||
}
|
||||
|
||||
/// Per-segment-type canonical step list (design doc §7.4 / §10.1).
|
||||
/// Per-segment-type canonical step list (design doc §7.4 / §10).
|
||||
pub fn build_step_template(segment_type: &str) -> Vec<StepTemplate> {
|
||||
match segment_type {
|
||||
"kiln_infeed" => vec![
|
||||
|
|
@ -320,6 +367,66 @@ pub fn build_step_template(segment_type: &str) -> Vec<StepTemplate> {
|
|||
description: "关门并等待门关到位",
|
||||
},
|
||||
],
|
||||
"front_load" => vec![
|
||||
StepTemplate {
|
||||
step_no: 1,
|
||||
step_code: "ROBOT_PERMIT",
|
||||
action_kind: "robot_permit",
|
||||
confirm_signal_role: "done",
|
||||
timeout_ms: 30_000,
|
||||
description: "允许机械臂码坯并等待完成",
|
||||
},
|
||||
StepTemplate {
|
||||
step_no: 2,
|
||||
step_code: "WAIT_RELEASE_READY",
|
||||
action_kind: "wait_signal",
|
||||
confirm_signal_role: "arrived",
|
||||
timeout_ms: 60_000,
|
||||
description: "等待码车位放车确认",
|
||||
},
|
||||
],
|
||||
"front_release" => vec![StepTemplate {
|
||||
step_no: 1,
|
||||
step_code: "ROBOT_RELEASE",
|
||||
action_kind: "robot_release",
|
||||
confirm_signal_role: "done",
|
||||
timeout_ms: 30_000,
|
||||
description: "码车放车并等待完成",
|
||||
}],
|
||||
"front_transfer" | "tail_transfer" => vec![
|
||||
StepTemplate {
|
||||
step_no: 1,
|
||||
step_code: "MOVE_TO_TARGET",
|
||||
action_kind: "transfer_move_to",
|
||||
confirm_signal_role: "arrived",
|
||||
timeout_ms: 60_000,
|
||||
description: "摆渡车定位到目标工位",
|
||||
},
|
||||
StepTemplate {
|
||||
step_no: 2,
|
||||
step_code: "WAIT_HANDOFF",
|
||||
action_kind: "wait_signal",
|
||||
confirm_signal_role: "done",
|
||||
timeout_ms: 60_000,
|
||||
description: "等待上下游交接完成",
|
||||
},
|
||||
],
|
||||
"unload" => vec![StepTemplate {
|
||||
step_no: 1,
|
||||
step_code: "STEP_ONCE",
|
||||
action_kind: "step_once",
|
||||
confirm_signal_role: "arrived",
|
||||
timeout_ms: 30_000,
|
||||
description: "卸砖位步进一格并等待到位",
|
||||
}],
|
||||
"return" => vec![StepTemplate {
|
||||
step_no: 1,
|
||||
step_code: "STEP_ONCE",
|
||||
action_kind: "step_once",
|
||||
confirm_signal_role: "arrived",
|
||||
timeout_ms: 30_000,
|
||||
description: "回车线步进一格并等待到位",
|
||||
}],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -360,27 +467,48 @@ mod tests {
|
|||
assert!(steps.iter().any(|s| s.action_kind == "pull_retract"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_segments_use_move_to_action() {
|
||||
let steps = build_step_template("front_transfer");
|
||||
assert!(steps.iter().any(|s| s.action_kind == "transfer_move_to"));
|
||||
let steps = build_step_template("tail_transfer");
|
||||
assert!(steps.iter().any(|s| s.action_kind == "transfer_move_to"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_segment_type_yields_empty_template() {
|
||||
assert!(build_step_template("unknown").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn station_template_covers_both_kilns() {
|
||||
let stations = kiln_template_stations();
|
||||
assert_eq!(stations.len(), 6);
|
||||
fn station_template_covers_kilns_and_public() {
|
||||
let stations = default_template_stations();
|
||||
assert_eq!(stations.len(), 6 + 5);
|
||||
assert!(stations.iter().any(|s| s.code == "ST-DRY1-IN"));
|
||||
assert!(stations.iter().any(|s| s.code == "ST-DRY2-OUT"));
|
||||
assert!(stations.iter().any(|s| s.code == "ST-FRONT-LOAD"));
|
||||
assert!(stations.iter().any(|s| s.code == "ST-RETURN-IN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segment_template_covers_three_per_kiln() {
|
||||
let segments = kiln_template_segments();
|
||||
assert_eq!(segments.len(), 6);
|
||||
fn segment_template_covers_kilns_and_public() {
|
||||
let segments = default_template_segments();
|
||||
assert_eq!(segments.len(), 6 + 6);
|
||||
let kiln_1 = segments.iter().filter(|s| s.line_code == "KILN_1").count();
|
||||
let kiln_2 = segments.iter().filter(|s| s.line_code == "KILN_2").count();
|
||||
let common = segments.iter().filter(|s| s.line_code == "COMMON").count();
|
||||
assert_eq!(kiln_1, 3);
|
||||
assert_eq!(kiln_2, 3);
|
||||
assert_eq!(common, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_keys_match_design_doc_section_7() {
|
||||
assert_eq!(default_segment_resources("SEG-FRONT-TRANSFER"), vec!["transfer_front"]);
|
||||
assert_eq!(default_segment_resources("SEG-TAIL-TRANSFER"), vec!["transfer_tail"]);
|
||||
assert_eq!(default_segment_resources("SEG-UNLOAD"), vec!["unload_position"]);
|
||||
assert_eq!(default_segment_resources("SEG-RETURN"), vec!["return_line"]);
|
||||
assert_eq!(default_segment_resources("SEG-FRONT-LOAD"), vec!["robot_arm"]);
|
||||
assert!(default_segment_resources("SEG-DRY1-INFEED").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -78,4 +78,8 @@
|
|||
## 环境变量
|
||||
|
||||
- `SIMULATE_PLC=1` — 调试模式,引擎发出命令后通过模拟器把确认信号写回监控缓存,使段流程可在无 PLC 现场时端到端运行。
|
||||
- `OPS_SEED_TEMPLATES=1` — 应用启动时自动写入 1 号 / 2 号干燥窑 6 段(infeed / step / outfeed × 2)的段 + 步骤骨架,仅插入缺失的记录,不覆盖已有配置。设备与工位信号绑定仍需通过 CRUD API 完成。
|
||||
- `OPS_SEED_TEMPLATES=1` — 应用启动时自动写入默认骨架:
|
||||
- 6 个干燥窑段(infeed / step / outfeed × 1 号 / 2 号)
|
||||
- 6 个公共段(前端码车 / 前端放车 / 前端摆渡 / 窑尾摆渡 / 卸砖 / 回车),并写入对应共享资源 key(`transfer_front` / `transfer_tail` / `unload_position` / `return_line` / `robot_arm`)
|
||||
- 关联工位(含 5 个公共工位)
|
||||
- 仅插入缺失的记录,不覆盖已有配置。设备与工位信号绑定仍需通过 CRUD API 完成。
|
||||
|
|
|
|||
Loading…
Reference in New Issue