eth: Poll and Handle Json based Ld ctrl cmd

- Upgrade to miniconf 9.0
- Only non report related laser diode ctrl command is implemented
This commit is contained in:
linuswck 2024-02-02 14:07:25 +08:00
parent 3d3d6f5cb5
commit 6096711d2c
13 changed files with 350 additions and 83 deletions

74
Cargo.lock generated
View File

@ -179,7 +179,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -229,10 +229,16 @@ dependencies = [
]
[[package]]
name = "embedded-nal"
version = "0.6.0"
name = "embedded-io"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db9efecb57ab54fa918730f2874d7d37647169c50fa1357fecb81abee840b113"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "embedded-nal"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447416d161ba378782c13e82b11b267d6d2104b4913679a7c5640e7e94f96ea7"
dependencies = [
"heapless 0.7.17",
"nb 1.1.0",
@ -361,6 +367,12 @@ dependencies = [
"bitflags",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "kirdy"
version = "0.0.0"
@ -420,11 +432,13 @@ checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
[[package]]
name = "miniconf"
version = "0.6.3"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07df30ef660fa32d54836743fbdf8208747a559fc4296ec07dc12b6df65343b2"
checksum = "9df2d7bdba3acb28460c347b21e1e88d869f2716ebe060eb6a79f7b76b57de72"
dependencies = [
"embedded-io",
"heapless 0.7.17",
"itoa",
"log",
"miniconf_derive",
"minimq",
@ -435,20 +449,20 @@ dependencies = [
[[package]]
name = "miniconf_derive"
version = "0.6.2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e142f4961afed7b940e84903e6f886b35d10017f15aafc960556f053f6a8fa35"
checksum = "89f46d25f40e41f552d76b8eb9e225fe493ebf978a5c3f42b7599e45cfe6b4e3"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.48",
]
[[package]]
name = "minimq"
version = "0.6.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b222dbc2667cc69a2d9acdf5c24280c49233295eda3a767f6ddfb4040f5cc80c"
checksum = "b561c2c86a3509f7c514f546fb24755753a30fdcf67cce8d5f2f38688483cd31"
dependencies = [
"bit_field",
"embedded-nal",
@ -477,9 +491,9 @@ checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
[[package]]
name = "no-std-net"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bcece43b12349917e096cddfa66107277f123e6c96a5aea78711dc601a47152"
checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
[[package]]
name = "num"
@ -547,22 +561,22 @@ dependencies = [
[[package]]
name = "num_enum"
version = "0.5.11"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.11"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.48",
]
[[package]]
@ -620,18 +634,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.70"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@ -705,9 +719,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.193"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
@ -725,13 +739,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.193"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -859,9 +873,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.41"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
@ -897,7 +911,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]

View File

@ -32,7 +32,7 @@ usb-device = "0.2.9"
usbd-serial = "0.1.1"
fugit = "0.3.6"
rtt-target = { version = "0.3.1", features = ["cortex-m"] }
miniconf = "0.6.3"
miniconf = "0.9.0"
serde = { version = "1.0.158", features = ["derive"], default-features = false }
sfkv = "0.1"
bit_field = "0.10"

View File

@ -1,14 +0,0 @@
# echo-client.py
import socket
#192, 168, 1, 132
HOST = "192.168.1.132" # The server's hostname or IP address
PORT = 1337 # The port used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(b"Hello, world")
data = s.recv(1024)
print(f"Received {data!r}")

24
eth_cmd_test.py Normal file
View File

@ -0,0 +1,24 @@
# Python Test Scripts for Controlling Kirdy
# Kirdy is written to be controlled via a json object based on miniconf rust crate
# Json Field:
# "rev": hw_rev
# "laser_diode_cmd": Check cmd_handler.rs for the cmd Enum to control the laser diode
# "data_f32": Optional f32 Data field depending on cmd
# "data_f64": Optional f64 Data field depending on cmd
import socket
import json
# Kirdy IP and Port Number
HOST = "192.168.1.132"
PORT = 1337
ld_cmd = {
"rev": 3,
"laser_diode_cmd": "SetI",
"data_f64": 0.0,
}
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.send(bytes(json.dumps(ld_cmd), "UTF-8"))

View File

@ -1,4 +1,4 @@
use miniconf::Miniconf;
use miniconf::Tree;
use stm32f4xx_hal::pac::ADC2;
use uom::si::electric_current::ampere;
use crate::laser_diode::ld_ctrl::LdCtrl;
@ -49,11 +49,11 @@ impl Settings{
const LD_CURRENT_TIME_STEP_MS: u32 = 1;
}
#[derive(Clone, Debug, Miniconf)]
#[derive(Clone, Debug, Tree)]
pub struct Settings {
ld_drive_current: ElectricCurrent,
ld_drive_current_limit: ElectricCurrent,
#[miniconf(defer)]
#[tree]
pd_responsitivity: pd_responsitivity::Parameters,
}

View File

@ -7,12 +7,12 @@ use uom::si::{
electric_current::microampere,
};
use uom::{si::{ISQ, SI, Quantity}, typenum::*};
use miniconf::Miniconf;
use miniconf::Tree;
// Ampere / Watt
pub type ResponsitivityUnit = Quantity<ISQ<N2, N1, P3, P1, Z0, Z0, Z0>, SI<f64>, f64>;
#[derive(Clone, Debug, PartialEq, Miniconf)]
#[derive(Clone, Debug, PartialEq, Tree)]
pub struct Parameters {
pub responsitivity: ResponsitivityUnit,
pub i_dark: ElectricCurrent,

View File

@ -2,7 +2,7 @@
#![cfg_attr(not(test), no_std)]
use cortex_m_rt::entry;
use log::info;
use log::{info, debug};
use stm32f4xx_hal::pac::{CorePeripherals, Peripherals};
mod device;
mod laser_diode;
@ -16,6 +16,7 @@ use uom::si::electric_potential::volt;
use uom::si::electric_current::{ampere, milliampere};
use uom::si::power::milliwatt;
use uom::si::f64::{ElectricPotential, ElectricCurrent, Power};
use serde::{Serialize, Deserialize};
// If RTT is used, print panic info through RTT
#[cfg(all(feature = "RTT", not(test)))]
@ -30,17 +31,24 @@ fn panic(info: &PanicInfo) -> ! {
#[cfg(all(not(feature = "RTT"), not(test)))]
use panic_halt as _;
use miniconf::{Error, JsonCoreSlash, Tree, TreeKey};
static mut ETH_DATA_BUFFER: [u8; 1024] = [0; 1024];
#[cfg(not(test))]
#[entry]
fn main() -> ! {
log_setup::init_log();
info!("Kirdy init");
let core_perif = CorePeripherals::take().unwrap();
let perif = Peripherals::take().unwrap();
let (mut wd, mut flash_store, mut laser, mut thermostat,) = bootup(core_perif, perif);
let (mut wd, mut _flash_store, mut laser, mut thermostat,) = bootup(core_perif, perif);
// Demo Fns for reading and writing stm32 Internal Flash
/*
let key = "test";
info!("Read the Flash Content Stored");
match flash_store.read(key).unwrap() {
@ -64,6 +72,7 @@ fn main() -> ! {
Some(val) => {info!("Val: {:?}", val)}
_ => {info!("Key does not match")}
};
*/
// https://github.com/iliekturtles/uom/blob/master/examples/si.rs
let volt_fmt = ElectricPotential::format_args(volt, Abbreviation);
@ -86,6 +95,28 @@ fn main() -> ! {
info!("power_excursion: {:?}", laser.pd_mon_status().pwr_excursion);
info!("Termination Status: {:?}", laser.get_term_status());
let mut eth_is_pending = false;
if net::net::eth_is_socket_active() {
cortex_m::interrupt::free(|cs|
{
eth_is_pending = net::net::is_pending(cs);
}
);
if eth_is_pending {
unsafe{
cortex_m::interrupt::free(|cs| {
net::net::clear_pending(cs);
});
let bytes = net::net::eth_recv(&mut ETH_DATA_BUFFER);
debug!("Number of bytes recv: {:?}", bytes);
laser = net::cmd_handler::execute_cmd(&mut ETH_DATA_BUFFER, bytes, laser);
}
}
}
sys_timer::sleep(500);
}
}

125
src/net/cmd_handler.rs Normal file
View File

@ -0,0 +1,125 @@
use core::fmt::Debug;
use miniconf::{Error, JsonCoreSlash, Tree, TreeKey};
use serde::{Deserialize, Serialize};
use uom::si::electric_current::{milliampere, ElectricCurrent};
use crate::laser_diode::laser_diode::LdDrive;
use log::info;
#[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)]
enum LdCmdEnum {
#[default]
Reserved,
// LD Drive Related
PowerUp,
PowerDown,
LdTermsShort,
LdTermsOpen,
SetI,
SetISoftLimit,
// PD Mon Related
SetPdResponsitivity,
SetPdDarkCurrent,
SetPdILimit,
SetLdPwrLimit,
ClearAlarmStatus,
// Report Related
GetModInTermStatus,
GetLdStatus,
GetAlramStatus,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct CmdJsonObj{
rev: u8,
laser_diode_cmd: LdCmdEnum,
data_f32: Option<f32>,
data_f64: Option<f64>,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct Cmd {
json: CmdJsonObj
}
pub fn execute_cmd(buffer: &mut [u8], buffer_size: usize, mut laser: LdDrive)->(LdDrive){
let mut cmd = Cmd {
json: CmdJsonObj::default()
};
match cmd.set_json("/json", &buffer[0..buffer_size]){
Ok(_) => {
info!("############ Command Received {:?}", cmd.json.laser_diode_cmd);
match cmd.json.laser_diode_cmd {
LdCmdEnum::PowerUp => {
laser.power_up()
}
LdCmdEnum::PowerDown => {
laser.power_down()
}
LdCmdEnum::LdTermsShort => {
laser.ld_short();
}
LdCmdEnum::LdTermsOpen => {
laser.ld_open();
}
LdCmdEnum::SetI => {
match cmd.json.data_f64 {
Some(val) => {
laser.ld_set_i(ElectricCurrent::new::<milliampere>(val));
}
None => {
info!("Wrong Data type is received")
}
}
}
LdCmdEnum::SetISoftLimit => {
match cmd.json.data_f64 {
Some(val) => {
laser.set_ld_drive_current_limit(ElectricCurrent::new::<milliampere>(val))
}
None => {
info!("Wrong Data type is received")
}
}
}
LdCmdEnum::SetPdResponsitivity => {
info!("Not supported Yet")
}
LdCmdEnum::SetPdDarkCurrent => {
info!("Not supported Yet")
}
LdCmdEnum::SetPdILimit => {
match cmd.json.data_f64 {
Some(val) => {
laser.set_pd_i_limit(ElectricCurrent::new::<milliampere>(val))
}
None => {
info!("Wrong Data type is received")
}
}
}
LdCmdEnum::SetLdPwrLimit => {
info!("Not supported Yet")
}
LdCmdEnum::ClearAlarmStatus => {
laser.pd_mon_clear_alarm()
}
LdCmdEnum::GetModInTermStatus => {
info!("Not supported Yet")
}
LdCmdEnum::GetLdStatus => {
info!("Not supported Yet")
}
LdCmdEnum::GetAlramStatus => {
info!("Not supported Yet")
}
_ => {
info!("Unimplemented Command")
}
}
}
Err(err) => {
info!("Invalid Command: {:?}", err);
}
}
laser
}

View File

@ -1 +1,2 @@
pub mod net;
pub mod cmd_handler;

View File

@ -1,4 +1,6 @@
use crate::device::sys_timer;
use core::cell::RefCell;
use cortex_m::interrupt::{CriticalSection, Mutex};
use log::{debug, info, warn};
use smoltcp::{
iface::{
@ -35,6 +37,11 @@ const ADDRESS: (IpAddress, u16) = (
);
const MAC: [u8; 6] = [0x02, 0x5f, 0x25, 0x37, 0x93, 0x0e];
/// Interrupt pending flag: set by the `ETH` interrupt handler, should
/// be cleared before polling the interface.
static NET_PENDING: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
static mut INCOMING_BYTE: [u8; 512] = [0; 512];
pub struct ServerHandle {
socket_handle: SocketHandle,
socket_set: SocketSet<'static>,
@ -149,7 +156,7 @@ impl ServerHandle {
}
}
pub fn poll(&mut self, buffer: &mut [u8]) {
pub fn echo(&mut self, buffer: &mut [u8]) {
self.iface.poll(now_fn(), &mut &mut self.dma, &mut self.socket_set);
let socket = self.socket_set.get_mut::<Socket>(self.socket_handle);
@ -168,6 +175,37 @@ impl ServerHandle {
self.iface.poll(now_fn(), &mut &mut self.dma, &mut self.socket_set);
}
pub fn recv(&mut self, buffer: &mut [u8])-> Result<usize, smoltcp::socket::tcp::RecvError> {
self.iface.poll(now_fn(), &mut &mut self.dma, &mut self.socket_set);
let socket = self.socket_set.get_mut::<Socket>(self.socket_handle);
socket.recv_slice(buffer)
}
pub fn send(&mut self, buffer: &mut [u8], num_bytes: usize) {
let socket = self.socket_set.get_mut::<Socket>(self.socket_handle);
if num_bytes > 0 {
socket.send_slice(&buffer[..num_bytes]).ok();
info!("Sent {} bytes.", num_bytes);
}
// Send bytes out
self.iface.poll(now_fn(), &mut &mut self.dma, &mut self.socket_set);
}
pub fn poll_socket_status(&mut self)-> bool {
let socket = self.socket_set.get_mut::<Socket>(self.socket_handle);
if !socket.is_listening() && !socket.is_open() || socket.state() == State::CloseWait {
socket.abort();
socket.listen(ADDRESS).ok();
info!("Disconnected... Reopening listening socket.");
return false;
} else if socket.state() == State::Closed || socket.state() == State::Closing {
return false;
}
return true;
}
}
use ieee802_3_miim::{
@ -271,19 +309,67 @@ impl<M: Miim> EthernetPhy<M> {
}
}
pub fn eth_send(buffer: &mut [u8], num_bytes: usize) {
unsafe {
if let Some(ref mut server_handle ) = SERVER_HANDLE {
server_handle.send(buffer, num_bytes);
}
else {
panic!("eth_send is called before init");
}
}
}
pub fn eth_recv(buffer: &mut [u8])-> usize{
unsafe {
if let Some(ref mut server_handle ) = SERVER_HANDLE {
match server_handle.recv(buffer){
Ok(recv_bytes) => {return recv_bytes}
Err(err) => {
debug!("TCP Recv Error: {}", err);
return 0
}
};
}
else {
panic!("eth_send is called before init");
}
}
}
pub fn eth_is_socket_active() -> bool {
unsafe {
if let Some(ref mut server_handle ) = SERVER_HANDLE {
server_handle.poll_socket_status()
}
else {
panic!("eth_is_socket_active is called before init");
}
}
}
/// Potentially wake up from `wfi()`, set the interrupt pending flag,
/// clear interrupt flags.
#[interrupt]
fn ETH() {
let interrupt_reason = stm32_eth::eth_interrupt_handler();
cortex_m::interrupt::free(|cs| {
*NET_PENDING.borrow(cs)
.borrow_mut() = true;
});
debug!("Ethernet Interrupt{:?}", interrupt_reason);
unsafe{
if let Some(ref mut server_handle ) = SERVER_HANDLE {
let mut data : [u8; 512] = [0u8; 512];
server_handle.poll(&mut data);
}
else {
panic!("Interrupt is called before init");
}
/// Has an interrupt occurred since last call to `clear_pending()`?
pub fn is_pending(cs: &CriticalSection) -> bool {
*NET_PENDING.borrow(cs)
.borrow()
}
/// Clear the interrupt pending flag before polling the interface for
/// data.
pub fn clear_pending(cs: &CriticalSection) {
*NET_PENDING.borrow(cs)
.borrow_mut() = false;
}

View File

@ -1,8 +1,8 @@
#[macro_use]
use miniconf::Miniconf;
use miniconf::serde::{Serialize, Deserialize};
use miniconf::Tree;
use miniconf::{Serialize, Deserialize};
#[derive(Clone, Copy, Debug, PartialEq, Miniconf)]
#[derive(Clone, Copy, Debug, PartialEq, Tree)]
pub struct Parameters {
/// Gain coefficient for proportional term
pub kp: f32,
@ -28,9 +28,9 @@ impl Default for Parameters {
}
}
#[derive(Clone, Debug, PartialEq, Miniconf)]
#[derive(Clone, Debug, PartialEq, Tree)]
pub struct Controller {
#[miniconf(defer)]
#[tree]
pub parameters: Parameters,
pub target : f64,
u1 : f64,
@ -87,9 +87,9 @@ impl Controller {
}
}
#[derive(Clone, Debug, Miniconf)]
#[derive(Clone, Debug, Tree)]
pub struct Summary {
#[miniconf(defer)]
#[tree]
parameters: Parameters,
target: f64,
}

View File

@ -8,10 +8,10 @@ use uom::si::{
ratio::ratio,
thermodynamic_temperature::{degree_celsius, kelvin},
};
use miniconf::Miniconf;
use miniconf::{Tree, TreeDeserialize};
/// Steinhart-Hart equation parameters
#[derive(Clone, Debug, PartialEq, Miniconf)]
#[derive(Clone, Debug, PartialEq, Tree)]
pub struct Parameters {
/// Base temperature
pub t0: ThermodynamicTemperature,

View File

@ -14,7 +14,7 @@ use uom::si::{
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time, ThermodynamicTemperature},
ratio::ratio,
};
use miniconf::Miniconf;
use miniconf::Tree;
pub const R_SENSE: ElectricalResistance = ElectricalResistance {
dimension: PhantomData,
@ -22,7 +22,7 @@ pub const R_SENSE: ElectricalResistance = ElectricalResistance {
value: 0.05,
};
#[derive(Clone, Debug, Miniconf)]
#[derive(Clone, Debug, Tree)]
pub struct TecSettings {
pub center_pt: ElectricPotential,
pub max_v_set: ElectricPotential,
@ -303,7 +303,7 @@ impl Thermostat{
}
#[derive(Miniconf)]
#[derive(Tree)]
pub struct StatusReport {
pid_update_ts: Time,
pid_update_interval: Time,
@ -315,27 +315,27 @@ pub struct StatusReport {
tec_vref: ElectricPotential,
}
#[derive(Miniconf)]
#[derive(Tree)]
pub struct TecSettingsSummaryField<T> {
value: T,
max: T,
}
#[derive(Miniconf)]
#[derive(Tree)]
pub struct TecSettingSummary {
center_point: ElectricPotential,
#[miniconf(defer)]
#[tree]
i_set: TecSettingsSummaryField<ElectricCurrent>,
#[miniconf(defer)]
#[tree]
max_v: TecSettingsSummaryField<ElectricPotential>,
#[miniconf(defer)]
#[tree]
max_i_pos: TecSettingsSummaryField<ElectricCurrent>,
#[miniconf(defer)]
#[tree]
max_i_neg: TecSettingsSummaryField<ElectricCurrent>,
}
#[derive(Miniconf)]
#[derive(Tree)]
pub struct SteinhartHartSummary {
#[miniconf(defer)]
#[tree]
params: steinhart_hart::Parameters,
}