提交 95eb55b6 authored 作者: Serhij S's avatar Serhij S

added missing docs

上级 2bb1cb92
......@@ -9,8 +9,10 @@ use std::{
use crate::Result;
pub mod serial; // Serial communications
pub mod tcp; // TCP communications
/// Serial communications
pub mod serial;
/// TCP communications
pub mod tcp;
/// A versatile (TCP/serial) client
#[derive(Clone)]
......@@ -83,12 +85,14 @@ impl Write for Client {
}
}
/// A guard for the session lock
pub struct SessionGuard {
client: Client,
session_id: usize,
}
impl SessionGuard {
/// Get the session id
pub fn session_id(&self) -> usize {
self.session_id
}
......@@ -100,11 +104,15 @@ impl Drop for SessionGuard {
}
}
/// Communication protocol
pub enum Protocol {
/// TCP
Tcp,
/// Serial
Serial,
}
/// Stream trait
pub trait Stream: Read + Write + Send {}
trait Communicator {
......@@ -122,12 +130,14 @@ trait Communicator {
fn unlock_session(&self);
}
/// A communication reader container (used to pass the reader via policy channels)
#[allow(clippy::module_name_repetitions)]
pub struct CommReader {
reader: Option<Box<dyn Read + Send + 'static>>,
}
impl CommReader {
/// Take reader from the container
pub fn take(&mut self) -> Option<Box<dyn Read + Send + 'static>> {
self.reader.take()
}
......@@ -137,10 +147,14 @@ impl DataDeliveryPolicy for CommReader {}
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(1);
/// Timeouts
#[derive(Clone)]
pub struct Timeouts {
/// Connect timeout
pub connect: Duration,
/// Read timeout
pub read: Duration,
/// Write timeout
pub write: Duration,
}
......@@ -151,6 +165,7 @@ impl Default for Timeouts {
}
impl Timeouts {
/// Create new timeouts with the default value
pub fn new(default: Duration) -> Self {
Self {
connect: default,
......@@ -158,6 +173,7 @@ impl Timeouts {
write: default,
}
}
/// Create new timeouts with zero values
pub fn none() -> Self {
Self {
connect: Duration::from_secs(0),
......@@ -167,6 +183,7 @@ impl Timeouts {
}
}
/// Connection handler object, used to perform initial chat in custom protocols
pub trait ConnectionHandler {
/// called right after the connection is established
fn on_connect(
......
......@@ -24,12 +24,18 @@ pub fn connect(path: &str, timeout: Duration, frame_delay: Duration) -> Result<C
Ok(Client(Serial::create(path, timeout, frame_delay)?))
}
/// Serial port parameters
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Parameters {
/// Serial port device path
pub port_dev: String,
/// Baud rate
pub baud_rate: serial::BaudRate,
/// Character size
pub char_size: serial::CharSize,
/// Parity
pub parity: serial::Parity,
/// Stop bits
pub stop_bits: serial::StopBits,
}
......@@ -111,6 +117,7 @@ fn parse_path(path: &str) -> Result<Parameters> {
})
}
/// Open a serial port
pub fn open(params: &Parameters, timeout: Duration) -> Result<SystemPort> {
let mut port = serial::open(&params.port_dev).map_err(Error::io)?;
port.reconfigure(&|settings| {
......@@ -128,6 +135,7 @@ pub fn open(params: &Parameters, timeout: Duration) -> Result<SystemPort> {
Ok(port)
}
/// Serial port client
#[allow(clippy::module_name_repetitions)]
pub struct Serial {
port: Mutex<SPort>,
......@@ -145,6 +153,7 @@ struct SPort {
last_frame: Option<Instant>,
}
/// Serial port client type
#[allow(clippy::module_name_repetitions)]
pub type SerialClient = Arc<Serial>;
......@@ -218,6 +227,7 @@ impl Communicator for Serial {
}
impl Serial {
/// Create a new serial client
pub fn create(path: &str, timeout: Duration, frame_delay: Duration) -> Result<Arc<Self>> {
let params = parse_path(path)?;
Ok(Self {
......
......@@ -38,6 +38,7 @@ pub fn connect_with_options<A: ToSocketAddrs + fmt::Debug>(
impl Stream for TcpStream {}
/// A TCP client structure
#[allow(clippy::module_name_repetitions)]
pub struct Tcp {
addr: SocketAddr,
......@@ -50,6 +51,7 @@ pub struct Tcp {
connection_handler: Option<Box<dyn ConnectionHandler + Send + Sync>>,
}
/// A TCP client type
#[allow(clippy::module_name_repetitions)]
pub type TcpClient = Arc<Tcp>;
......
......@@ -23,6 +23,7 @@ use signal_hook::{
};
use tracing::error;
/// Controller prelude
pub mod prelude {
pub use super::{Context, Controller, WResult, Worker, WorkerOptions};
pub use roboplc_derive::WorkerOpts;
......@@ -31,6 +32,7 @@ pub mod prelude {
/// Result type, which must be returned by workers' `run` method
pub type WResult = std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>;
/// Sleep step (used in blocking)
pub const SLEEP_STEP: Duration = Duration::from_millis(100);
/// Controller state beacon. Can be cloned and shared with no limitations.
......@@ -40,7 +42,7 @@ pub struct State {
}
impl State {
pub fn new() -> Self {
fn new() -> Self {
Self {
state: AtomicI8::new(ControllerStateKind::Starting as i8).into(),
}
......@@ -71,11 +73,17 @@ impl Default for State {
#[allow(clippy::module_name_repetitions)]
pub enum ControllerStateKind {
#[default]
/// The controller is starting
Starting = 0,
/// The controller is active (accepting tasks)
Active = 1,
/// The controller is running (tasks are being executed)
Running = 2,
/// The controller is stopping
Stopping = -1,
/// The controller is stopped
Stopped = -100,
/// The controller state is unknown
Unknown = -128,
}
......
......@@ -10,6 +10,7 @@ use self::prelude::DataChannel;
type ConditionFunction<T> = Box<dyn Fn(&T) -> bool + Send + Sync>;
/// The hub prelude
pub mod prelude {
pub use super::Hub;
pub use crate::event_matches;
......@@ -17,8 +18,10 @@ pub mod prelude {
pub use rtsc::DataChannel;
}
/// The default priority for the client channel
pub const DEFAULT_PRIORITY: usize = 100;
/// The default client channel capacity
pub const DEFAULT_CHANNEL_CAPACITY: usize = 1024;
/// Sync data communcation hub to implement in-process pub/sub model for thread workers
......@@ -43,6 +46,7 @@ impl<T: DataDeliveryPolicy + Clone> Default for Hub<T> {
}
impl<T: DataDeliveryPolicy + Clone> Hub<T> {
/// Creates a new hub with default settings
pub fn new() -> Self {
Self::default()
}
......@@ -238,6 +242,7 @@ where
}
}
/// A client for the hub
pub struct Client<T: DataDeliveryPolicy + Clone> {
name: Arc<str>,
hub: Hub<T>,
......@@ -285,6 +290,7 @@ impl<T: DataDeliveryPolicy + Clone> Drop for Client<T> {
}
}
/// Client options
pub struct ClientOptions<T: DataDeliveryPolicy + Clone> {
name: Arc<str>,
priority: usize,
......@@ -294,6 +300,7 @@ pub struct ClientOptions<T: DataDeliveryPolicy + Clone> {
}
impl<T: DataDeliveryPolicy + Clone> ClientOptions<T> {
/// Creates a new client options object
pub fn new<F>(name: &str, condition: F) -> Self
where
F: Fn(&T) -> bool + Send + Sync + 'static,
......
......@@ -8,8 +8,10 @@ use crate::{DataDeliveryPolicy, Error, Result};
type ConditionFunction<T> = Box<dyn Fn(&T) -> bool + Send + Sync>;
/// The default priority for the client channel
pub const DEFAULT_PRIORITY: usize = 100;
/// The default client channel capacity
pub const DEFAULT_CHANNEL_CAPACITY: usize = 1024;
/// Async data communcation hub to implement in-process pub/sub model for thread workers
......@@ -34,6 +36,7 @@ impl<T: DataDeliveryPolicy + Clone> Default for Hub<T> {
}
impl<T: DataDeliveryPolicy + Clone> Hub<T> {
/// Creates a new hub instance
pub fn new() -> Self {
Self::default()
}
......@@ -191,6 +194,7 @@ where
}
}
/// A client for the hub
pub struct Client<T: DataDeliveryPolicy + Clone> {
name: Arc<str>,
hub: Hub<T>,
......@@ -232,6 +236,7 @@ impl<T: DataDeliveryPolicy + Clone> Drop for Client<T> {
}
}
/// Client options
pub struct ClientOptions<T: DataDeliveryPolicy + Clone> {
name: Arc<str>,
priority: usize,
......@@ -241,6 +246,7 @@ pub struct ClientOptions<T: DataDeliveryPolicy + Clone> {
}
impl<T: DataDeliveryPolicy + Clone> ClientOptions<T> {
/// Creates a new client options object
pub fn new<F>(name: &str, condition: F) -> Self
where
F: Fn(&T) -> bool + Send + Sync + 'static,
......
......@@ -103,6 +103,7 @@ where
}
config
}
/// Creates a new EAPI connection configuration with the given path
pub fn new(path: &str) -> Self {
Self {
path: path.to_owned(),
......@@ -140,10 +141,12 @@ where
self.reconnect_delay = reconnect_delay;
self
}
/// Set action handler for the given OID
pub fn action_handler(mut self, oid: OID, handler: ActionHandlerFn<D, V>) -> Self {
self.action_handlers.insert(oid, handler);
self
}
/// Set bulk action handler for the given OID mask
pub fn bulk_action_handler(mut self, mask: OIDMask, handler: ActionHandlerFn<D, V>) -> Self {
self.bulk_action_handlers.push((mask, handler));
self
......@@ -467,6 +470,7 @@ where
warn!(client = self.inner.name, "disconnected from EAPI bus");
Ok(())
}
/// Pushes a data object to the EVA ICS node core
pub fn dobj_push<T>(&self, name: Arc<String>, value: T) -> Result<()>
where
T: for<'a> BinWrite<Args<'a> = ()>,
......@@ -481,12 +485,14 @@ where
})
.map_err(Into::into)
}
/// Pushes a data object error to the EVA ICS node core
pub fn dobj_error(&self, name: Arc<String>) -> Result<()> {
self.inner
.tx
.try_send(PushPayload::DObjError(name))
.map_err(Into::into)
}
/// Pushes a state event to the EVA ICS node core
pub fn state_push<T: Serialize>(&self, oid: Arc<OID>, value: T) -> Result<()> {
self.inner
.tx
......@@ -496,12 +502,14 @@ where
})
.map_err(Into::into)
}
/// Pushes a custom (raw) state event to the EVA ICS node core
pub fn raw_state_push(&self, oid: Arc<OID>, event: RawStateEventOwned) -> Result<()> {
self.inner
.tx
.try_send(PushPayload::State { oid, event })
.map_err(Into::into)
}
/// Pushes a state error event to the EVA ICS node core
pub fn state_error(&self, oid: Arc<OID>) -> Result<()> {
self.inner
.tx
......
......@@ -19,17 +19,22 @@ pub mod pipe;
/// Raw UDP communication
pub mod raw_udp;
/// Generic I/O mapping trait
#[allow(clippy::module_name_repetitions)]
pub trait IoMapping {
/// Options for the mapping
type Options;
/// Read data from the raw buffer
fn read<T>(&mut self) -> Result<T>
where
T: for<'a> BinRead<Args<'a> = ()>;
/// Write data to the raw buffer
fn write<T>(&mut self, value: T) -> Result<()>
where
T: for<'a> BinWrite<Args<'a> = ()>;
}
/// I/O mapping prelude
pub mod prelude {
pub use super::IoMapping as _;
pub use binrw::prelude::*;
......
......@@ -26,6 +26,7 @@ use super::IoMapping;
mod regs;
mod server;
/// Modbus prelude
pub mod prelude {
pub use super::{
ModbusMapping, ModbusMappingOptions, ModbusRegister, ModbusRegisterKind, ModbusServer,
......@@ -35,6 +36,7 @@ pub mod prelude {
/// Swaps endianess of floating point numbers in case of non-standard IEEE 754 layout.
pub trait SwapModbusEndianess {
/// Swaps endianess of floating point numbers in case of non-standard IEEE 754 layout.
fn to_swapped_modbus_endianness(&self) -> Self;
}
......@@ -69,9 +71,11 @@ pub struct ModbusMappingOptions {
}
impl ModbusMappingOptions {
/// Creates new options for Modbus value mapping
pub fn new() -> Self {
Self { bulk_write: true }
}
/// Enables or disables bulk writes
pub fn bulk_write(mut self, value: bool) -> Self {
self.bulk_write = value;
self
......@@ -99,6 +103,7 @@ pub struct ModbusMapping {
}
impl ModbusMapping {
/// Creates new Modbus value mapping
pub fn create<R>(client: &Client, unit_id: u8, register: R, count: u16) -> Result<Self>
where
R: TryInto<ModbusRegister>,
......@@ -117,6 +122,7 @@ impl ModbusMapping {
options: <_>::default(),
})
}
/// Sets options for Modbus value mapping
pub fn with_options(mut self, options: ModbusMappingOptions) -> Self {
self.options = options;
self
......
......@@ -5,20 +5,27 @@ use crate::{Error, Result};
/// A Modbus register kind.
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub enum Kind {
/// Coil register (boolean)
Coil,
/// Discrete register (boolean)
Discrete,
/// Input register (16-bit)
Input,
/// Holding register (16-bit)
Holding,
}
/// A Modbus register type, contains the kind and the offset.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct Register {
/// The register kind.
pub kind: Kind,
/// The register offset.
pub offset: u16,
}
impl Register {
/// Creates a new register.
pub fn new(kind: Kind, offset: u16) -> Self {
Self { kind, offset }
}
......
......@@ -88,8 +88,10 @@ fn handle_client<
Ok(())
}
/// Function to block certain context storage
pub type AllowFn = fn(ModbusRegisterKind, std::ops::Range<u16>) -> WritePermission;
/// Context storage write permission
pub enum WritePermission {
/// Write is allowed.
Allow,
......@@ -126,6 +128,7 @@ pub struct ModbusServer<const C: usize, const D: usize, const I: usize, const H:
allow_external_write_fn: Arc<AllowFn>,
}
impl<const C: usize, const D: usize, const I: usize, const H: usize> ModbusServer<C, D, I, H> {
/// Creates new Modbus server
pub fn bind(
protocol: Protocol,
unit: u8,
......@@ -152,6 +155,7 @@ impl<const C: usize, const D: usize, const I: usize, const H: usize> ModbusServe
pub fn set_allow_external_write_fn(&mut self, f: AllowFn) {
self.allow_external_write_fn = f.into();
}
/// Creates a new mapping for the server storage context.
pub fn mapping(&self, register: ModbusRegister, count: u16) -> ModbusServerMapping<C, D, I, H> {
let buf_capacity = match register.kind {
ModbusRegisterKind::Coil | ModbusRegisterKind::Discrete => usize::from(count),
......@@ -164,9 +168,11 @@ impl<const C: usize, const D: usize, const I: usize, const H: usize> ModbusServe
data_buf: Vec::with_capacity(buf_capacity),
}
}
/// Returns a reference to the internal storage.
pub fn storage(&self) -> Arc<Mutex<ModbusStorage<C, D, I, H>>> {
self.storage.clone()
}
/// Runs the server. This function blocks the current thread.
pub fn serve(&mut self) -> Result<()> {
let timeout = self.timeout;
let unit = self.unit;
......
//! Data processing with subprocesses
use std::{
collections::BTreeMap,
ffi::{OsStr, OsString},
......@@ -17,16 +18,19 @@ use crate::{
DataDeliveryPolicy, Result,
};
/// Pipe reader
pub struct Reader {
rx: Receiver<String>,
}
impl Reader {
/// Reads a line from the pipe. Blocks until a line is available.
pub fn line(&self) -> Result<String> {
self.rx.recv_blocking().map_err(Into::into)
}
}
/// Data pipe with a subprocess
pub struct Pipe {
program: OsString,
args: Vec<OsString>,
......@@ -37,6 +41,7 @@ pub struct Pipe {
}
impl Pipe {
/// Creates a new pipe with a subprocess
pub fn new<P: AsRef<OsStr>>(program: P) -> (Self, Reader) {
let (tx, rx) = pchannel_async::bounded(10);
(
......@@ -51,19 +56,23 @@ impl Pipe {
Reader { rx },
)
}
/// Adds a command line argument
pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
self.args.push(arg.as_ref().to_owned());
self
}
/// Adds multiple command line arguments
pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> &mut Self {
self.args
.extend(args.into_iter().map(|x| x.as_ref().to_owned()));
self
}
/// Adds an environment variable
pub fn env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.environment.insert(key.into(), value.into());
self
}
/// Adds multiple environment variables
pub fn envs(
&mut self,
envs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
......@@ -77,6 +86,7 @@ impl Pipe {
self.input_data = Some(data.into());
self
}
/// Delay before restarting the subprocess after it terminates
pub fn restart_delay(&mut self, delay: Duration) -> &mut Self {
self.restart_delay = delay;
self
......
......@@ -26,6 +26,7 @@ impl<T> UdpReceiver<T>
where
T: for<'a> BinRead<Args<'a> = ()>,
{
/// Binds to the specified address and creates a new receiver
pub fn bind<A: ToSocketAddrs>(addr: A, buf_size: usize) -> Result<Self> {
let server = UdpSocket::bind(addr)?;
Ok(Self {
......@@ -70,6 +71,7 @@ impl<T> UdpSender<T>
where
T: for<'a> BinWrite<Args<'a> = ()>,
{
/// Connects to the specified address and creates a new sender
pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Self> {
let socket = UdpSocket::bind(("0.0.0.0", 0))?;
let target = addr
......@@ -84,6 +86,7 @@ where
})
}
/// Sends a value to the target address
pub fn send(&mut self, value: T) -> Result<()> {
let mut buf = Cursor::new(&mut self.data_buf);
value.write_le(&mut buf)?;
......
#![ doc = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/", "README.md" ) ) ]
#![deny(missing_docs)]
use core::{fmt, num};
use std::io::Write;
use std::panic::PanicInfo;
......@@ -41,6 +42,7 @@ pub mod supervisor;
#[cfg(target_os = "linux")]
pub mod thread_rt;
/// The crate result type
pub type Result<T> = std::result::Result<T, Error>;
/// The crate error type
......@@ -71,7 +73,7 @@ pub enum Error {
/// Standard I/O errors
#[error("I/O error: {0}")]
IO(#[from] std::io::Error),
// Non-standard I/O errors
/// Non-standard I/O errors
#[error("Communication error: {0}")]
Comm(String),
/// 3rd party API errors
......@@ -160,15 +162,19 @@ impl_error!(num::ParseFloatError, InvalidData);
impl_error!(binrw::Error, BinRw);
impl Error {
/// Returns true if the data is skipped
pub fn is_data_skipped(&self) -> bool {
matches!(self, Error::ChannelSkipped)
}
/// Creates new invalid data error
pub fn invalid_data<S: fmt::Display>(msg: S) -> Self {
Error::InvalidData(msg.to_string())
}
/// Creates new I/O error (for non-standard I/O)
pub fn io<S: fmt::Display>(msg: S) -> Self {
Error::Comm(msg.to_string())
}
/// Creates new function failed error
pub fn failed<S: fmt::Display>(msg: S) -> Self {
Error::Failed(msg.to_string())
}
......@@ -266,6 +272,7 @@ pub fn configure_logger(filter: LevelFilter) {
builder.init();
}
/// Prelude module
pub mod prelude {
#[cfg(target_os = "linux")]
pub use super::suicide;
......
......@@ -7,6 +7,7 @@ use crate::thread_rt::{Builder, ScopedTask, Task};
use crate::time::Interval;
use crate::{Error, Result};
/// The supervisor prelude
pub mod prelude {
pub use super::Supervisor;
pub use crate::thread_rt::{Builder, Scheduling};
......@@ -35,6 +36,7 @@ macro_rules! vacant_entry {
}
impl<T> Supervisor<T> {
/// Creates a new supervisor object
pub fn new() -> Self {
Self::default()
}
......@@ -101,6 +103,7 @@ impl<T> Supervisor<T> {
}
}
/// A scoped supervisor object
#[allow(clippy::module_name_repetitions)]
#[derive(Serialize)]
pub struct ScopedSupervisor<'a, 'env: 'a, T> {
......@@ -110,6 +113,7 @@ pub struct ScopedSupervisor<'a, 'env: 'a, T> {
}
impl<'a, 'env, T> ScopedSupervisor<'a, 'env, T> {
/// Creates a new scoped supervisor object
pub fn new(scope: &'a thread::Scope<'a, 'env>) -> Self {
Self {
tasks: <_>::default(),
......
......@@ -85,12 +85,18 @@ pub struct Builder {
#[serde(rename_all = "UPPERCASE")]
pub enum Scheduling {
#[serde(rename = "RR")]
/// Round-robin
RoundRobin,
/// First in, first out
FIFO,
/// Idle
Idle,
/// Batch
Batch,
/// Deadline
DeadLine,
#[default]
/// Other
Other,
}
......@@ -134,6 +140,7 @@ impl_builder_from!(&str);
impl_builder_from!(String);
impl Builder {
/// Creates a new thread builder
pub fn new() -> Self {
Self::default()
}
......@@ -316,9 +323,11 @@ pub struct Task<T> {
}
impl<T> Task<T> {
/// Returns the task name
pub fn name(&self) -> &str {
&self.name
}
/// Returns the task handle
pub fn handle(&self) -> &JoinHandle<T> {
&self.handle
}
......@@ -335,12 +344,15 @@ impl<T> Task<T> {
self.rt_params = rt_params;
Ok(())
}
/// Returns true if the task is finished
pub fn is_finished(&self) -> bool {
self.handle.is_finished()
}
/// Joins the task
pub fn join(self) -> thread::Result<T> {
self.handle.join()
}
/// Converts the task into a standard [`JoinHandle`]
pub fn into_join_handle(self) -> JoinHandle<T> {
self.into()
}
......@@ -348,6 +360,7 @@ impl<T> Task<T> {
pub fn elapsed(&self) -> Duration {
self.info.started_mt.elapsed()
}
/// Returns true if the task is blocking
pub fn is_blocking(&self) -> bool {
self.blocking
}
......@@ -377,9 +390,11 @@ pub struct ScopedTask<'scope, T> {
}
impl<'scope, T> ScopedTask<'scope, T> {
/// Returns the task name
pub fn name(&self) -> &str {
&self.name
}
/// Returns the task handle
pub fn handle(&self) -> &ScopedJoinHandle<T> {
&self.handle
}
......@@ -396,12 +411,15 @@ impl<'scope, T> ScopedTask<'scope, T> {
self.rt_params = rt_params;
Ok(())
}
/// Returns true if the task is finished
pub fn is_finished(&self) -> bool {
self.handle.is_finished()
}
/// Joins the task
pub fn join(self) -> thread::Result<T> {
self.handle.join()
}
/// Converts the task into a standard [`ScopedJoinHandle`]
pub fn into_join_handle(self) -> ScopedJoinHandle<'scope, T> {
self.into()
}
......@@ -409,6 +427,7 @@ impl<'scope, T> ScopedTask<'scope, T> {
pub fn elapsed(&self) -> Duration {
self.info.started_mt.elapsed()
}
/// Returns true if the task is blocking
pub fn is_blocking(&self) -> bool {
self.blocking
}
......@@ -429,6 +448,7 @@ pub struct RTParams {
}
impl RTParams {
/// Creates a new real-time parameters object
pub fn new() -> Self {
Self::default()
}
......@@ -659,6 +679,7 @@ pub struct SystemConfig {
}
impl SystemConfig {
/// Creates a new system config object
pub fn new() -> Self {
Self::default()
}
......@@ -681,6 +702,7 @@ impl SystemConfig {
}
}
/// A guard object to restore system parameters when dropped
pub struct SystemConfigGuard {
config: SystemConfig,
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论