SummerCart64/sw/deployer/src/sc64/mod.rs

815 lines
26 KiB
Rust
Raw Normal View History

mod cic;
mod error;
pub mod firmware;
mod link;
mod server;
mod time;
mod types;
pub use self::{
error::Error,
link::list_local_devices,
server::ServerEvent,
types::{
BootMode, ButtonMode, ButtonState, CicSeed, DataPacket, DdDiskState, DdDriveType, DdMode,
DebugPacket, DiskPacket, DiskPacketKind, FpgaDebugData, McuStackUsage, SaveType,
SaveWriteback, Switch, TvType,
},
};
use self::{
cic::{calculate_ipl3_checksum, guess_ipl3_seed, IPL3_LENGTH, IPL3_OFFSET},
link::{Command, Link},
time::{convert_from_datetime, convert_to_datetime},
types::{
get_config, get_setting, Config, ConfigId, FirmwareStatus, Setting, SettingId, UpdateStatus,
},
};
use chrono::{DateTime, Local};
use std::{
io::{Read, Seek, Write},
time::Instant,
{cmp::min, time::Duration},
};
pub struct SC64 {
link: Link,
}
pub struct DeviceState {
pub bootloader_switch: Switch,
pub rom_write_enable: Switch,
pub rom_shadow_enable: Switch,
pub dd_mode: DdMode,
pub isv_address: u32,
pub boot_mode: BootMode,
pub save_type: SaveType,
pub cic_seed: CicSeed,
pub tv_type: TvType,
pub dd_sd_enable: Switch,
pub dd_drive_type: DdDriveType,
pub dd_disk_state: DdDiskState,
pub button_state: ButtonState,
pub button_mode: ButtonMode,
pub rom_extended_enable: Switch,
pub led_enable: Switch,
pub datetime: DateTime<Local>,
pub fpga_debug_data: FpgaDebugData,
pub mcu_stack_usage: McuStackUsage,
}
const SC64_V2_IDENTIFIER: &[u8; 4] = b"SCv2";
const SUPPORTED_MAJOR_VERSION: u16 = 2;
2023-06-30 01:11:25 +02:00
const SUPPORTED_MINOR_VERSION: u16 = 16;
const SDRAM_ADDRESS: u32 = 0x0000_0000;
const SDRAM_LENGTH: usize = 64 * 1024 * 1024;
const ROM_SHADOW_ADDRESS: u32 = 0x04FE_0000;
const ROM_SHADOW_LENGTH: usize = 128 * 1024;
const ROM_EXTENDED_ADDRESS: u32 = 0x0400_0000;
const ROM_EXTENDED_LENGTH: usize = 14 * 1024 * 1024;
const MAX_ROM_LENGTH: usize = 78 * 1024 * 1024;
const DDIPL_ADDRESS: u32 = 0x03BC_0000;
const DDIPL_LENGTH: usize = 4 * 1024 * 1024;
const SAVE_ADDRESS: u32 = 0x03FE_0000;
const EEPROM_ADDRESS: u32 = 0x0500_2000;
const EEPROM_4K_LENGTH: usize = 512;
const EEPROM_16K_LENGTH: usize = 2 * 1024;
const SRAM_LENGTH: usize = 32 * 1024;
const FLASHRAM_LENGTH: usize = 128 * 1024;
const SRAM_BANKED_LENGTH: usize = 3 * 32 * 1024;
const SRAM_1M_LENGTH: usize = 128 * 1024;
const BOOTLOADER_ADDRESS: u32 = 0x04E0_0000;
const FIRMWARE_ADDRESS: u32 = 0x0010_0000; // Arbitrary offset in SDRAM memory
const FIRMWARE_UPDATE_TIMEOUT: Duration = Duration::from_secs(90);
const ISV_BUFFER_LENGTH: usize = 64 * 1024;
pub const MEMORY_LENGTH: usize = 0x0500_2980;
const MEMORY_CHUNK_LENGTH: usize = 1 * 1024 * 1024;
impl SC64 {
fn command_identifier_get(&mut self) -> Result<[u8; 4], Error> {
let data = self.link.execute_command(&Command {
id: b'v',
args: [0, 0],
data: vec![],
})?;
if data.len() != 4 {
return Err(Error::new(
"Invalid data length received for identifier get command",
));
}
Ok(data[0..4].try_into().unwrap())
}
fn command_version_get(&mut self) -> Result<(u16, u16, u32), Error> {
let data = self.link.execute_command(&Command {
id: b'V',
args: [0, 0],
data: vec![],
})?;
if data.len() != 8 {
return Err(Error::new(
"Invalid data length received for version get command",
));
}
let major = u16::from_be_bytes(data[0..2].try_into().unwrap());
let minor = u16::from_be_bytes(data[2..4].try_into().unwrap());
let revision = u32::from_be_bytes(data[4..8].try_into().unwrap());
Ok((major, minor, revision))
}
fn command_state_reset(&mut self) -> Result<(), Error> {
self.link.execute_command(&Command {
id: b'R',
args: [0, 0],
data: vec![],
})?;
Ok(())
}
fn command_cic_params_set(
&mut self,
disable: bool,
seed: u8,
checksum: &[u8; 6],
) -> Result<(), Error> {
let args = [
u32::from_be_bytes([(disable as u8) << 0, seed, checksum[0], checksum[1]]),
u32::from_be_bytes([checksum[2], checksum[3], checksum[4], checksum[5]]),
];
self.link.execute_command(&Command {
id: b'B',
args,
data: vec![],
})?;
Ok(())
}
fn command_config_get(&mut self, config_id: ConfigId) -> Result<Config, Error> {
let data = self.link.execute_command(&Command {
id: b'c',
args: [config_id.into(), 0],
data: vec![],
})?;
if data.len() != 4 {
return Err(Error::new(
"Invalid data length received for config get command",
));
}
let value = u32::from_be_bytes(data[0..4].try_into().unwrap());
Ok((config_id, value).try_into()?)
}
fn command_config_set(&mut self, config: Config) -> Result<(), Error> {
self.link.execute_command(&Command {
id: b'C',
args: config.into(),
data: vec![],
})?;
Ok(())
}
fn command_setting_get(&mut self, setting_id: SettingId) -> Result<Setting, Error> {
let data = self.link.execute_command(&Command {
id: b'a',
args: [setting_id.into(), 0],
data: vec![],
})?;
if data.len() != 4 {
return Err(Error::new(
"Invalid data length received for setting get command",
));
}
let value = u32::from_be_bytes(data[0..4].try_into().unwrap());
Ok((setting_id, value).try_into()?)
}
fn command_setting_set(&mut self, setting: Setting) -> Result<(), Error> {
self.link.execute_command(&Command {
id: b'A',
args: setting.into(),
data: vec![],
})?;
Ok(())
}
fn command_time_get(&mut self) -> Result<DateTime<Local>, Error> {
let data = self.link.execute_command(&Command {
id: b't',
args: [0, 0],
data: vec![],
})?;
if data.len() != 8 {
return Err(Error::new(
"Invalid data length received for time get command",
));
}
Ok(convert_to_datetime(&data[0..8].try_into().unwrap())?)
}
fn command_time_set(&mut self, datetime: DateTime<Local>) -> Result<(), Error> {
self.link.execute_command(&Command {
id: b'T',
args: convert_from_datetime(datetime),
data: vec![],
})?;
Ok(())
}
fn command_memory_read(&mut self, address: u32, length: usize) -> Result<Vec<u8>, Error> {
let data = self.link.execute_command(&Command {
id: b'm',
args: [address, length as u32],
data: vec![],
})?;
if data.len() != length {
return Err(Error::new(
"Invalid data length received for memory read command",
));
}
Ok(data)
}
fn command_memory_write(&mut self, address: u32, data: &[u8]) -> Result<(), Error> {
self.link.execute_command(&Command {
id: b'M',
args: [address, data.len() as u32],
data: data.to_vec(),
})?;
Ok(())
}
fn command_usb_write(&mut self, datatype: u8, data: &[u8]) -> Result<(), Error> {
self.link.execute_command_raw(
&Command {
id: b'U',
args: [datatype as u32, data.len() as u32],
data: data.to_vec(),
},
true,
false,
)?;
Ok(())
}
fn command_dd_set_block_ready(&mut self, error: bool) -> Result<(), Error> {
self.link.execute_command(&Command {
id: b'D',
args: [error as u32, 0],
data: vec![],
})?;
Ok(())
}
fn command_writeback_enable(&mut self) -> Result<(), Error> {
self.link.execute_command(&Command {
id: b'W',
args: [0, 0],
data: vec![],
})?;
Ok(())
}
fn command_flash_wait_busy(&mut self, wait: bool) -> Result<u32, Error> {
let data = self.link.execute_command(&Command {
id: b'p',
args: [wait as u32, 0],
data: vec![],
})?;
if data.len() != 4 {
return Err(Error::new(
"Invalid data length received for flash wait busy command",
));
}
let erase_block_size = u32::from_be_bytes(data[0..4].try_into().unwrap());
Ok(erase_block_size)
}
fn command_flash_erase_block(&mut self, address: u32) -> Result<(), Error> {
self.link.execute_command(&Command {
id: b'P',
args: [address, 0],
data: vec![],
})?;
Ok(())
}
fn command_firmware_backup(&mut self, address: u32) -> Result<(FirmwareStatus, u32), Error> {
let data = self.link.execute_command_raw(
&Command {
id: b'f',
args: [address, 0],
data: vec![],
},
false,
true,
)?;
if data.len() != 8 {
return Err(Error::new(
"Invalid data length received for firmware backup command",
));
}
let status = u32::from_be_bytes(data[0..4].try_into().unwrap());
let length = u32::from_be_bytes(data[4..8].try_into().unwrap());
Ok((status.try_into()?, length))
}
fn command_firmware_update(
&mut self,
address: u32,
length: usize,
) -> Result<FirmwareStatus, Error> {
let data = self.link.execute_command_raw(
&Command {
id: b'F',
args: [address, length as u32],
data: vec![],
},
false,
true,
)?;
if data.len() != 4 {
return Err(Error::new(
"Invalid data length received for firmware update command",
));
}
Ok(u32::from_be_bytes(data[0..4].try_into().unwrap()).try_into()?)
}
fn command_debug_get(&mut self) -> Result<FpgaDebugData, Error> {
let data = self.link.execute_command(&Command {
id: b'?',
args: [0, 0],
data: vec![],
})?;
Ok(data.try_into()?)
}
fn command_stack_usage_get(&mut self) -> Result<McuStackUsage, Error> {
let data = self.link.execute_command(&Command {
id: b'%',
args: [0, 0],
data: vec![],
})?;
Ok(data.try_into()?)
}
}
impl SC64 {
pub fn upload_rom<T: Read + Seek>(
&mut self,
reader: &mut T,
length: usize,
no_shadow: bool,
) -> Result<(), Error> {
if length > MAX_ROM_LENGTH {
return Err(Error::new("ROM length too big"));
}
let mut pi_config = vec![0u8; 4];
reader.rewind()?;
reader.read_exact(&mut pi_config)?;
reader.rewind()?;
let endian_swapper = match &pi_config[0..4] {
[0x37, 0x80, 0x40, 0x12] => {
|b: &mut [u8]| b.chunks_exact_mut(2).for_each(|c| c.swap(0, 1))
}
[0x40, 0x12, 0x37, 0x80] => |b: &mut [u8]| {
b.chunks_exact_mut(4).for_each(|c| {
c.swap(0, 3);
c.swap(1, 2)
})
},
_ => |_: &mut [u8]| {},
};
let rom_shadow_enabled = !no_shadow && length > (SDRAM_LENGTH - ROM_SHADOW_LENGTH);
let rom_extended_enabled = length > SDRAM_LENGTH;
let sdram_length = if rom_shadow_enabled {
min(length, SDRAM_LENGTH - ROM_SHADOW_LENGTH)
} else {
min(length, SDRAM_LENGTH)
};
self.memory_write_chunked(reader, SDRAM_ADDRESS, sdram_length, Some(endian_swapper))?;
self.command_config_set(Config::RomShadowEnable(rom_shadow_enabled.into()))?;
if rom_shadow_enabled {
let rom_shadow_length = min(length - sdram_length, ROM_SHADOW_LENGTH);
self.flash_program(
reader,
ROM_SHADOW_ADDRESS,
rom_shadow_length,
Some(endian_swapper),
)?;
}
self.command_config_set(Config::RomExtendedEnable(rom_extended_enabled.into()))?;
if rom_extended_enabled {
let rom_extended_length = min(length - SDRAM_LENGTH, ROM_EXTENDED_LENGTH);
self.flash_program(
reader,
ROM_EXTENDED_ADDRESS,
rom_extended_length,
Some(endian_swapper),
)?;
}
Ok(())
}
pub fn upload_ddipl<T: Read>(&mut self, reader: &mut T, length: usize) -> Result<(), Error> {
if length > DDIPL_LENGTH {
return Err(Error::new("DDIPL length too big"));
}
self.memory_write_chunked(reader, DDIPL_ADDRESS, length, None)
}
pub fn upload_save<T: Read>(&mut self, reader: &mut T, length: usize) -> Result<(), Error> {
let save_type = get_config!(self, SaveType)?;
let (address, save_length) = match save_type {
SaveType::None => {
return Err(Error::new("No save type is enabled"));
}
SaveType::Eeprom4k => (EEPROM_ADDRESS, EEPROM_4K_LENGTH),
SaveType::Eeprom16k => (EEPROM_ADDRESS, EEPROM_16K_LENGTH),
SaveType::Sram => (SAVE_ADDRESS, SRAM_LENGTH),
SaveType::Flashram => (SAVE_ADDRESS, FLASHRAM_LENGTH),
SaveType::SramBanked => (SAVE_ADDRESS, SRAM_BANKED_LENGTH),
SaveType::Sram1m => (SAVE_ADDRESS, SRAM_1M_LENGTH),
};
if length != save_length {
return Err(Error::new(
"Save file size did not match currently enabled save type",
));
}
self.memory_write_chunked(reader, address, save_length, None)
}
pub fn download_save<T: Write>(&mut self, writer: &mut T) -> Result<(), Error> {
let save_type = get_config!(self, SaveType)?;
let (address, save_length) = match save_type {
SaveType::None => {
return Err(Error::new("No save type is enabled"));
}
SaveType::Eeprom4k => (EEPROM_ADDRESS, EEPROM_4K_LENGTH),
SaveType::Eeprom16k => (EEPROM_ADDRESS, EEPROM_16K_LENGTH),
SaveType::Sram => (SAVE_ADDRESS, SRAM_LENGTH),
SaveType::Flashram => (SAVE_ADDRESS, FLASHRAM_LENGTH),
SaveType::SramBanked => (SAVE_ADDRESS, SRAM_BANKED_LENGTH),
SaveType::Sram1m => (SAVE_ADDRESS, SRAM_1M_LENGTH),
};
self.memory_read_chunked(writer, address, save_length)
}
pub fn dump_memory<T: Write>(
&mut self,
writer: &mut T,
address: u32,
length: usize,
) -> Result<(), Error> {
if address + length as u32 > MEMORY_LENGTH as u32 {
return Err(Error::new("Invalid dump address or length"));
}
self.memory_read_chunked(writer, address, length)
}
pub fn calculate_cic_parameters(&mut self) -> Result<(), Error> {
let boot_mode = get_config!(self, BootMode)?;
let address = match boot_mode {
BootMode::DirectRom => SDRAM_ADDRESS,
BootMode::DirectDdIpl => DDIPL_ADDRESS,
_ => BOOTLOADER_ADDRESS,
};
let ipl3 = self.command_memory_read(address + IPL3_OFFSET, IPL3_LENGTH)?;
let seed = guess_ipl3_seed(&ipl3)?;
let checksum = &calculate_ipl3_checksum(&ipl3, seed)?;
self.command_cic_params_set(false, seed, checksum)
}
pub fn set_boot_mode(&mut self, boot_mode: BootMode) -> Result<(), Error> {
self.command_config_set(Config::BootMode(boot_mode))
}
pub fn set_save_type(&mut self, save_type: SaveType) -> Result<(), Error> {
self.command_config_set(Config::SaveType(save_type))
}
pub fn set_tv_type(&mut self, tv_type: TvType) -> Result<(), Error> {
self.command_config_set(Config::TvType(tv_type))
}
pub fn get_datetime(&mut self) -> Result<DateTime<Local>, Error> {
self.command_time_get()
}
pub fn set_datetime(&mut self, datetime: DateTime<Local>) -> Result<(), Error> {
self.command_time_set(datetime)
}
pub fn set_led_blink(&mut self, enabled: bool) -> Result<(), Error> {
self.command_setting_set(Setting::LedEnable(enabled.into()))
}
pub fn get_device_state(&mut self) -> Result<DeviceState, Error> {
Ok(DeviceState {
bootloader_switch: get_config!(self, BootloaderSwitch)?,
rom_write_enable: get_config!(self, RomWriteEnable)?,
rom_shadow_enable: get_config!(self, RomShadowEnable)?,
dd_mode: get_config!(self, DdMode)?,
isv_address: get_config!(self, IsvAddress)?,
boot_mode: get_config!(self, BootMode)?,
save_type: get_config!(self, SaveType)?,
cic_seed: get_config!(self, CicSeed)?,
tv_type: get_config!(self, TvType)?,
dd_sd_enable: get_config!(self, DdSdEnable)?,
dd_drive_type: get_config!(self, DdDriveType)?,
dd_disk_state: get_config!(self, DdDiskState)?,
button_state: get_config!(self, ButtonState)?,
button_mode: get_config!(self, ButtonMode)?,
rom_extended_enable: get_config!(self, RomExtendedEnable)?,
led_enable: get_setting!(self, LedEnable)?,
datetime: self.get_datetime()?,
fpga_debug_data: self.command_debug_get()?,
mcu_stack_usage: self.command_stack_usage_get()?,
})
}
pub fn configure_64dd(
&mut self,
dd_mode: DdMode,
drive_type: DdDriveType,
) -> Result<(), Error> {
self.command_config_set(Config::DdMode(dd_mode))?;
self.command_config_set(Config::DdSdEnable(Switch::Off))?;
self.command_config_set(Config::DdDriveType(drive_type))?;
self.command_config_set(Config::DdDiskState(DdDiskState::Ejected))?;
self.command_config_set(Config::ButtonMode(ButtonMode::UsbPacket))?;
Ok(())
}
pub fn set_64dd_disk_state(&mut self, disk_state: DdDiskState) -> Result<(), Error> {
self.command_config_set(Config::DdDiskState(disk_state))
}
pub fn configure_is_viewer_64(&mut self, offset: Option<u32>) -> Result<(), Error> {
if let Some(offset) = offset {
if get_config!(self, RomShadowEnable)?.into() {
if offset > (SAVE_ADDRESS - ISV_BUFFER_LENGTH as u32) {
return Err(Error::new(
format!(
"ROM shadow is enabled, IS-Viewer 64 at offset 0x{offset:08X} won't work"
)
.as_str(),
));
}
}
self.command_config_set(Config::RomWriteEnable(Switch::On))?;
self.command_config_set(Config::IsvAddress(offset))?;
} else {
self.command_config_set(Config::RomWriteEnable(Switch::Off))?;
self.command_config_set(Config::IsvAddress(0))?;
}
Ok(())
}
pub fn set_save_writeback(&mut self, enabled: bool) -> Result<(), Error> {
if enabled {
self.command_writeback_enable()?;
} else {
let save_type = get_config!(self, SaveType)?;
self.set_save_type(save_type)?;
}
Ok(())
}
pub fn receive_data_packet(&mut self) -> Result<Option<DataPacket>, Error> {
if let Some(packet) = self.link.receive_packet()? {
return Ok(Some(packet.try_into()?));
}
Ok(None)
}
pub fn reply_disk_packet(&mut self, disk_packet: Option<DiskPacket>) -> Result<(), Error> {
if let Some(packet) = disk_packet {
match packet.kind {
DiskPacketKind::Read => {
self.command_memory_write(packet.info.address, &packet.info.data)?;
}
DiskPacketKind::Write => {}
}
self.command_dd_set_block_ready(false)?;
} else {
self.command_dd_set_block_ready(true)?;
}
Ok(())
}
pub fn send_debug_packet(&mut self, debug_packet: DebugPacket) -> Result<(), Error> {
self.command_usb_write(debug_packet.datatype, &debug_packet.data)
}
pub fn check_device(&mut self) -> Result<(), Error> {
let identifier = self.command_identifier_get().map_err(|e| {
Error::new(format!("Couldn't get SC64 device identifier: {e}").as_str())
})?;
if &identifier != SC64_V2_IDENTIFIER {
return Err(Error::new("Unknown identifier received, not a SC64 device"));
}
Ok(())
}
pub fn check_firmware_version(&mut self) -> Result<(u16, u16, u32), Error> {
let unsupported_version_message = format!(
"Unsupported SC64 firmware version, minimum supported version: {}.{}.x",
SUPPORTED_MAJOR_VERSION, SUPPORTED_MINOR_VERSION
);
let (major, minor, revision) = self
.command_version_get()
.map_err(|_| Error::new(unsupported_version_message.as_str()))?;
if major != SUPPORTED_MAJOR_VERSION || minor < SUPPORTED_MINOR_VERSION {
return Err(Error::new(unsupported_version_message.as_str()));
}
Ok((major, minor, revision))
}
pub fn reset_state(&mut self) -> Result<(), Error> {
self.command_state_reset()
}
pub fn backup_firmware(&mut self) -> Result<Vec<u8>, Error> {
self.command_state_reset()?;
let (status, length) = self.command_firmware_backup(FIRMWARE_ADDRESS)?;
if !matches!(status, FirmwareStatus::Ok) {
return Err(Error::new(
format!("Firmware backup error: {}", status).as_str(),
));
}
self.command_memory_read(FIRMWARE_ADDRESS, length as usize)
}
pub fn update_firmware(&mut self, data: &[u8]) -> Result<(), Error> {
self.command_state_reset()?;
self.command_memory_write(FIRMWARE_ADDRESS, data)?;
let status = self.command_firmware_update(FIRMWARE_ADDRESS, data.len())?;
if !matches!(status, FirmwareStatus::Ok) {
return Err(Error::new(
format!("Firmware update verify error: {}", status).as_str(),
));
}
let timeout = Instant::now();
let mut last_update_status = UpdateStatus::Err;
loop {
if let Some(packet) = self.receive_data_packet()? {
if let DataPacket::UpdateStatus(status) = packet {
match status {
UpdateStatus::Done => {
std::thread::sleep(Duration::from_secs(2));
return Ok(());
}
UpdateStatus::Err => {
return Err(Error::new(
format!(
"Firmware update error on step {}, device is, most likely, bricked",
last_update_status
)
.as_str(),
))
}
current_update_status => last_update_status = current_update_status,
}
}
}
if timeout.elapsed() > FIRMWARE_UPDATE_TIMEOUT {
return Err(Error::new(
format!(
"Firmware update timeout, SC64 did not finish update in {} seconds, last step: {}",
FIRMWARE_UPDATE_TIMEOUT.as_secs(),
last_update_status
)
.as_str(),
));
}
std::thread::sleep(Duration::from_millis(1));
}
}
fn memory_read_chunked(
&mut self,
writer: &mut dyn Write,
address: u32,
length: usize,
) -> Result<(), Error> {
let mut memory_address = address;
let mut bytes_left = length;
while bytes_left > 0 {
let bytes = min(MEMORY_CHUNK_LENGTH, bytes_left);
let data = self.command_memory_read(memory_address, bytes)?;
writer.write_all(&data)?;
memory_address += bytes as u32;
bytes_left -= bytes;
}
Ok(())
}
fn memory_write_chunked(
&mut self,
reader: &mut dyn Read,
address: u32,
length: usize,
transform: Option<fn(&mut [u8])>,
) -> Result<(), Error> {
let mut limited_reader = reader.take(length as u64);
let mut memory_address = address;
let mut data: Vec<u8> = vec![0u8; MEMORY_CHUNK_LENGTH];
loop {
let bytes = limited_reader.read(&mut data)?;
if bytes == 0 {
break;
}
if let Some(transform) = transform {
transform(&mut data[0..bytes]);
}
self.command_memory_write(memory_address, &data[0..bytes])?;
memory_address += bytes as u32;
}
Ok(())
}
fn flash_erase(&mut self, address: u32, length: usize) -> Result<(), Error> {
let erase_block_size = self.command_flash_wait_busy(false)?;
for offset in (0..length as u32).step_by(erase_block_size as usize) {
self.command_flash_erase_block(address + offset)?;
}
Ok(())
}
fn flash_program(
&mut self,
reader: &mut dyn Read,
address: u32,
length: usize,
transform: Option<fn(&mut [u8])>,
) -> Result<(), Error> {
self.flash_erase(address, length)?;
self.memory_write_chunked(reader, address, length, transform)?;
self.command_flash_wait_busy(true)?;
Ok(())
}
}
pub fn new_local(port: Option<String>) -> Result<SC64, Error> {
let port = if let Some(port) = port {
port
} else {
list_local_devices()?[0].port.clone()
};
let mut sc64 = SC64 {
link: link::new_local(&port)?,
};
sc64.check_device()?;
Ok(sc64)
}
pub fn new_remote(address: String) -> Result<SC64, Error> {
let mut sc64 = SC64 {
link: link::new_remote(&address)?,
};
sc64.check_device()?;
Ok(sc64)
}
pub fn run_server(
port: Option<String>,
address: String,
event_callback: fn(ServerEvent),
) -> Result<(), Error> {
let port = if let Some(port) = port {
port
} else {
list_local_devices()?[0].port.clone()
};
server::run_server(&port, address, event_callback)
}