Initial commit

This commit is contained in:
beerpsi 2023-12-21 11:47:26 +07:00
parent e92aafc6cf
commit 8df563eb20
18 changed files with 701 additions and 0 deletions

121
.gitignore vendored Normal file
View File

@ -0,0 +1,121 @@
/target
/Cargo.lock
**/target
# Created by https://www.toptal.com/developers/gitignore/api/jetbrains
# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains
### JetBrains ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
# End of https://www.toptal.com/developers/gitignore/api/jetbrains

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/chuniio-tasoller-amdaemon/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/chuniio-tasoller-chusan/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/chuniio-tasoller-common/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/chuniio-tasoller-amdaemon/target" />
<excludeFolder url="file://$MODULE_DIR$/chuniio-tasoller-chusan/target" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="RsStaticConstNaming" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/chuniio-tasoller.iml" filepath="$PROJECT_DIR$/.idea/chuniio-tasoller.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[workspace]
members = ["chuniio-tasoller-amdaemon", "chuniio-tasoller-chusan", "chuniio-tasoller-common"]
resolver = "2"
[profile.release]
strip = true # Automatically strip symbols from the binary.
opt-level = "z" # Optimize for size.
lto = true
codegen-units = 1
panic = "abort"

7
chuniio-tasoller-amdaemon/Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "chuniio-tasoller-amdaemon"
version = "0.1.0"

View File

@ -0,0 +1,16 @@
[package]
name = "chuniio-tasoller-amdaemon"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.75"
chuniio-tasoller-common = { path = "../chuniio-tasoller-common" }
lazy_static = "1.4.0"
log = "0.4.20"
rusb = "0.9.3"
winapi = { version = "0.3.9", features = ["ntdef", "winbase", "winerror", "minwindef", "winuser"] }

View File

@ -0,0 +1,6 @@
#[derive(Debug, Clone, Copy)]
pub struct Configuration {
pub test_key: u32,
pub service_key: u32,
pub coin_key: u32,
}

View File

