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");
|
.expect("Failed to connect enabled sources");
|
||||||
|
|
||||||
if crate::seed::enabled_via_env() {
|
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!(
|
Ok(report) => tracing::info!(
|
||||||
"Seeded kiln templates (stations={}, segments={}, steps={})",
|
"Seeded default templates (stations={}, segments={}, steps={}, resources={})",
|
||||||
report.stations_inserted,
|
report.stations_inserted,
|
||||||
report.segments_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,
|
runtime.state,
|
||||||
SegmentState::Executing | SegmentState::Confirming | SegmentState::Resetting
|
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
|
let run_halt: Vec<&SegmentInterlock> = interlocks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| i.applies_to == "run_halt")
|
.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> {
|
fn next_sequential(steps: &[SegmentStep], current: i32) -> Option<i32> {
|
||||||
steps
|
steps
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,43 @@ pub fn evaluate_all(
|
||||||
Ok(())
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -363,6 +400,41 @@ mod tests {
|
||||||
assert!(evaluate(&rule, &ctx, &monitor).is_err());
|
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]
|
#[test]
|
||||||
fn evaluate_all_returns_first_failure() {
|
fn evaluate_all_returns_first_failure() {
|
||||||
let pid_ok = Uuid::new_v4();
|
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
|
//! Idempotently inserts the skeleton stations, segments, steps, and shared
|
||||||
//! so operators can start binding equipment / signals through the CRUD APIs
|
//! resource declarations operators need before they can wire equipment and
|
||||||
//! instead of authoring every row by hand. Idempotent: re-running is a no-op
|
//! signal bindings through the CRUD APIs. Re-running is a no-op once codes
|
||||||
//! when the codes already exist.
|
//! exist.
|
||||||
//!
|
//!
|
||||||
//! Only the *control flow* is seeded; equipment / station-signal bindings stay
|
//! Coverage:
|
||||||
//! operator-managed because the field config differs per site.
|
//!
|
||||||
|
//! - 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;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
/// Insert kiln-1 / kiln-2 templates. Returns the number of rows freshly
|
/// Insert kiln + public segment templates. Returns counts so callers can log.
|
||||||
/// inserted (segments + stations + steps).
|
pub async fn ensure_default_templates(pool: &PgPool) -> Result<TemplateReport, sqlx::Error> {
|
||||||
pub async fn ensure_kiln_templates(pool: &PgPool) -> Result<TemplateReport, sqlx::Error> {
|
|
||||||
let mut report = TemplateReport::default();
|
let mut report = TemplateReport::default();
|
||||||
let mut tx = pool.begin().await?;
|
let mut tx = pool.begin().await?;
|
||||||
|
|
||||||
for station in kiln_template_stations() {
|
for station in default_template_stations() {
|
||||||
let inserted = sqlx::query(
|
let inserted = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO station (code, name, line_code, segment_code, station_type, enabled, description)
|
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();
|
report.stations_inserted += inserted.rows_affected();
|
||||||
}
|
}
|
||||||
|
|
||||||
for segment in kiln_template_segments() {
|
for segment in default_template_segments() {
|
||||||
let inserted = sqlx::query(
|
let inserted = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO process_segment (
|
INSERT INTO process_segment (
|
||||||
|
|
@ -64,6 +71,23 @@ pub async fn ensure_kiln_templates(pool: &PgPool) -> Result<TemplateReport, sqlx
|
||||||
.await?;
|
.await?;
|
||||||
let Some(segment_id) = segment_id else { continue };
|
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
|
// Only seed steps for an empty segment. Once an operator owns the
|
||||||
// step list, the seed mustn't trample it.
|
// step list, the seed mustn't trample it.
|
||||||
let existing_step_count: i64 =
|
let existing_step_count: i64 =
|
||||||
|
|
@ -108,6 +132,7 @@ pub struct TemplateReport {
|
||||||
pub stations_inserted: u64,
|
pub stations_inserted: u64,
|
||||||
pub segments_inserted: u64,
|
pub segments_inserted: u64,
|
||||||
pub steps_inserted: u64,
|
pub steps_inserted: u64,
|
||||||
|
pub resources_inserted: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StationTemplate {
|
struct StationTemplate {
|
||||||
|
|
@ -119,47 +144,60 @@ struct StationTemplate {
|
||||||
description: &'static str,
|
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> {
|
fn kiln_template_stations() -> Vec<StationTemplate> {
|
||||||
let lines = [("KILN_1", "1 号"), ("KILN_2", "2 号")];
|
let dry: &[(&'static str, &'static str, &'static str, &'static str)] = &[
|
||||||
let kinds: &[(&str, &str, &str)] = &[
|
// (line, code, name, station_type)
|
||||||
("dry_in", "干燥窑进口", "DRY_INFEED"),
|
("KILN_1", "ST-DRY1-IN", "1 号干燥窑进口位", "dry_in"),
|
||||||
("dry_step", "干燥窑内", "DRY_STEP"),
|
("KILN_1", "ST-DRY1-STEP", "1 号干燥窑内位", "dry_step"),
|
||||||
("dry_out", "干燥窑出口", "DRY_OUTFEED"),
|
("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());
|
dry.iter()
|
||||||
for (line_code, line_name) in lines {
|
.map(|(line, code, name, station_type)| StationTemplate {
|
||||||
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 {
|
|
||||||
code,
|
code,
|
||||||
name,
|
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,
|
segment_code,
|
||||||
station_type: kind,
|
station_type,
|
||||||
description: "Seeded by ensure_kiln_templates",
|
description: "Seeded by ensure_default_templates",
|
||||||
});
|
})
|
||||||
}
|
.collect()
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SegmentTemplate {
|
struct SegmentTemplate {
|
||||||
|
|
@ -172,65 +210,74 @@ struct SegmentTemplate {
|
||||||
description: &'static str,
|
description: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kiln_template_segments() -> Vec<SegmentTemplate> {
|
fn default_template_segments() -> Vec<SegmentTemplate> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for (line_code, label) in [("KILN_1", "1 号"), ("KILN_2", "2 号")] {
|
out.extend(kiln_template_segments());
|
||||||
out.push(SegmentTemplate {
|
out.extend(public_template_segments());
|
||||||
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
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StepTemplate {
|
pub struct StepTemplate {
|
||||||
pub step_no: i32,
|
pub step_no: i32,
|
||||||
|
|
@ -241,7 +288,7 @@ pub struct StepTemplate {
|
||||||
pub description: &'static str,
|
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> {
|
pub fn build_step_template(segment_type: &str) -> Vec<StepTemplate> {
|
||||||
match segment_type {
|
match segment_type {
|
||||||
"kiln_infeed" => vec![
|
"kiln_infeed" => vec![
|
||||||
|
|
@ -320,6 +367,66 @@ pub fn build_step_template(segment_type: &str) -> Vec<StepTemplate> {
|
||||||
description: "关门并等待门关到位",
|
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(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -360,27 +467,48 @@ mod tests {
|
||||||
assert!(steps.iter().any(|s| s.action_kind == "pull_retract"));
|
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]
|
#[test]
|
||||||
fn unknown_segment_type_yields_empty_template() {
|
fn unknown_segment_type_yields_empty_template() {
|
||||||
assert!(build_step_template("unknown").is_empty());
|
assert!(build_step_template("unknown").is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn station_template_covers_both_kilns() {
|
fn station_template_covers_kilns_and_public() {
|
||||||
let stations = kiln_template_stations();
|
let stations = default_template_stations();
|
||||||
assert_eq!(stations.len(), 6);
|
assert_eq!(stations.len(), 6 + 5);
|
||||||
assert!(stations.iter().any(|s| s.code == "ST-DRY1-IN"));
|
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]
|
#[test]
|
||||||
fn segment_template_covers_three_per_kiln() {
|
fn segment_template_covers_kilns_and_public() {
|
||||||
let segments = kiln_template_segments();
|
let segments = default_template_segments();
|
||||||
assert_eq!(segments.len(), 6);
|
assert_eq!(segments.len(), 6 + 6);
|
||||||
let kiln_1 = segments.iter().filter(|s| s.line_code == "KILN_1").count();
|
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 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_1, 3);
|
||||||
assert_eq!(kiln_2, 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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -78,4 +78,8 @@
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
- `SIMULATE_PLC=1` — 调试模式,引擎发出命令后通过模拟器把确认信号写回监控缓存,使段流程可在无 PLC 现场时端到端运行。
|
- `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