354 lines
9.4 KiB
Rust
354 lines
9.4 KiB
Rust
//! FMI 2.0 Co-Simulation C ABI.
|
|
//!
|
|
//! Exports the standard `fmi2*` symbols expected by an FMI 2.0 Co-Sim host
|
|
//! (TwinCAT, B&R, Siemens TIA, Codesys, fmpy, OpenModelica, ...). Each
|
|
//! `fmi2Component` is a heap-allocated [`crate::model::FmuInstance`] handed
|
|
//! back to the host as an opaque pointer.
|
|
//!
|
|
//! Only Co-Simulation is supported (the host drives the real-time scheduler;
|
|
//! this FMU just re-solves the steady cycle on each `fmi2DoStep`).
|
|
|
|
use std::collections::HashMap;
|
|
use std::ffi::{CStr, CString};
|
|
use std::os::raw::{c_char, c_int, c_uint, c_void};
|
|
use std::sync::Mutex;
|
|
|
|
use libc::{c_double, size_t};
|
|
|
|
use crate::embedded;
|
|
use crate::model::{FmiStatus, FmuInstance};
|
|
|
|
pub type fmi2Real = c_double;
|
|
pub type fmi2Integer = c_int;
|
|
pub type fmi2Boolean = c_int;
|
|
pub type fmi2String = *const c_char;
|
|
pub type fmi2ValueReference = c_uint;
|
|
pub type fmi2Time = c_double;
|
|
pub type fmi2Component = *mut c_void;
|
|
pub type fmi2Status = c_int;
|
|
|
|
pub const FMI2_OK: fmi2Status = 0;
|
|
pub const FMI2_WARNING: fmi2Status = 1;
|
|
pub const FMI2_DISCARD: fmi2Status = 2;
|
|
pub const FMI2_ERROR: fmi2Status = 3;
|
|
pub const FMI2_FATAL: fmi2Status = 4;
|
|
pub const FMI2_PENDING: fmi2Status = 5;
|
|
|
|
pub const FMI2_TRUE: fmi2Boolean = 1;
|
|
pub const FMI2_FALSE: fmi2Boolean = 0;
|
|
|
|
/// FMI 2.0 callback functions. We accept the struct for ABI compatibility but
|
|
/// do not invoke the callbacks in this skeleton (the host owns memory/logging).
|
|
#[repr(C)]
|
|
pub struct fmi2CallbackFunctions {
|
|
pub logger: *mut c_void,
|
|
pub allocate_memory: *mut c_void,
|
|
pub free_memory: *mut c_void,
|
|
pub step_finished: *mut c_void,
|
|
pub intermediate_update: *mut c_void,
|
|
}
|
|
|
|
fn status_to_i32(s: FmiStatus) -> fmi2Status {
|
|
s.as_i32()
|
|
}
|
|
|
|
/// Global registry of last error messages per instance (for host diagnostics).
|
|
static LAST_ERRORS: Mutex<Option<HashMap<usize, String>>> = Mutex::new(None);
|
|
|
|
fn record_error(ptr: usize, msg: String) {
|
|
let mut guard = LAST_ERRORS.lock().unwrap();
|
|
guard.get_or_insert_with(HashMap::new).insert(ptr, msg);
|
|
}
|
|
|
|
/// FMI 2.0 types platform for Co-Simulation.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2GetTypesPlatform() -> fmi2String {
|
|
static PLATFORM: &[u8] = b"default\0";
|
|
PLATFORM.as_ptr() as *const c_char
|
|
}
|
|
|
|
/// FMI standard version string.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2GetVersion() -> fmi2String {
|
|
static VERSION: &[u8] = b"2.0\0";
|
|
VERSION.as_ptr() as *const c_char
|
|
}
|
|
|
|
/// Instantiate one FMU. The model JSON + IO-map JSON are loaded via
|
|
/// [`crate::embedded::load_model`]: embedded at build time (codegen path) or
|
|
/// from `ENTROPYK_FMU_MODEL` / `ENTROPYK_FMU_IO` env vars (dev path).
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2Instantiate(
|
|
_instance_name: fmi2String,
|
|
fmu_type: fmi2String,
|
|
_callback_functions: *const fmi2CallbackFunctions,
|
|
_visible: fmi2Boolean,
|
|
_logging: fmi2Boolean,
|
|
) -> fmi2Component {
|
|
// Skeleton supports Co-Simulation only.
|
|
if !fmu_type.is_null() {
|
|
if let Ok(t) = CStr::from_ptr(fmu_type).to_str() {
|
|
if t != "CoSimulation" {
|
|
return std::ptr::null_mut();
|
|
}
|
|
}
|
|
}
|
|
|
|
match embedded::load_model() {
|
|
Ok((model_json, io_json)) => match FmuInstance::new(&model_json, &io_json) {
|
|
Ok(instance) => Box::into_raw(Box::new(instance)) as fmi2Component,
|
|
Err(e) => {
|
|
record_error(0, e);
|
|
std::ptr::null_mut()
|
|
}
|
|
},
|
|
Err(e) => {
|
|
record_error(0, e);
|
|
std::ptr::null_mut()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Release an instance.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2FreeInstance(c: fmi2Component) {
|
|
if !c.is_null() {
|
|
unsafe { drop(Box::from_raw(c as *mut FmuInstance)) };
|
|
}
|
|
}
|
|
|
|
/// Helper to access the instance behind a component handle.
|
|
unsafe fn instance(c: fmi2Component) -> Option<&'static mut FmuInstance> {
|
|
if c.is_null() {
|
|
return None;
|
|
}
|
|
unsafe { Some(&mut *(c as *mut FmuInstance)) }
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2SetDebugLogging(
|
|
_c: fmi2Component,
|
|
_logging: fmi2Boolean,
|
|
_n: size_t,
|
|
_categories: *const c_uint,
|
|
) -> fmi2Status {
|
|
FMI2_OK
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2SetupExperiment(
|
|
_c: fmi2Component,
|
|
_tolerance_defined: fmi2Boolean,
|
|
_tolerance: fmi2Real,
|
|
_start_time: fmi2Real,
|
|
_stop_time_defined: fmi2Boolean,
|
|
_stop_time: fmi2Real,
|
|
) -> fmi2Status {
|
|
FMI2_OK
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2EnterInitializationMode(c: fmi2Component) -> fmi2Status {
|
|
match unsafe { instance(c) } {
|
|
Some(inst) => status_to_i32(inst.enter_init()),
|
|
None => FMI2_ERROR,
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2ExitInitializationMode(c: fmi2Component) -> fmi2Status {
|
|
// Outputs are already populated by enter_init; nothing more to do.
|
|
if c.is_null() {
|
|
FMI2_ERROR
|
|
} else {
|
|
FMI2_OK
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2Terminate(c: fmi2Component) -> fmi2Status {
|
|
if c.is_null() {
|
|
FMI2_ERROR
|
|
} else {
|
|
FMI2_OK
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2Reset(c: fmi2Component) -> fmi2Status {
|
|
// Re-run a cold solve from current inputs.
|
|
match unsafe { instance(c) } {
|
|
Some(inst) => status_to_i32(inst.do_step()),
|
|
None => FMI2_ERROR,
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2GetReal(
|
|
c: fmi2Component,
|
|
vr: *const fmi2ValueReference,
|
|
n: size_t,
|
|
value: *mut fmi2Real,
|
|
) -> fmi2Status {
|
|
let inst = match unsafe { instance(c) } {
|
|
Some(i) => i,
|
|
None => return FMI2_ERROR,
|
|
};
|
|
if vr.is_null() || value.is_null() {
|
|
return FMI2_ERROR;
|
|
}
|
|
let vrs = unsafe { std::slice::from_raw_parts(vr, n) };
|
|
let out = unsafe { std::slice::from_raw_parts_mut(value, n) };
|
|
let mut all_ok = true;
|
|
for (k, &v) in vrs.iter().enumerate() {
|
|
match inst.get_real(v) {
|
|
Ok(x) => out[k] = x,
|
|
Err(_) => {
|
|
out[k] = f64::NAN;
|
|
all_ok = false;
|
|
}
|
|
}
|
|
}
|
|
if all_ok {
|
|
FMI2_OK
|
|
} else {
|
|
FMI2_WARNING
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2SetReal(
|
|
c: fmi2Component,
|
|
vr: *const fmi2ValueReference,
|
|
n: size_t,
|
|
value: *const fmi2Real,
|
|
) -> fmi2Status {
|
|
let inst = match unsafe { instance(c) } {
|
|
Some(i) => i,
|
|
None => return FMI2_ERROR,
|
|
};
|
|
if vr.is_null() || value.is_null() {
|
|
return FMI2_ERROR;
|
|
}
|
|
let vrs = unsafe { std::slice::from_raw_parts(vr, n) };
|
|
let vals = unsafe { std::slice::from_raw_parts(value, n) };
|
|
let mut all_ok = true;
|
|
for (k, &v) in vrs.iter().enumerate() {
|
|
if status_to_i32(inst.set_real(v, vals[k])) != FMI2_OK {
|
|
all_ok = false;
|
|
}
|
|
}
|
|
if all_ok {
|
|
FMI2_OK
|
|
} else {
|
|
FMI2_ERROR
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2GetInteger(
|
|
_c: fmi2Component,
|
|
_vr: *const fmi2ValueReference,
|
|
_n: size_t,
|
|
_value: *mut fmi2Integer,
|
|
) -> fmi2Status {
|
|
FMI2_ERROR
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2SetInteger(
|
|
_c: fmi2Component,
|
|
_vr: *const fmi2ValueReference,
|
|
_n: size_t,
|
|
_value: *const fmi2Integer,
|
|
) -> fmi2Status {
|
|
FMI2_ERROR
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2GetBoolean(
|
|
_c: fmi2Component,
|
|
_vr: *const fmi2ValueReference,
|
|
_n: size_t,
|
|
_value: *mut fmi2Boolean,
|
|
) -> fmi2Status {
|
|
FMI2_ERROR
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2SetBoolean(
|
|
_c: fmi2Component,
|
|
_vr: *const fmi2ValueReference,
|
|
_n: size_t,
|
|
_value: *const fmi2Boolean,
|
|
) -> fmi2Status {
|
|
FMI2_ERROR
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2GetString(
|
|
_c: fmi2Component,
|
|
_vr: *const fmi2ValueReference,
|
|
_n: size_t,
|
|
_value: *mut fmi2String,
|
|
) -> fmi2Status {
|
|
FMI2_ERROR
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2SetString(
|
|
_c: fmi2Component,
|
|
_vr: *const fmi2ValueReference,
|
|
_n: size_t,
|
|
_value: *const fmi2String,
|
|
) -> fmi2Status {
|
|
FMI2_ERROR
|
|
}
|
|
|
|
/// Advance the model by one communication step. Entropyk is steady-state, so
|
|
/// `currentCommunicationPoint` and `communicationStepSize` are accepted but
|
|
/// ignored: each step re-solves the cycle at the current inputs (quasi-steady
|
|
/// co-simulation).
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2DoStep(
|
|
c: fmi2Component,
|
|
_current_communication_point: fmi2Real,
|
|
_communication_step_size: fmi2Real,
|
|
_no_set_fmu_state_prior_to_current_point: fmi2Boolean,
|
|
) -> fmi2Status {
|
|
match unsafe { instance(c) } {
|
|
Some(inst) => status_to_i32(inst.do_step()),
|
|
None => FMI2_ERROR,
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2CancelStep(_c: fmi2Component) -> fmi2Status {
|
|
FMI2_ERROR
|
|
}
|
|
|
|
/// Non-standard helper for the host to retrieve the last error message as a C
|
|
/// string (null-terminated). Returns null if no error. Caller must NOT free.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn fmi2GetLastError() -> fmi2String {
|
|
static mut BUF: Option<CString> = None;
|
|
let msg = LAST_ERRORS
|
|
.lock()
|
|
.unwrap()
|
|
.as_ref()
|
|
.and_then(|m| m.values().last().cloned());
|
|
match msg {
|
|
Some(s) => {
|
|
// Rebuild the static buffer each call (single-threaded host query).
|
|
let c = CString::new(s).unwrap_or_default();
|
|
let ptr = c.as_ptr();
|
|
unsafe { BUF = Some(c) };
|
|
// Keep the CString alive for the caller's read by leaking the old
|
|
// value only after the pointer is handed out. The static retains it.
|
|
ptr
|
|
}
|
|
None => std::ptr::null(),
|
|
}
|
|
}
|
|
|