@ -0,0 +1,139 @@
mod configuration;
use crate::configuration::Configuration;
use chuniio_tasoller_common::create_input_shared_memory;
use chuniio_tasoller_common::log::init_logger;
use lazy_static::lazy_static;
use std::ffi::{c_int, c_void, CString};
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
use winapi::shared::minwindef::{BOOL, DWORD, HINSTANCE, LPVOID, TRUE};
use winapi::shared::ntdef::HRESULT;
use winapi::shared::winerror::S_OK;
use winapi::um::winbase::GetPrivateProfileIntA;
use winapi::um::winnt::DLL_PROCESS_ATTACH;
use winapi::um::winuser::GetAsyncKeyState;
static COIN_COUNT: AtomicU16 = AtomicU16::new(0);
static COIN_PRESSED: AtomicBool = AtomicBool::new(false);
lazy_static! {
static ref CONFIGURATION: Configuration = {
let io3 = CString::new("io3").unwrap();
let test = CString::new("test").unwrap();
let service = CString::new("service").unwrap();
let coin = CString::new("coin").unwrap();
let cfg_file = CString::new(".\\segatools.ini").unwrap();
unsafe {
Configuration {
test_key: GetPrivateProfileIntA(
io3.as_ptr(),
test.as_ptr(),
0x31,
cfg_file.as_ptr(),
),
service_key: GetPrivateProfileIntA(
io3.as_ptr(),
service.as_ptr(),
0x32,
cfg_file.as_ptr(),
),
coin_key: GetPrivateProfileIntA(
io3.as_ptr(),
coin.as_ptr(),
0x33,
cfg_file.as_ptr(),
),
}
}
};
}
#[no_mangle]
extern "system" fn DllMain(_dll_module: HINSTANCE, call_reason: DWORD, _reserved: LPVOID) -> BOOL {
match call_reason {
DLL_PROCESS_ATTACH => {
init_logger();
}
_ => {}
}
TRUE
}
#[no_mangle]
pub extern "C" fn chuni_io_get_api_version() -> u16 {
0x0101
}
#[no_mangle]
pub extern "C" fn chuni_io_jvs_init() -> HRESULT {
S_OK
}
#[no_mangle]
pub unsafe extern "C" fn chuni_io_jvs_poll(opbtn: *mut u8, beams: *mut u8) {
let Ok(input_shmem) = create_input_shared_memory(false) else {
return;
};
let input = input_shmem.as_slice();
let bit = input[3];
if bit & (1 << 6) != 0 {
*opbtn |= 0x1;
}
// fn2
if bit & (1 << 7) != 0 {
*opbtn |= 0x2;
}
for i in 0..6 {
if bit & (1 << i) != 0 {
*beams |= 1 << i;
}
}
}
#[no_mangle]
pub unsafe extern "C" fn chuni_io_jvs_read_coin_counter(out: *mut u16) {
if out.is_null() {
return;
}
if GetAsyncKeyState(CONFIGURATION.coin_key as c_int) != 0 {
let coin_previously_pressed = COIN_PRESSED
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |_| Some(true))
.unwrap();
if !coin_previously_pressed {
COIN_COUNT.fetch_add(1, Ordering::Relaxed);
}
} else {
COIN_PRESSED.store(false, Ordering::Relaxed);
}
*out = COIN_COUNT.load(Ordering::Relaxed);
}
// Stubs. These are handled by chusanApp
#[no_mangle]
pub extern "C" fn chuni_io_slider_init() -> HRESULT {
S_OK
}
#[no_mangle]
pub extern "C" fn chuni_io_slider_start(_callback: *const c_void) {}
#[no_mangle]
pub extern "C" fn chuni_io_slider_set_leds(_rgb: *mut u8) {}
#[no_mangle]
pub extern "C" fn chuni_io_slider_stop() {}
#[no_mangle]
pub extern "C" fn chuni_io_led_init() -> HRESULT {
S_OK
}
#[no_mangle]
pub extern "C" fn chuni_io_led_set_colors(_board: u8, _data: *const u8) {}

View File

@ -0,0 +1,2 @@
[build]
target = "i686-pc-windows-msvc"

7
chuniio-tasoller-chusan/Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "chuniio-tasoller-chusan"
version = "0.1.0"

View File

@ -0,0 +1,15 @@
[package]
name = "chuniio-tasoller-chusan"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.75"
chuniio-tasoller-common = { path = "../chuniio-tasoller-common" }
log = "0.4.20"
rusb = "0.9.3"
winapi = { version = "0.3.9", features = ["ntdef", "winerror", "minwindef"] }

View File

