Skip to main content

az_device_contract_codegen/
planner.rs

1use crate::model::{
2    ApiKind, ContractMethod, ContractRenderRequest, ContractService, ContractTypedItem,
3    ReadReturnKind, ValueType,
4};
5use std::fmt::{Display, Formatter};
6
7pub const STRING_BYTE_CAPACITY: usize = 32;
8pub const STRING_REGISTER_WIDTH: u16 = 17;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum AreaKind {
12    Coil,
13    DiscreteInput,
14    HoldingRegister,
15    InputRegister,
16}
17
18impl AreaKind {
19    pub fn c_label(&self) -> &'static str {
20        match self {
21            Self::Coil => "线圈",
22            Self::DiscreteInput => "离散输入",
23            Self::HoldingRegister => "保持寄存器",
24            Self::InputRegister => "输入寄存器",
25        }
26    }
27
28    pub fn function_code_label(&self) -> &'static str {
29        match self {
30            Self::Coil => "0x05/0x0F",
31            Self::DiscreteInput => "0x02",
32            Self::HoldingRegister => "0x06/0x10",
33            Self::InputRegister => "0x04",
34        }
35    }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum MethodWireKind {
40    Bit,
41    Register,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum PlannedValue {
46    Dto {
47        type_name: String,
48        items: Vec<ItemPlan>,
49    },
50    Scalar {
51        item: ItemPlan,
52    },
53    Parameters {
54        items: Vec<ItemPlan>,
55    },
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ItemPlan {
60    pub name: String,
61    pub summary: Option<String>,
62    pub value_type: ValueType,
63    pub offset: u16,
64    pub width: u16,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct MethodPlan {
69    pub api_kind: ApiKind,
70    pub interface_name: String,
71    pub interface_summary: Option<String>,
72    pub method_name: String,
73    pub method_summary: Option<String>,
74    pub c_method_name: String,
75    pub rust_method_name: String,
76    pub area_kind: AreaKind,
77    pub wire_kind: MethodWireKind,
78    pub start_address: u16,
79    pub quantity: u16,
80    pub planned_value: PlannedValue,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ContractPlan {
85    pub services: Vec<ContractService>,
86    pub methods: Vec<MethodPlan>,
87    pub total_coils: u16,
88    pub total_discrete_inputs: u16,
89    pub total_holding_registers: u16,
90    pub total_input_registers: u16,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ContractPlanError {
95    message: String,
96}
97
98impl ContractPlanError {
99    fn new(message: impl Into<String>) -> Self {
100        Self {
101            message: message.into(),
102        }
103    }
104}
105
106impl Display for ContractPlanError {
107    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
108        formatter.write_str(&self.message)
109    }
110}
111
112impl std::error::Error for ContractPlanError {}
113
114pub fn build_plan(request: &ContractRenderRequest) -> Result<ContractPlan, ContractPlanError> {
115    if request.transports.is_empty() {
116        return Err(ContractPlanError::new("至少选择一种传输类型"));
117    }
118    if request.services.is_empty() {
119        return Err(ContractPlanError::new("至少定义一个 Api 服务"));
120    }
121
122    let mut coil_cursor = 0_u16;
123    let mut discrete_input_cursor = 0_u16;
124    let mut holding_register_cursor = 0_u16;
125    let mut input_register_cursor = 0_u16;
126    let mut methods = Vec::new();
127
128    for service in &request.services {
129        if service.methods.is_empty() {
130            return Err(ContractPlanError::new(format!(
131                "服务 {} 至少需要一个方法",
132                service.interface_name
133            )));
134        }
135        for method in &service.methods {
136            let normalized_interface_name = normalize_type_name(&service.interface_name);
137            let c_method_name = to_snake_case(&method.method_name);
138            let rust_method_name = c_method_name.clone();
139            let (area_kind, wire_kind, planned_value) = plan_method_value(service, method)?;
140            let quantity = planned_value_width(&planned_value);
141            let start_address = match area_kind {
142                AreaKind::Coil => {
143                    let start = coil_cursor;
144                    coil_cursor = checked_next_cursor(coil_cursor, quantity, "线圈")?;
145                    start
146                }
147                AreaKind::DiscreteInput => {
148                    let start = discrete_input_cursor;
149                    discrete_input_cursor =
150                        checked_next_cursor(discrete_input_cursor, quantity, "离散输入")?;
151                    start
152                }
153                AreaKind::HoldingRegister => {
154                    let start = holding_register_cursor;
155                    holding_register_cursor =
156                        checked_next_cursor(holding_register_cursor, quantity, "保持寄存器")?;
157                    start
158                }
159                AreaKind::InputRegister => {
160                    let start = input_register_cursor;
161                    input_register_cursor =
162                        checked_next_cursor(input_register_cursor, quantity, "输入寄存器")?;
163                    start
164                }
165            };
166            methods.push(MethodPlan {
167                api_kind: service.api_kind.clone(),
168                interface_name: normalized_interface_name,
169                interface_summary: trim_option(&service.interface_summary),
170                method_name: method.method_name.trim().to_string(),
171                method_summary: trim_option(&method.summary),
172                c_method_name,
173                rust_method_name,
174                area_kind,
175                wire_kind,
176                start_address,
177                quantity,
178                planned_value,
179            });
180        }
181    }
182
183    Ok(ContractPlan {
184        services: request.services.clone(),
185        methods,
186        total_coils: coil_cursor,
187        total_discrete_inputs: discrete_input_cursor,
188        total_holding_registers: holding_register_cursor,
189        total_input_registers: input_register_cursor,
190    })
191}
192
193fn checked_next_cursor(cursor: u16, quantity: u16, label: &str) -> Result<u16, ContractPlanError> {
194    cursor
195        .checked_add(quantity)
196        .ok_or_else(|| ContractPlanError::new(format!("{label}地址规划超出 16 位地址上限")))
197}
198
199fn plan_method_value(
200    service: &ContractService,
201    method: &ContractMethod,
202) -> Result<(AreaKind, MethodWireKind, PlannedValue), ContractPlanError> {
203    let method_name = method.method_name.trim();
204    if method_name.is_empty() {
205        return Err(ContractPlanError::new("方法名不能为空"));
206    }
207
208    match service.api_kind {
209        ApiKind::Write => {
210            if method.parameters.is_empty() {
211                return Err(ContractPlanError::new(format!(
212                    "写方法 {method_name} 至少需要一个参数"
213                )));
214            }
215            let bit_only = method
216                .parameters
217                .iter()
218                .all(|parameter| matches!(parameter.value_type, ValueType::Boolean));
219            let items = build_item_plans(&method.parameters, bit_only)?;
220            let planned_value = PlannedValue::Parameters { items };
221            Ok((
222                if bit_only {
223                    AreaKind::Coil
224                } else {
225                    AreaKind::HoldingRegister
226                },
227                if bit_only {
228                    MethodWireKind::Bit
229                } else {
230                    MethodWireKind::Register
231                },
232                planned_value,
233            ))
234        }
235        ApiKind::StaticRead | ApiKind::RuntimeRead => {
236            let read_return_kind = method
237                .read_return_kind
238                .clone()
239                .unwrap_or(ReadReturnKind::Dto);
240            let planned_value = match read_return_kind {
241                ReadReturnKind::Dto => {
242                    if method.read_fields.is_empty() {
243                        return Err(ContractPlanError::new(format!(
244                            "读方法 {method_name} 选择 DTO 返回时至少需要一个字段"
245                        )));
246                    }
247                    let dto_type_name = normalize_type_name(
248                        method
249                            .read_return_type_name
250                            .as_deref()
251                            .unwrap_or(method_name),
252                    );
253                    let bit_only = method
254                        .read_fields
255                        .iter()
256                        .all(|field| matches!(field.value_type, ValueType::Boolean));
257                    let items = build_item_plans(&method.read_fields, bit_only)?;
258                    (
259                        if matches!(service.api_kind, ApiKind::RuntimeRead) && bit_only {
260                            AreaKind::DiscreteInput
261                        } else {
262                            AreaKind::InputRegister
263                        },
264                        if bit_only {
265                            MethodWireKind::Bit
266                        } else {
267                            MethodWireKind::Register
268                        },
269                        PlannedValue::Dto {
270                            type_name: dto_type_name,
271                            items,
272                        },
273                    )
274                }
275                other => {
276                    let scalar_type = read_return_kind_to_value_type(&other);
277                    let bit_only = matches!(scalar_type, ValueType::Boolean)
278                        && matches!(service.api_kind, ApiKind::RuntimeRead);
279                    let item = ItemPlan {
280                        name: "value".to_string(),
281                        summary: trim_option(&method.summary),
282                        value_type: scalar_type.clone(),
283                        offset: 0,
284                        width: value_type_width(&scalar_type, bit_only),
285                    };
286                    (
287                        if bit_only {
288                            AreaKind::DiscreteInput
289                        } else {
290                            AreaKind::InputRegister
291                        },
292                        if bit_only {
293                            MethodWireKind::Bit
294                        } else {
295                            MethodWireKind::Register
296                        },
297                        PlannedValue::Scalar { item },
298                    )
299                }
300            };
301            Ok(planned_value)
302        }
303    }
304}
305
306fn build_item_plans(
307    items: &[ContractTypedItem],
308    bit_only: bool,
309) -> Result<Vec<ItemPlan>, ContractPlanError> {
310    let mut offset = 0_u16;
311    let mut planned = Vec::with_capacity(items.len());
312    for item in items {
313        let item_name = item.name.trim();
314        if item_name.is_empty() {
315            return Err(ContractPlanError::new("字段或参数名不能为空"));
316        }
317        let width = value_type_width(&item.value_type, bit_only);
318        planned.push(ItemPlan {
319            name: to_snake_case(item_name),
320            summary: trim_option(&item.summary),
321            value_type: item.value_type.clone(),
322            offset,
323            width,
324        });
325        offset = checked_next_cursor(offset, width, "方法块")?;
326    }
327    Ok(planned)
328}
329
330fn planned_value_width(value: &PlannedValue) -> u16 {
331    match value {
332        PlannedValue::Dto { items, .. } | PlannedValue::Parameters { items } => items
333            .iter()
334            .map(|item| item.width)
335            .fold(0_u16, |summary, width| summary.saturating_add(width)),
336        PlannedValue::Scalar { item } => item.width,
337    }
338}
339
340fn value_type_width(value_type: &ValueType, bit_only: bool) -> u16 {
341    if bit_only {
342        return 1;
343    }
344    match value_type {
345        ValueType::Boolean => 1,
346        ValueType::Int => 2,
347        ValueType::String | ValueType::Bytes => STRING_REGISTER_WIDTH,
348    }
349}
350
351fn read_return_kind_to_value_type(read_return_kind: &ReadReturnKind) -> ValueType {
352    match read_return_kind {
353        ReadReturnKind::Boolean => ValueType::Boolean,
354        ReadReturnKind::Int => ValueType::Int,
355        ReadReturnKind::String => ValueType::String,
356        ReadReturnKind::Bytes => ValueType::Bytes,
357        ReadReturnKind::Dto => ValueType::Bytes,
358    }
359}
360
361pub fn trim_option(value: &Option<String>) -> Option<String> {
362    value.as_ref().and_then(|item| {
363        let normalized = item.trim();
364        if normalized.is_empty() {
365            None
366        } else {
367            Some(normalized.to_string())
368        }
369    })
370}
371
372pub fn normalize_type_name(raw: &str) -> String {
373    let words = split_words(raw);
374    if words.is_empty() {
375        return "GeneratedType".to_string();
376    }
377    words
378        .into_iter()
379        .map(|word| {
380            let mut chars = word.chars();
381            match chars.next() {
382                Some(first) => {
383                    let mut normalized = String::new();
384                    normalized.extend(first.to_uppercase());
385                    normalized.push_str(chars.as_str());
386                    normalized
387                }
388                None => String::new(),
389            }
390        })
391        .collect::<Vec<_>>()
392        .join("")
393}
394
395pub fn to_upper_snake_case(raw: &str) -> String {
396    let words = split_words(raw);
397    if words.is_empty() {
398        return "GENERATED_ITEM".to_string();
399    }
400    words
401        .into_iter()
402        .map(|word| word.to_uppercase())
403        .collect::<Vec<_>>()
404        .join("_")
405}
406
407pub fn to_snake_case(raw: &str) -> String {
408    let words = split_words(raw);
409    if words.is_empty() {
410        return "generated_item".to_string();
411    }
412    let normalized = words
413        .into_iter()
414        .map(|word| word.to_lowercase())
415        .collect::<Vec<_>>()
416        .join("_");
417    if normalized
418        .chars()
419        .next()
420        .map(|character| character.is_ascii_digit())
421        .unwrap_or(false)
422    {
423        format!("generated_{normalized}")
424    } else {
425        normalized
426    }
427}
428
429fn split_words(raw: &str) -> Vec<String> {
430    let mut words = Vec::new();
431    let mut current = String::new();
432    let mut previous_is_lower_or_digit = false;
433
434    for character in raw.chars() {
435        if !character.is_ascii_alphanumeric() {
436            if !current.is_empty() {
437                words.push(current.clone());
438                current.clear();
439            }
440            previous_is_lower_or_digit = false;
441            continue;
442        }
443
444        let is_boundary = character.is_ascii_uppercase() && previous_is_lower_or_digit;
445        if is_boundary && !current.is_empty() {
446            words.push(current.clone());
447            current.clear();
448        }
449
450        current.push(character);
451        previous_is_lower_or_digit = character.is_ascii_lowercase() || character.is_ascii_digit();
452    }
453
454    if !current.is_empty() {
455        words.push(current);
456    }
457
458    words
459}