commit 789a437590c9101be670f2833ab29f7d2bcfbc8f Author: Serge Bazanski Date: Wed Oct 11 17:18:08 2023 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..4346b63 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +q3k's Pullstruder +=== + +PET bottle recycling machine. Based on the [Recreator3d](http://recreator3d.com/) by Joshua R. Taylor, but with a few twists. + +Work in progress. + + +Electronics +=== + +Mainboard is a Melzi 'v5.0' with an ATMega1284P. Reused from a TRONXY X1. + +Schematic of 2.0: https://reprap.org/mediawiki/images/7/7d/Melzi-circuit.png + +EXP connector: +---- + + PC1 | 1 2 | PA1 + PC0 | 3 4 | PA2 + PD3 | 5 6 | PA3 + PD2 | 7 8 | PA4 + VCC | 9 10 | GND + +LCD board +=== + +| EXP Pin | AVR Pin | AVR Function | Screen board function | +|---------|---------|--------------|-----------------------| +| 1 | PC1 | I2C SDA | LCD D7 | +| 2 | PA1 | ADC1 | Button Matrix | +| 3 | PC0 | I2C SCL | LCD D6 | +| 4 | PA2 | ADC2 | LCD EN | +| 5 | PD3 | INT1 | LCD D5 | +| 6 | PA3 | ADC3 | LCD RS | +| 7 | PD2 | INT0 | LCD D4 | +| 8 | PA4 | ADC4 | Unused? | + +Button Matrix +--- + +Based on a voltage divider between button(s) pressed and a constant pull-up to VCC. + +Resistor to VCC: 4.7k + +| Button | Resistor to GND | Voltage when pressed (VCC=5V) | ADC Value | +|--------|-----------------|-------------------------------|-----------| +| Left | 470 | 0.45V | 93 | +| Down | 1k | 0.87V | 180 | +| Menu | 2.2k | 1.6V | 326 | +| Right | 4.7k | 2.5V | 512 | +| Up | 10k | 3.4V | 697 | + + +Firmware +=== + +In progress, written in Rust. Aims to be purpose-specific and not just a repurposed 3D printer control firmware. diff --git a/firmware/.cargo/config.toml b/firmware/.cargo/config.toml new file mode 100644 index 0000000..655a1ec --- /dev/null +++ b/firmware/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "avr-specs/avr-atmega1284p.json" + +[unstable] +build-std = ["core"] diff --git a/firmware/.gitignore b/firmware/.gitignore new file mode 100644 index 0000000..bf70652 --- /dev/null +++ b/firmware/.gitignore @@ -0,0 +1,2 @@ +target +old diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock new file mode 100644 index 0000000..de14291 --- /dev/null +++ b/firmware/Cargo.lock @@ -0,0 +1,318 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "atmega-hal" +version = "0.1.0" +source = "git+https://github.com/rahix/avr-hal?rev=7b3e82a15e97e657559ec82cf934ba36c38312ec#7b3e82a15e97e657559ec82cf934ba36c38312ec" +dependencies = [ + "avr-device", + "avr-hal-generic", +] + +[[package]] +name = "atomic-polyfill" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "avr-device" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b1e02e846628f283a1c103ef5946224a07092761e4fb91fb93ab794d0b0ada" +dependencies = [ + "avr-device-macros", + "bare-metal", + "cfg-if 1.0.0", + "vcell", +] + +[[package]] +name = "avr-device-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817bb402f7da3b9e586d8f2db8a8f3f7d8fdc1562aaa4238a5ed495ebd19fc8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "avr-hal-generic" +version = "0.1.0" +source = "git+https://github.com/rahix/avr-hal?rev=7b3e82a15e97e657559ec82cf934ba36c38312ec#7b3e82a15e97e657559ec82cf934ba36c38312ec" +dependencies = [ + "avr-device", + "cfg-if 0.1.10", + "embedded-hal", + "embedded-storage", + "nb 0.1.3", + "paste", + "rustversion", + "ufmt", + "void", +] + +[[package]] +name = "bare-metal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-storage" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723dce4e9f25b6e6c5f35628e144794e5b459216ed7da97b7c4b66cdb3fa82ca" + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hd44780-driver" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab2b13fdeaed7dde9133a57c28b2cbde4a8fc8c3196b5631428aad114857d3a" +dependencies = [ + "embedded-hal", +] + +[[package]] +name = "heapless" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "panic-halt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812" + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "recreator3d-q3k-fw" +version = "1.0.0" +dependencies = [ + "atmega-hal", + "avr-device", + "embedded-hal", + "hd44780-driver", + "heapless", + "nb 0.1.3", + "panic-halt", + "ufmt", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "ufmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d3c0c63312dfc9d8e5c71114d617018a19f6058674003c0da29ee8d8036cdd" +dependencies = [ + "proc-macro-hack", + "ufmt-macros", + "ufmt-write", +] + +[[package]] +name = "ufmt-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ab6c92f30c996394a8bd525aef9f03ce01d0d7ac82d81902968057e37dd7d9" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ufmt-write" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml new file mode 100644 index 0000000..951d1c0 --- /dev/null +++ b/firmware/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "recreator3d-q3k-fw" +version = "1.0.0" +authors = ["q3k"] +edition = "2021" + +[dependencies] +panic-halt = "0.2.0" +ufmt = "0.1.0" +nb = "0.1.2" +embedded-hal = "0.2.3" +hd44780-driver = "0" +heapless = "0" + +[dependencies.avr-device] +version = "0" +features = ["rt"] + +[dependencies.atmega-hal] +git = "https://github.com/rahix/avr-hal" +rev = "7b3e82a15e97e657559ec82cf934ba36c38312ec" +features = ["atmega1284p"] + +[profile.dev] +lto = true +opt-level = "z" +panic = "abort" diff --git a/firmware/avr-specs/avr-atmega1284p.json b/firmware/avr-specs/avr-atmega1284p.json new file mode 100644 index 0000000..d3c8d98 --- /dev/null +++ b/firmware/avr-specs/avr-atmega1284p.json @@ -0,0 +1,25 @@ +{ + "arch": "avr", + "atomic-cas": false, + "cpu": "atmega1284p", + "data-layout": "e-P1-p:16:8-i8:8-i16:8-i32:8-i64:8-f32:8-f64:8-n8-a:8", + "eh-frame-header": false, + "exe-suffix": ".elf", + "executables": true, + "late-link-args": { + "gcc": [ + "-lgcc" + ] + }, + "linker": "avr-gcc", + "llvm-target": "avr-unknown-unknown", + "max-atomic-width": 8, + "no-default-libraries": false, + "pre-link-args": { + "gcc": [ + "-mmcu=atmega1284p" + ] + }, + "target-c-int-width": "16", + "target-pointer-width": "16" +} diff --git a/firmware/result b/firmware/result new file mode 120000 index 0000000..350fe7f --- /dev/null +++ b/firmware/result @@ -0,0 +1 @@ +/nix/store/4dg5g26sw712f6a9di2zslby8pz9klbd-nixos-system-mimeomia-23.11pre532072.81e8f48ebdec \ No newline at end of file diff --git a/firmware/shell.nix b/firmware/shell.nix new file mode 100644 index 0000000..bb9cd3b --- /dev/null +++ b/firmware/shell.nix @@ -0,0 +1,7 @@ +with import {}; + +pkgs.mkShell { + nativeBuildInputs = [ + pkgs.pkgsCross.avr.buildPackages.gcc + ]; +} diff --git a/firmware/src/display.rs b/firmware/src/display.rs new file mode 100644 index 0000000..fd8062d --- /dev/null +++ b/firmware/src/display.rs @@ -0,0 +1,186 @@ +use hd44780_driver::HD44780; +use embedded_hal::digital::v2::OutputPin; +use core::fmt::Write; + +use crate::hw::Delay; +use crate::logic::State; +use crate::view; + +pub struct Display { + lcd: HD44780, + delay: Delay, + layout: Layout, +} + +impl Display> +where + RS: OutputPin, EN: OutputPin, D4: OutputPin, + D5: OutputPin, D6: OutputPin, D7: OutputPin, +{ + pub fn new(rs: RS, en: EN, d4: D4, d5: D5, d6: D6, d7: D7) -> Self + { + let mut delay = Delay::new(); + let mut lcd = HD44780::new_4bit(rs, en, d4, d5, d6, d7, &mut delay).unwrap(); + lcd.reset(&mut delay).ok(); + lcd.clear(&mut delay).ok(); + lcd.set_display_mode(hd44780_driver::display_mode::DisplayMode { + cursor_visibility: hd44780_driver::Cursor::Invisible, + cursor_blink: hd44780_driver::CursorBlink::Off, + display: hd44780_driver::Display::On, + }, &mut delay).ok(); + Display { + lcd, + delay, + layout: Layout::Empty, + } + } +} + +impl Display { + fn pos(&self, x: u8, y: u8) -> u8 { + match y { + 0 => x, + 1 => x + 64, + 2 => x + 20, + 3 => x + 84, + _ => 0, + } + } + fn write_str_at(&mut self, x: usize, y: usize, s: &str) { + if x > 19 || y > 3 { + return + } + let mut len = 20 - x; + if len as usize > s.len() { + len = s.len(); + } + + let x = x as u8; + let y = y as u8; + self.lcd.set_cursor_pos(self.pos(x, y), &mut self.delay).ok(); + let len = len as usize; + self.lcd.write_str(&s[..len], &mut self.delay).ok(); + } + pub fn write(&mut self, l: Layout) { + match l { + Layout::Empty => { + if let Layout::Empty = self.layout { + return; + } + self.lcd.clear(&mut self.delay).ok(); + }, + Layout::Splash => { + self.lcd.clear(&mut self.delay).ok(); + self.write_str_at((20 - SPLASH_1.len()) / 2, 1, SPLASH_1); + self.write_str_at((20 - SPLASH_2.len()) / 2, 2, SPLASH_2); + }, + Layout::Status(ref cur) => { + let mut temperature_changed = true; + let mut time_left_changed = true; + let mut state_changed = true; + let mut speed_changed = true; + if let Layout::Status(ref prev) = &self.layout { + temperature_changed = prev.temperature != cur.temperature; + time_left_changed = prev.time_left != cur.time_left; + state_changed = prev.state != cur.state; + speed_changed = prev.speed != cur.speed; + } else { + self.lcd.clear(&mut self.delay).ok(); + self.write_str_at(0, 0, " State"); + self.write_str_at(0, 1, "Temperature"); + self.write_str_at(0, 2, " Speed"); + self.write_str_at(0, 3, " Time Left"); + } + + let mut s: heapless::String<16> = heapless::String::new(); + + if state_changed { + let s = match cur.state { + State::Idle => "Idle ", + State::Preheating => "Heating", + State::Running => "Running", + }; + self.write_str_at(12, 0, s); + } + if temperature_changed { + s.clear(); + write!(s, "{}.{}C", cur.temperature / 10, cur.temperature % 10).ok(); + while s.len() < 8 { + s.push(' ').ok(); + } + self.write_str_at(12, 1, &s); + } + if speed_changed { + s.clear(); + write!(s, "{}mm/s", cur.speed).ok(); + while s.len() < 8 { + s.push(' ').ok(); + } + self.write_str_at(12, 2, &s); + } + if time_left_changed { + s.clear(); + let h = cur.time_left / 3600; + let m = (cur.time_left / 60) % 60; + let ss = cur.time_left % 60; + write!(s, "{:02}:{:02}:{:02}", h, m, ss).ok(); + while s.len() < 8 { + s.push(' ').ok(); + } + self.write_str_at(12, 3, &s); + } + }, + Layout::Menu(ref cur) => { + let redraw_options = match &self.layout { + Layout::Menu(LayoutMenu { name, .. }) => *name != cur.name, + _ => true, + }; + let redraw_cursor = match &self.layout { + Layout::Menu(LayoutMenu { pos, .. }) => *pos != cur.pos, + _ => true, + }; + + if redraw_options { + self.lcd.clear(&mut self.delay).ok(); + for (i, option) in cur.options.iter().enumerate() { + self.write_str_at(1, i, option.label); + } + } + if redraw_cursor { + for (i, _) in cur.options.iter().enumerate() { + if i == cur.pos { + self.write_str_at(0, i, ">"); + } else { + self.write_str_at(0, i, " "); + } + } + } + }, + } + self.layout = l; + } +} + +pub struct LayoutStatus { + pub temperature: u16, + pub time_left: u32, + pub state: State, + pub speed: u16, +} + +pub struct LayoutMenu { + pub name: view::ViewID, + pub pos: usize, + pub options: &'static [view::MenuOption], +} + +pub enum Layout { + Empty, + Splash, + Status(LayoutStatus), + Menu(LayoutMenu), +} + +const SPLASH_1: &'static str = "q3k's pullstruder"; +const SPLASH_2: &'static str = "2023/10/07"; + diff --git a/firmware/src/hw.rs b/firmware/src/hw.rs new file mode 100644 index 0000000..cce6e32 --- /dev/null +++ b/firmware/src/hw.rs @@ -0,0 +1,9 @@ +use embedded_hal::blocking::delay::DelayMs; + +pub type Clock = atmega_hal::clock::MHz16; +pub type Delay = atmega_hal::delay::Delay; + +pub fn delay_ms(ms: u16) { + Delay::new().delay_ms(ms) +} + diff --git a/firmware/src/key.rs b/firmware/src/key.rs new file mode 100644 index 0000000..e6472bb --- /dev/null +++ b/firmware/src/key.rs @@ -0,0 +1,20 @@ +#[derive(Eq, PartialEq, Clone, Copy)] +pub enum Key { + Up, + Down, + Left, + Right, + Select, +} + +pub fn parse(button_adc_val: u16) -> Option { + match button_adc_val { + 80..=100 => Some(Key::Left), + 170..=190 => Some(Key::Down), + 320..=330 => Some(Key::Select), + 500..=520 => Some(Key::Right), + 690..=710 => Some(Key::Up), + _ => None, + } +} + diff --git a/firmware/src/logic.rs b/firmware/src/logic.rs new file mode 100644 index 0000000..1d8f8ef --- /dev/null +++ b/firmware/src/logic.rs @@ -0,0 +1,122 @@ +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum State { + Idle, + Preheating, + Running, +} + +#[derive(Clone)] +pub enum UserRequest { + Start, + Adjust { + speed: Option, + temperature: Option, + time: Option, + }, + Stop, +} + +pub struct Input { + pub timestamp: u32, + pub temperature: u16, + pub request: Option, +} + +pub struct Controller { + pub state: State, + deadline: u32, + configured_seconds: u32, + last_timestamp: u32, + pub speed: u16, + pub temperature: u16, +} + +impl Controller { + pub fn new() -> Self { + Self { + state: State::Idle, + deadline: 0, + configured_seconds: 3600, + last_timestamp: 0, + speed: 300, + temperature: 2137, + } + } + + pub fn time_left(&self) -> u32 { + match self.state { + State::Running => { + if self.last_timestamp > self.deadline { + return 0; + } + self.deadline - self.last_timestamp + }, + _ => self.configured_seconds, + } + } + + pub fn process(&mut self, i: &Input) { + self.last_timestamp = i.timestamp; + match self.state { + State::Idle => self.process_idle(i), + State::Preheating => self.process_preheating(i), + State::Running => self.process_running(i), + } + } + + fn apply_adjust(&mut self, speed: Option, temperature: Option, time: Option) { + if let Some(speed) = speed { + self.speed = speed; + } + if let Some(temperature) = temperature { + self.temperature = temperature; + } + if let Some(time) = time { + self.configured_seconds = time; + } + } + + fn process_idle(&mut self, i: &Input) { + if let Some(req) = &i.request { + match req { + UserRequest::Start => { + self.deadline = i.timestamp + self.configured_seconds; + self.state = State::Preheating; + }, + UserRequest::Adjust { speed, temperature, time } => { + self.apply_adjust(speed.clone(), temperature.clone(), time.clone()); + }, + UserRequest::Stop => {}, + } + } + } + + fn process_preheating(&mut self, i: &Input) { + if let Some(req) = &i.request { + match req { + UserRequest::Start {..} => {}, + UserRequest::Adjust { speed, temperature, time } => { + self.apply_adjust(speed.clone(), temperature.clone(), time.clone()); + }, + UserRequest::Stop => { + self.state = State::Idle; + }, + } + } + } + + fn process_running(&mut self, i: &Input) { + if let Some(req) = &i.request { + match req { + UserRequest::Start {..} => {}, + UserRequest::Adjust { speed, temperature, time } => { + self.apply_adjust(speed.clone(), temperature.clone(), time.clone()); + }, + UserRequest::Stop => { + self.state = State::Idle; + }, + } + } + } +} + diff --git a/firmware/src/main.rs b/firmware/src/main.rs new file mode 100644 index 0000000..7ee2875 --- /dev/null +++ b/firmware/src/main.rs @@ -0,0 +1,96 @@ +#![no_std] +#![no_main] +#![feature(abi_avr_interrupt)] + +use panic_halt as _; + +mod key; +mod view; +mod display; +mod logic; +mod hw; + +/// Seconds elapsed since startup. Overflows after 136 years of uptime. +static mut TICKS_SEC: u32 = 0; + +#[avr_device::interrupt(atmega1284p)] +fn TIMER1_COMPA() { + unsafe { + TICKS_SEC += 1; + if TICKS_SEC == 0 { + panic!("have you been running this non-stop for 136 years????"); + } + } +} + +fn ticks_sec() -> u32 { + let v = unsafe { + TICKS_SEC + }; + return v; +} + +#[avr_device::entry] +fn main() -> ! { + let dp = atmega_hal::Peripherals::take().unwrap(); + let pins = atmega_hal::pins!(dp); + + + let mut led = pins.pa4.into_output().downgrade(); + + let mut display = display::Display::new( + pins.pa3.into_output(), // RS + pins.pa2.into_output(), // EN + pins.pd2.into_output(), // D4 + pins.pd3.into_output(), // D5 + pins.pc0.into_output(), // D6 + pins.pc1.into_output(), // D7 + ); + + let mut adc = atmega_hal::Adc::::new(dp.ADC, Default::default()); + let menu_pin = pins.pa1.into_analog_input(&mut adc); + + // Configure TMR1 at 1Hz. + let tmr1 = dp.TC1; + tmr1.tccr1a.write(|w| w.wgm1().bits(0b00)); + tmr1.tccr1b.write(|w| w.cs1().prescale_1024().wgm1().bits(0b01)); + tmr1.ocr1a.write(|w| w.bits(15624)); + tmr1.timsk1.write(|w| w.ocie1a().set_bit()); + + unsafe { + avr_device::interrupt::enable(); + } + + display.write(display::Layout::Splash); + hw::delay_ms(1000); + + let mut ctrl = logic::Controller::new(); + let mut menu = view::ViewManager::new(); + let mut prev_key: Option = None; + + loop { + let ticks = ticks_sec(); + + // UI + let key = key::parse(adc.read_blocking(&menu_pin)); + let mut menu_input = view::ViewInput { + key: None, + }; + if key != prev_key { + menu_input.key = key; + }; + prev_key = key; + let req = menu.process(&menu_input); + display.write(menu.layout(&ctrl)); + + // Logic + let input = logic::Input { + timestamp: ticks, + temperature: 2137, + request: req, + }; + ctrl.process(&input); + led.toggle(); + + } +} diff --git a/firmware/src/temperature.rs b/firmware/src/temperature.rs new file mode 100644 index 0000000..adc50e3 --- /dev/null +++ b/firmware/src/temperature.rs @@ -0,0 +1,17 @@ +/// Convert from ADC value to decidegrees. +fn convert(adc: u16) -> u16 { + // The thermistor is connected via a resistor divider: + // + // VCC <---/\/\/\---+---/\/\/\----> GND + // 4.7k | NTC + // V ADC + // + // If we express V_SUP in arbitrary ADC units (0-1023), then we can + // calculate the resistance as a function of ADC value: + // + // R = 4700 / ((1024 / ADC) - 1) + + // Clamp to safe value. This prevents division by zero. + let adc = if adc >= 1024 { 1024 } else { adc}; + let adc = if adc < 1 { 1 } else { adc }; + let r = 4700 / ((1024 / adc) - 1); diff --git a/firmware/src/view.rs b/firmware/src/view.rs new file mode 100644 index 0000000..924a9bf --- /dev/null +++ b/firmware/src/view.rs @@ -0,0 +1,159 @@ +use crate::key::Key; +use crate::logic::{Controller, UserRequest}; +use crate::display::{Layout, LayoutStatus, LayoutMenu}; + +pub struct ViewInput { + pub key: Option, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum ViewID { + Status, + Main, +} + +#[derive(Clone)] +pub struct MenuAction { + goto_view: Option, + logic_request: Option, +} + +impl MenuAction { + fn nothing() -> Self { + Self { + goto_view: None, + logic_request: None, + } + } + + fn just_goto_view(vid: ViewID) -> Self { + Self { + goto_view: Some(vid), + logic_request: None, + } + } + + fn just_logic_request(req: UserRequest) -> Self { + Self { + goto_view: None, + logic_request: Some(req), + } + } +} + +pub trait View { + fn layout(&self, controller: &Controller) -> Layout; + fn process(&mut self, input: &ViewInput) -> MenuAction; + fn enter(&mut self) { } +} + +pub struct StatusView; + +impl View for StatusView { + fn layout(&self, controller: &Controller) -> Layout { + Layout::Status(LayoutStatus { + temperature: controller.temperature, + time_left: controller.time_left(), + state: controller.state, + speed: controller.speed, + }) + } + fn process(&mut self, input: &ViewInput) -> MenuAction { + if input.key.is_some() { + return MenuAction::just_goto_view(ViewID::Main); + } + return MenuAction::nothing(); + } +} + +struct Menu { + id: ViewID, + pos: usize, + options: &'static [MenuOption], +} + +pub struct MenuOption { + pub label: &'static str, + action: MenuAction, +} + +impl View for Menu { + fn layout(&self, _controller: &Controller) -> Layout { + Layout::Menu(LayoutMenu { + name: self.id, + pos: self.pos, + options: self.options, + }) + } + + fn process(&mut self, input: &ViewInput) -> MenuAction{ + match input.key { + Some(Key::Up) => { if self.pos > 0 { self.pos -= 1; } }, + Some(Key::Down) => { if self.pos < self.options.len()-1 { self.pos += 1; } }, + Some(Key::Select) => { return self.options[self.pos].action.clone() }, + _ => (), + } + return MenuAction::nothing(); + } + fn enter(&mut self) { + self.pos = 0; + } +} + +pub struct ViewManager { + status: StatusView, + main: Menu, + cur: ViewID, +} + +impl ViewManager { + pub fn new() -> Self { + Self { + status: StatusView, + main: Menu { + id: ViewID::Main, + pos: 0, + options: &[ + MenuOption { + label: "Back", + action: MenuAction { + goto_view: Some(ViewID::Status), + logic_request: None, + }, + }, + MenuOption { + label: "Start", + action: MenuAction { + goto_view: Some(ViewID::Status), + logic_request: Some(UserRequest::Start), + }, + }, + ], + }, + cur: ViewID::Status, + } + } + + pub fn layout(&self, controller: &Controller) -> Layout { + match self.cur { + ViewID::Status => self.status.layout(controller), + ViewID::Main => self.main.layout(controller), + } + } + + pub fn process(&mut self, input: &ViewInput) -> Option { + let new = match self.cur { + ViewID::Status => self.status.process(input), + ViewID::Main => self.main.process(input), + }; + if let Some(id) = new.goto_view { + self.cur = id; + match self.cur { + ViewID::Status => self.status.enter(), + ViewID::Main => self.main.enter(), + } + } + return new.logic_request; + } +} +