@ -0,0 +1,218 @@
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::OnceLock;
use std::thread;
use std::thread::JoinHandle;
use std::time::Duration;
use anyhow::{anyhow, Result};
use rusb::{DeviceHandle, GlobalContext};
use winapi::{
shared::{
minwindef::{BOOL, DWORD, HINSTANCE, LPVOID, TRUE},
ntdef::HRESULT,
winerror::S_OK,
},
um::winnt::DLL_PROCESS_ATTACH,
};
use chuniio_tasoller_common::{create_input_shared_memory, log::init_logger};
use log::{error, info};
use winapi::shared::winerror::E_FAIL;
type SliderCallbackFn = unsafe extern "C" fn(data: *const u8);
static TASOLLER: OnceLock<DeviceHandle<GlobalContext>> = OnceLock::new();
static mut SLIDER_THREAD: OnceLock<JoinHandle<()>> = OnceLock::new();
static SLIDER_ACTIVE: AtomicBool = AtomicBool::new(false);
static mut USB_OUT: [u8; 240] = [0; 240];
#[no_mangle]
extern "system" fn DllMain(_dll_module: HINSTANCE, call_reason: DWORD, _reserved: LPVOID) -> BOOL {
match call_reason {
DLL_PROCESS_ATTACH => {
init_logger();
}
_ => {}
}
TRUE
}
#[no_mangle]
pub extern "C" fn chuni_io_get_api_version() -> u16 {
0x0101
}
#[no_mangle]
pub extern "C" fn chuni_io_slider_init() -> HRESULT {
info!("chuni_io_slider_init");
match tasoller_init() {
Ok(_) => S_OK,
Err(e) => {
error!("Failed to initialize Tasoller: {e:#}");
E_FAIL
}
}
}
#[no_mangle]
pub extern "C" fn chuni_io_slider_start(callback: *const c_void) {
if callback.is_null() {
return;
}
unsafe {
if SLIDER_THREAD.get().is_some() {
return;
}
SLIDER_ACTIVE.store(true, Ordering::Relaxed);
let callback = std::mem::transmute::<_, SliderCallbackFn>(callback);
let thread = thread::spawn(move || {
let input_shmem = match create_input_shared_memory(false) {
Ok(s) => s,
Err(e) => {
error!("Could not obtain shared memory for Tasoller input: {e:#}");
return;
}
};
let usb_in = input_shmem.as_slice();
let mut pressure = [0u8; 32];
while SLIDER_ACTIVE.load(Ordering::Relaxed) {
for i in 0..32 {
pressure[if i % 2 == 0 { 30 - i } else { 32 - i }] = usb_in[i + 4];
}
callback(pressure.as_ptr());
thread::sleep(Duration::from_nanos(1_000_000));
}
});
SLIDER_THREAD.set(thread).unwrap();
}
}
#[no_mangle]
pub extern "C" fn chuni_io_slider_set_leds(data: *const u8) {
if data.is_null() {
return;
}
unsafe {
let Some(device) = TASOLLER.get() else { return };
let led_report = std::slice::from_raw_parts(data, 96);
for n in 0..31 {
USB_OUT[n * 3 + 3] = led_report[n * 3 + 2];
USB_OUT[n * 3 + 4] = led_report[n * 3 + 1];
USB_OUT[n * 3 + 5] = led_report[n * 3 + 0];
}
if let Err(e) = device.write_bulk(0x03, &USB_OUT, Duration::from_micros(1)) {
error!("Error writing slider LED data to Tasoller: {e:#}");
}
}
}
#[no_mangle]
pub unsafe extern "C" fn chuni_io_slider_stop() {
let Some(thread) = SLIDER_THREAD.take() else {
return;
};
SLIDER_ACTIVE.store(false, Ordering::Relaxed);
thread.join().expect("Couldn't join slider input thread");
}
#[no_mangle]
pub extern "C" fn chuni_io_led_init() -> HRESULT {
S_OK
}
#[no_mangle]
pub extern "C" fn chuni_io_led_set_colors(board: u8, data: *const u8) {
if data.is_null() {
return;
}
let start = if board == 0 { 96 } else { 168 };
let rgb_start = if board == 0 { 0x96 } else { 0xb4 };
unsafe {
let Some(device) = TASOLLER.get() else { return };
for n in 0..24 {
USB_OUT[start + n * 3 + 1] = *data.wrapping_add(rgb_start);
USB_OUT[start + n * 3 + 0] = *data.wrapping_add(rgb_start + 1);
USB_OUT[start + n * 3 + 2] = *data.wrapping_add(rgb_start + 2);
}
if let Err(e) = device.write_bulk(0x03, &USB_OUT, Duration::from_micros(1)) {
error!("Error writing LED data to Tasoller: {e:#}");
}
}
}
fn tasoller_init() -> Result<()> {
let mut device = match rusb::open_device_with_vid_pid(0x1CCF, 0x2333) {
Some(dev) => dev,
None => {
return Err(anyhow!("Tasoller not found."));
}
};
device
.claim_interface(0)
.map_err(|e| anyhow!("Cannot open Tasoller: {e:#}"))?;
TASOLLER
.set(device)
.map_err(|e| anyhow!("Cannot store device handle: {e:#?}"))?;
unsafe {
USB_OUT[0] = 0x42;
USB_OUT[1] = 0x4C;
USB_OUT[2] = 0x00;
}
thread::spawn(input_thread_proc);
Ok(())
}
fn input_thread_proc() {
info!("Input thread started");
let mut shmem = match create_input_shared_memory(true) {
Ok(s) => s,
Err(e) => {
error!("Could not obtain shared memory for Tasoller input: {e:#}");
return;
}
};
let usb_in = unsafe { shmem.as_slice_mut() };
let device = TASOLLER.get().unwrap();
loop {
if let Err(e) = device.read_interrupt(0x84, usb_in, Duration::from_micros(1)) {
error!("Failed to read data from Tasoller: {e:#}");
}
}
}
// Stubs. These are handled by the amdaemon dll.
#[no_mangle]
pub extern "C" fn chuni_io_jvs_init() -> HRESULT {
S_OK
}
#[no_mangle]
pub extern "C" fn chuni_io_jvs_poll(_opbtn: *mut u8, _beams: *mut u8) {}
#[no_mangle]
pub extern "C" fn chuni_io_jvs_read_coin_counter(_total: *mut u8) {}

