Connect front panel to emulator

This commit is contained in:
pjht 2024-01-29 11:02:37 -06:00
parent 9e0073df41
commit 9706b052cb
Signed by: pjht
GPG Key ID: CA239FC6934E6F3A
2 changed files with 241 additions and 401 deletions

View File

@ -1,11 +1,12 @@
use std::{path::Path, sync::mpsc::Sender};
use device_query::Keycode;
use eframe::{
egui::{self, Id, Painter, Response, Sense, TextureOptions, Ui, Widget},
epaint::{pos2, vec2, Color32, Pos2, Rect, TextureHandle},
};
use crate::audio::AudioMessage;
use crate::{audio::AudioMessage, cpu::Status};
use self::switch::SwitchState;
@ -15,8 +16,27 @@ pub mod switch;
const NULL_UV: Rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
const NULL_TINT: Color32 = Color32::WHITE;
#[derive(Copy, Clone, Default)]
struct FrontpanelState {
#[derive(Clone, Copy, Debug)]
pub enum ActionSwitch {
RunStop,
SingleStep,
Examine,
Deposit,
Reset,
Protect,
Aux1,
Aux2,
}
#[derive(Clone, Copy, Debug)]
pub enum FrontpanelInteraction {
ActionSwClicked(ActionSwitch, SwitchState),
AdChanged(u16),
PowerChanged(bool),
}
#[derive(Copy, Clone)]
pub struct FrontpanelState {
ad_sws: u16,
power: bool,
runstop: SwitchState,
@ -27,27 +47,73 @@ struct FrontpanelState {
prot: SwitchState,
aux1: SwitchState,
aux2: SwitchState,
addr: u16,
data: u8,
status: Status,
}
impl FrontpanelState {
pub fn new() -> Self {
Self {
ad_sws: 0,
power: false,
runstop: SwitchState::Neut,
single_step: SwitchState::Neut,
exam: SwitchState::Neut,
dep: SwitchState::Neut,
reset: SwitchState::Neut,
prot: SwitchState::Neut,
aux1: SwitchState::Neut,
aux2: SwitchState::Neut,
addr: 0,
data: 0,
status: Status::empty(),
}
}
pub fn ad_sws(&self) -> u16 {
self.ad_sws
}
pub fn power(&self) -> bool {
self.power
}
pub fn set_addr(&mut self, addr: u16) {
self.addr = addr;
}
pub fn set_data(&mut self, data: u8) {
self.data = data;
}
pub fn set_status(&mut self, status: Status) {
self.status = status;
}
}
pub struct Frontpanel<'a> {
id: Id,
textures: &'a Textures,
audio_tx: Sender<AudioMessage>,
state: &'a mut FrontpanelState,
interaction: Option<FrontpanelInteraction>,
}
impl<'a> Frontpanel<'a> {
pub fn new(id: Id, textures: &'a Textures, audio_tx: Sender<AudioMessage>) -> Self {
pub fn new(textures: &'a Textures, state: &'a mut FrontpanelState) -> Self {
Self {
id,
textures,
audio_tx,
}
state,
interaction: None,
}
}
impl Widget for Frontpanel<'_> {
pub fn interaction(&self) -> Option<FrontpanelInteraction> {
self.interaction
}
}
impl Widget for &mut Frontpanel<'_> {
fn ui(self, ui: &mut Ui) -> egui::Response {
let mut state: FrontpanelState = ui.data(|data| data.get_temp(self.id).unwrap_or_default());
let sw_textures = switch::Textures::new(
self.textures.sw_up.clone(),
self.textures.sw_neut.clone(),
@ -68,7 +134,14 @@ impl Widget for Frontpanel<'_> {
ui.image(&self.textures.fp);
for led in &LEDS {
let pos = led.pos + fp_rect.left_top().to_vec2();
let led_data = 0xAAAA;
let led_data = match led.source {
LedSource::Protect => 0x0,
LedSource::Iff => 0x0,
LedSource::Run => 0x0,
LedSource::CpuStatus => u16::from(self.state.status.bits()),
LedSource::Data => u16::from(self.state.data),
LedSource::Address => self.state.addr,
};
let led_on = (led_data & led.mask) > 0;
ui.allocate_ui_at_rect(Rect::from_center_size(pos, vec2(16.0, 16.0)), |ui| {
ui.add(led::Led::new(led_on, &led_textures));
@ -81,44 +154,42 @@ impl Widget for Frontpanel<'_> {
let pos = switch.pos + fp_rect.left_top().to_vec2();
if i == 0 {
ui.allocate_ui_at_rect(Rect::from_center_size(pos, vec2(11.0, 25.0)), |ui| {
let mut power_inv = !state.power;
let mut power_inv = !self.state.power;
if ui
.add(switch::ToggleSwitch::new(&mut power_inv, &sw_textures))
.drag_started()
{
if !power_inv {
self.audio_tx.send(AudioMessage::FanOn).unwrap();
} else {
self.audio_tx.send(AudioMessage::FanOff).unwrap();
}
self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap();
self.state.power = !power_inv;
self.interaction =
Some(FrontpanelInteraction::PowerChanged(self.state.power));
};
state.power = !power_inv;
});
}
if (1..17).contains(&i) {
let bit_mask = 1 << (16 - i);
let mut sw_state = state.ad_sws & bit_mask > 0;
let mut sw_state = self.state.ad_sws & bit_mask > 0;
ui.allocate_ui_at_rect(Rect::from_center_size(pos, vec2(11.0, 25.0)), |ui| {
if ui
.add(switch::ToggleSwitch::new(&mut sw_state, &sw_textures))
.drag_started()
{
self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap();
self.state.ad_sws =
(self.state.ad_sws & !(bit_mask)) | ((sw_state as u16) << (16 - i));
self.interaction =
Some(FrontpanelInteraction::AdChanged(self.state.ad_sws));
};
});
state.ad_sws = (state.ad_sws & !(bit_mask)) | ((sw_state as u16) << (16 - i));
}
if (17..25).contains(&i) {
let state = match i {
17 => &mut state.runstop,
18 => &mut state.single_step,
19 => &mut state.exam,
20 => &mut state.dep,
21 => &mut state.reset,
22 => &mut state.prot,
23 => &mut state.aux1,
24 => &mut state.aux2,
17 => &mut self.state.runstop,
18 => &mut self.state.single_step,
19 => &mut self.state.exam,
20 => &mut self.state.dep,
21 => &mut self.state.reset,
22 => &mut self.state.prot,
23 => &mut self.state.aux1,
24 => &mut self.state.aux2,
_ => unreachable!(),
};
ui.allocate_ui_at_rect(Rect::from_center_size(pos, vec2(11.0, 25.0)), |ui| {
@ -126,13 +197,24 @@ impl Widget for Frontpanel<'_> {
.add(switch::ThreePosSwitch::new(state, &sw_textures))
.drag_started()
{
self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap();
let sw = match i {
17 => ActionSwitch::RunStop,
18 => ActionSwitch::SingleStep,
19 => ActionSwitch::Examine,
20 => ActionSwitch::Deposit,
21 => ActionSwitch::Reset,
22 => ActionSwitch::Protect,
23 => ActionSwitch::Aux1,
24 => ActionSwitch::Aux2,
_ => unreachable!(),
};
self.interaction =
Some(FrontpanelInteraction::ActionSwClicked(sw, *state));
};
});
}
}
});
ui.data_mut(|data| data.insert_temp(self.id, state));
resp
}
}

View File

@ -10,7 +10,7 @@ use audio::{AudioMessage, AudioThread};
use cpu::{MemCycle, Status, I8080};
use device_query::DeviceState;
use eframe::{
egui::{self, menu, Button, Id, Label, Pos2, Rect, TextureHandle, TextureOptions, Ui},
egui::{self, menu, Button, Label, TextureHandle, TextureOptions},
NativeOptions,
};
use egui_modal::Modal;
@ -18,7 +18,7 @@ use rand::RngCore;
use rfd::FileDialog;
use serde::{Deserialize, Serialize};
use crate::frontpanel::{switch, Frontpanel};
use crate::frontpanel::{switch::SwitchState, Frontpanel, FrontpanelInteraction, FrontpanelState};
fn main() -> Result<(), eframe::Error> {
env_logger::init();
@ -37,23 +37,8 @@ struct Options {
struct AltairEmulator {
textures: frontpanel::Textures,
ad_sws: u16,
power: bool,
runstop: SwitchState,
single_step: SwitchState,
exam: SwitchState,
dep: switch::SwitchState,
reset: SwitchState,
prot: SwitchState,
aux1: SwitchState,
aux2: SwitchState,
fp_address: u16,
fp_data: u8,
fp_status: Status,
mem: [u8; 65536],
cpu: I8080,
mouse_newdown: bool,
mouse_olddown: bool,
running: bool,
audio_tx: Sender<AudioMessage>,
options: Options,
@ -61,6 +46,7 @@ struct AltairEmulator {
device_state: DeviceState,
kbd_newdown: bool,
kbd_olddown: bool,
fp_state: FrontpanelState,
}
impl AltairEmulator {
@ -103,23 +89,8 @@ impl AltairEmulator {
// 21: 005
Self {
textures: frontpanel::Textures::new(&cc.egui_ctx),
ad_sws: 0x0,
power: false,
runstop: SwitchState::Neut,
single_step: SwitchState::Neut,
exam: SwitchState::Neut,
dep: switch::SwitchState::Neut,
reset: SwitchState::Neut,
prot: SwitchState::Neut,
aux1: SwitchState::Neut,
aux2: SwitchState::Neut,
mem,
fp_address: 0,
fp_data: 0,
fp_status: Status::empty(),
cpu,
mouse_newdown: false,
mouse_olddown: false,
running: false,
audio_tx,
options,
@ -127,55 +98,62 @@ impl AltairEmulator {
device_state: DeviceState::new(),
kbd_newdown: false,
kbd_olddown: false,
fp_state: FrontpanelState::new(),
}
}
fn update_fp(&mut self) {
let cycle = self.cpu.get_mem_cycle();
self.fp_status = cycle.get_status();
self.fp_state.set_status(cycle.get_status());
match cycle {
MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => {
self.fp_address = a;
self.fp_data = self.mem[a as usize];
self.fp_state.set_addr(a);
self.fp_state.set_data(self.mem[a as usize]);
}
MemCycle::Write(a, _) | MemCycle::StackWrite(a, _) | MemCycle::Out(a, _) => {
self.fp_address = a;
self.fp_data = 0xff;
self.fp_state.set_addr(a);
self.fp_state.set_data(0xff);
}
MemCycle::In(a) => {
self.fp_address = a;
self.fp_data = 0;
self.fp_state.set_addr(a);
self.fp_state.set_data(0);
}
MemCycle::Inta(_) => todo!(),
MemCycle::Hlta(_) => {
self.fp_data = 0xff;
self.fp_address = 0xffff;
self.fp_state.set_addr(0xffff);
self.fp_state.set_data(0xff);
}
MemCycle::IntaHlt(_) => todo!(),
}
}
fn run_cpu_cycle(&mut self) {
let cycle = self.cpu.get_mem_cycle();
let data = match cycle {
MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => self.mem[a as usize],
MemCycle::Write(a, d) | MemCycle::StackWrite(a, d) => {
self.mem[a as usize] = d;
0
}
MemCycle::In(_) => 0,
MemCycle::Out(_, _) => 0,
MemCycle::Inta(_) => todo!(),
MemCycle::Hlta(_) => {
self.running = false;
0
}
MemCycle::IntaHlt(_) => todo!(),
};
self.cpu.finish_m_cycle(data);
self.update_fp();
}
}
impl eframe::App for AltairEmulator {
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, "options", &self.options);
}
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
fn image_topleft(pos: impl Into<Pos2>, texture: &TextureHandle, ui: &mut Ui) {
ui.allocate_ui_at_rect(Rect::from_min_size(pos.into(), texture.size_vec2()), |ui| {
ui.image(texture);
});
}
fn image_center(pos: impl Into<Pos2>, texture: &TextureHandle, ui: &mut Ui) {
ui.allocate_ui_at_rect(
Rect::from_center_size(pos.into(), texture.size_vec2()),
|ui| {
ui.image(texture);
},
);
}
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.set_pixels_per_point(1.0);
// frame.set_window_size((800.0, 333.0).into());
let mut disable_fp_sws = self.option_window.is_some();
@ -226,17 +204,84 @@ impl eframe::App for AltairEmulator {
});
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.style_mut().debug.debug_on_hover = true;
ui.add(Frontpanel::new(
Id::new("frontpanel"),
&self.textures,
self.audio_tx.clone(),
));
ui.add(Frontpanel::new(
Id::new("frontpanel2"),
&self.textures,
self.audio_tx.clone(),
));
let mut fp = Frontpanel::new(&self.textures, &mut self.fp_state);
ui.add(&mut fp);
let interaction = fp.interaction();
if let Some(interaction) = interaction {
self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap();
match interaction {
FrontpanelInteraction::ActionSwClicked(sw, state) => {
if self.fp_state.power() {
match sw {
frontpanel::ActionSwitch::RunStop => {
if state == SwitchState::Up {
self.running = false;
} else {
self.running = true;
}
}
frontpanel::ActionSwitch::SingleStep => {
self.run_cpu_cycle();
}
frontpanel::ActionSwitch::Examine => {
if state == SwitchState::Up {
// Assume M1
self.cpu.finish_m_cycle(0xC3); // JMP
self.cpu.finish_m_cycle(self.fp_state.ad_sws() as u8);
self.cpu
.finish_m_cycle((self.fp_state.ad_sws() >> 8) as u8);
self.update_fp();
} else {
// Assume M1
self.cpu.finish_m_cycle(0x0); // NOP
self.update_fp();
}
}
frontpanel::ActionSwitch::Deposit => {
if state == SwitchState::Up {
// Assume M1
self.mem[self.cpu.get_mem_cycle().address() as usize] =
self.fp_state.ad_sws() as u8;
self.update_fp();
} else {
// Assume M1
self.cpu.finish_m_cycle(0x0); // NOP
self.mem[self.cpu.get_mem_cycle().address() as usize] =
self.fp_state.ad_sws() as u8;
self.update_fp();
}
}
frontpanel::ActionSwitch::Reset => {
if state == SwitchState::Up {
self.cpu.reset();
self.update_fp();
}
}
frontpanel::ActionSwitch::Protect => (),
frontpanel::ActionSwitch::Aux1 => (),
frontpanel::ActionSwitch::Aux2 => (),
}
}
}
FrontpanelInteraction::AdChanged(_) => (),
FrontpanelInteraction::PowerChanged(pwr) => {
if pwr {
self.audio_tx.send(AudioMessage::FanOn).unwrap();
self.cpu = I8080::new();
self.update_fp();
} else {
self.audio_tx.send(AudioMessage::FanOff).unwrap();
self.running = false;
self.fp_state.set_status(Status::empty());
self.fp_state.set_addr(0);
self.fp_state.set_data(0);
}
}
}
}
if self.running {
self.run_cpu_cycle();
}
// dbg!(ui.input(|input| input.pointer.latest_pos()));
// dbg!(ui.input(|input| input.pointer.latest_pos()));
// let (_, fp_rect) = ui.allocate_space((800.0, 333.0).into());
@ -589,19 +634,21 @@ impl eframe::App for AltairEmulator {
// self.cpu.finish_m_cycle(data);
// self.update_fp();
// }
//
ui.add(Label::new("Hello"));
});
// let old_fan_enabled = self.options.fan_enabled;
let old_fan_enabled = self.options.fan_enabled;
if let Some(option_window) = self.option_window.as_mut() {
if option_window.draw(ctx, &mut self.options) {
self.option_window = None;
}
}
// if (old_fan_enabled != self.options.fan_enabled) && self.power {
// self.fan_audio_tx.send(self.options.fan_enabled).unwrap();
// }
if (old_fan_enabled != self.options.fan_enabled) && self.fp_state.power() {
if self.options.fan_enabled {
self.audio_tx.send(AudioMessage::FanOn).unwrap();
} else {
self.audio_tx.send(AudioMessage::FanOff).unwrap();
}
}
if self.running {
ctx.request_repaint();
}
@ -721,292 +768,3 @@ impl Textures {
Ok(ctx.load_texture(name, image, TextureOptions::LINEAR))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SwitchState {
Up,
Neut,
Down,
}
#[derive(Clone, Copy, Debug)]
struct SwitchInfo {
pos: Pos2,
}
static SWITCHES: [SwitchInfo; 25] = [
SwitchInfo {
pos: Pos2::new(43.0, 234.0),
},
SwitchInfo {
pos: Pos2::new(183.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(228.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(257.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(285.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(330.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(359.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(388.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(433.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(461.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(491.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(536.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(564.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(593.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(638.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(667.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(696.0, 175.0),
},
SwitchInfo {
pos: Pos2::new(190.0, 234.0),
},
SwitchInfo {
pos: Pos2::new(248.0, 234.0),
},
SwitchInfo {
pos: Pos2::new(305.0, 234.0),
},
SwitchInfo {
pos: Pos2::new(363.0, 234.0),
},
SwitchInfo {
pos: Pos2::new(420.0, 234.0),
},
SwitchInfo {
pos: Pos2::new(478.0, 234.0),
},
SwitchInfo {
pos: Pos2::new(535.0, 234.0),
},
SwitchInfo {
pos: Pos2::new(593.0, 234.0),
},
];
#[derive(Clone, Copy, Debug)]
enum LedSource {
Protect,
Iff,
Run,
CpuStatus,
Data,
Address,
}
struct LedInfo {
pos: Pos2,
source: LedSource,
mask: u16,
}
static LEDS: [LedInfo; 36] = [
LedInfo {
pos: Pos2::new(118.0, 58.0),
source: LedSource::Protect,
mask: 0xFF,
},
LedInfo {
pos: Pos2::new(89.0, 58.0),
source: LedSource::Iff,
mask: 0xFF,
},
LedInfo {
pos: Pos2::new(89.0, 117.0),
source: LedSource::Run,
mask: 0x0,
},
LedInfo {
pos: Pos2::new(118.0, 117.0),
source: LedSource::Iff,
mask: 0x0,
},
LedInfo {
pos: Pos2::new(149.0, 58.0),
source: LedSource::CpuStatus,
mask: 0x80,
},
LedInfo {
pos: Pos2::new(179.0, 58.0),
source: LedSource::CpuStatus,
mask: 0x40,
},
LedInfo {
pos: Pos2::new(209.0, 58.0),
source: LedSource::CpuStatus,
mask: 0x20,
},
LedInfo {
pos: Pos2::new(239.0, 58.0),
source: LedSource::CpuStatus,
mask: 0x10,
},
LedInfo {
pos: Pos2::new(269.0, 58.0),
source: LedSource::CpuStatus,
mask: 0x8,
},
LedInfo {
pos: Pos2::new(299.0, 58.0),
source: LedSource::CpuStatus,
mask: 0x4,
},
LedInfo {
pos: Pos2::new(329.0, 58.0),
source: LedSource::CpuStatus,
mask: 0x2,
},
LedInfo {
pos: Pos2::new(359.0, 58.0),
source: LedSource::CpuStatus,
mask: 0x1,
},
LedInfo {
pos: Pos2::new(461.0, 58.0),
source: LedSource::Data,
mask: 0x80,
},
LedInfo {
pos: Pos2::new(491.0, 58.0),
source: LedSource::Data,
mask: 0x40,
},
LedInfo {
pos: Pos2::new(536.0, 58.0),
source: LedSource::Data,
mask: 0x20,
},
LedInfo {
pos: Pos2::new(564.0, 58.0),
source: LedSource::Data,
mask: 0x10,
},
LedInfo {
pos: Pos2::new(593.0, 58.0),
source: LedSource::Data,
mask: 0x8,
},
LedInfo {
pos: Pos2::new(638.0, 58.0),
source: LedSource::Data,
mask: 0x4,
},
LedInfo {
pos: Pos2::new(667.0, 58.0),
source: LedSource::Data,
mask: 0x2,
},
LedInfo {
pos: Pos2::new(696.0, 58.0),
source: LedSource::Data,
mask: 0x1,
},
LedInfo {
pos: Pos2::new(182.0, 117.0),
source: LedSource::Address,
mask: 0x8000,
},
LedInfo {
pos: Pos2::new(227.0, 117.0),
source: LedSource::Address,
mask: 0x4000,
},
LedInfo {
pos: Pos2::new(256.0, 117.0),
source: LedSource::Address,
mask: 0x2000,
},
LedInfo {
pos: Pos2::new(285.0, 117.0),
source: LedSource::Address,
mask: 0x1000,
},
LedInfo {
pos: Pos2::new(330.0, 117.0),
source: LedSource::Address,
mask: 0x800,
},
LedInfo {
pos: Pos2::new(359.0, 117.0),
source: LedSource::Address,
mask: 0x400,
},
LedInfo {
pos: Pos2::new(388.0, 117.0),
source: LedSource::Address,
mask: 0x200,
},
LedInfo {
pos: Pos2::new(433.0, 117.0),
source: LedSource::Address,
mask: 0x100,
},
LedInfo {
pos: Pos2::new(461.0, 117.0),
source: LedSource::Address,
mask: 0x80,
},
LedInfo {
pos: Pos2::new(490.0, 117.0),
source: LedSource::Address,
mask: 0x40,
},
LedInfo {
pos: Pos2::new(536.0, 117.0),
source: LedSource::Address,
mask: 0x20,
},
LedInfo {
pos: Pos2::new(564.0, 117.0),
source: LedSource::Address,
mask: 0x10,
},
LedInfo {
pos: Pos2::new(593.0, 117.0),
source: LedSource::Address,
mask: 0x8,
},
LedInfo {
pos: Pos2::new(638.0, 117.0),
source: LedSource::Address,
mask: 0x4,
},
LedInfo {
pos: Pos2::new(667.0, 117.0),
source: LedSource::Address,
mask: 0x2,
},
LedInfo {
pos: Pos2::new(696.0, 117.0),
source: LedSource::Address,
mask: 0x1,
},
];