View File

@ -0,0 +1,15 @@
[package]
name = "chuniio-tasoller-common"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
chrono = "0.4.31"
env_logger = "0.10.1"
log = "0.4.20"
rusb = "0.9.3"
shared_memory = "0.12.4"
winapi = { version = "0.3.9", features = ["debugapi"] }

View File

@ -0,0 +1,24 @@
pub mod log;
use anyhow::{anyhow, Result};
use shared_memory::{Shmem, ShmemConf, ShmemError};
pub fn create_input_shared_memory(is_owner: bool) -> Result<Shmem> {
let shmem_conf = ShmemConf::new().size(36).os_id("tasoller_input");
let shmem_result = match shmem_conf.clone().create() {
Ok(s) => Ok(s),
Err(ShmemError::MappingIdExists) => shmem_conf.open(),
Err(e) => {
return Err(anyhow!(
"Failed to create/open shared memory tasoller_input: {e:#}"
));
}
};
shmem_result
.map(|mut m| {
m.set_owner(is_owner);
m
})
.map_err(|e| anyhow!("Failed to create/open shared memory tasoller_input: {e:#}"))
}

View File

@ -0,0 +1,77 @@
use std::ffi::CString;
use std::fmt;
use std::io::Write;
use std::sync::atomic::{AtomicUsize, Ordering};
use winapi::um::debugapi::OutputDebugStringA;
#[derive(Debug)]
pub struct Logger {}
impl Write for Logger {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if let Ok(c_str) = CString::new(buf) {
unsafe { OutputDebugStringA(c_str.as_ptr()) }
}
std::io::stdout().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
std::io::stdout().flush()
}
}
struct Padded<T> {
pub value: T,
pub width: usize,
}
impl<T: fmt::Display> fmt::Display for Padded<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{: <width$}", self.value, width = self.width)
}
}
static MAX_MODULE_WIDTH: AtomicUsize = AtomicUsize::new(0);
fn max_target_width(target: &str) -> usize {
let max_width = MAX_MODULE_WIDTH.load(Ordering::Relaxed);
if max_width < target.len() {
MAX_MODULE_WIDTH.store(target.len(), Ordering::Relaxed);
target.len()
} else {
max_width
}
}
pub fn init_logger() {
env_logger::builder()
.filter_level(::log::LevelFilter::Error)
.filter_module(
"chuniio-tasoller",
if cfg!(debug_assertions) {
::log::LevelFilter::Debug
} else {
::log::LevelFilter::Info
},
)
.parse_default_env()
.target(env_logger::Target::Pipe(Box::new(Logger {})))
.format(|f, record| {
let target = record.target();
let max_width = max_target_width(target);
let level = record.level();
let mut style = f.style();
let target = style.set_bold(true).value(Padded {
value: target,
width: max_width,
});
let time = chrono::Local::now().format("%d/%m/%Y %H:%M:%S");
writeln!(f, "[{}] {} {} -> {}", time, level, target, record.args())
})
.init();
}