Cleanup code and add simple README
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -42,6 +42,7 @@ dependencies = [
|
|||||||
"cortex-m 0.7.4",
|
"cortex-m 0.7.4",
|
||||||
"cortex-m-rt",
|
"cortex-m-rt",
|
||||||
"embedded-graphics",
|
"embedded-graphics",
|
||||||
|
"embedded-time",
|
||||||
"panic-halt",
|
"panic-halt",
|
||||||
"panic-rtt-target",
|
"panic-rtt-target",
|
||||||
"rtt-target 0.2.2",
|
"rtt-target 0.2.2",
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ stm32f3xx-hal = { version = "0.9.0", features = ["ld", "rt", "stm32f303xe"] }
|
|||||||
rtt-target = { version = "0.2.2", features = ["cortex-m"] }
|
rtt-target = { version = "0.2.2", features = ["cortex-m"] }
|
||||||
panic-rtt-target = { version = "0.1.1", features = ["cortex-m"] }
|
panic-rtt-target = { version = "0.1.1", features = ["cortex-m"] }
|
||||||
ssd1306 = "0.7.0"
|
ssd1306 = "0.7.0"
|
||||||
embedded-graphics = "0.7.1"
|
embedded-graphics = "0.7.1"
|
||||||
|
embedded-time = "0.12.1"
|
||||||
24
README.org
24
README.org
@@ -1,11 +1,29 @@
|
|||||||
#+TITLE: bad-apple-embedded-rs
|
#+TITLE: bad-apple-embedded-rs
|
||||||
|
|
||||||
|
#+html: <p align="center" width="300"><img src="assets/bad_apple.jpg" /></p>
|
||||||
|
|
||||||
|
Why? Because I wanted to and because Rust rocks!
|
||||||
|
|
||||||
* Features
|
* Features
|
||||||
|
This project was built using an =STM32-F303ZE= with a simple =ssd1306= OLED display.
|
||||||
|
It draws 21x10 with (almost perfectly) stable 8 frames per second and
|
||||||
|
was designed and built to play Bad Apple, but anything else would also work.
|
||||||
|
Frame timings and length of the video can be adjusted, too high frames per second can lead to slowed down playback due to the render time being larger than the desired one.
|
||||||
|
Faster render times are being adjusted for.
|
||||||
|
|
||||||
|
* How to use
|
||||||
|
1. Convert a video to an ASCII file, using =video-to-ascii.py=
|
||||||
|
2. Adjust frame timing in the main program (draw time per picture, width, height, total frames)
|
||||||
|
3. Flash and enjoy!
|
||||||
|
|
||||||
|
* Input format
|
||||||
|
The screen renders video through ASCII characters. It takes in a long text file, containing all the images in a contiguous stream, which is then read in =IMAGE_LEN=-sized (height * width + newline chars) chunks in the main program.
|
||||||
|
If these parameters don't match up, the end result won't either.
|
||||||
|
|
||||||
* Dependencies
|
* Dependencies
|
||||||
The video-to-ascii conversion script uses [[https://github.com/ivanl-exe/image-to-ascii/][image-to-ascii]] for the conversion in the background.
|
The =video-to-ascii.py= conversion script uses [[https://github.com/ivanl-exe/image-to-ascii/][image-to-ascii]] for the conversion in the background.
|
||||||
It is assumed to be located on the same height as this project and that it is build in release-mode (due to performance).
|
It is assumed to be located on the same height as this project and that it is build in release-mode (due to performance).
|
||||||
|
|
||||||
* TODO
|
* TODO
|
||||||
- [ ] Add README Stuff
|
- [ ] Fix timing in draw
|
||||||
- [ ] Rewrite ascii conversion in Rust (with an own impl of ascii-to-text)
|
- [ ] Rewrite ascii conversion in Rust or Haskell (with an own implementation of =ascii-to-text=)
|
||||||
|
|||||||
BIN
assets/bad_apple.jpg
Normal file
BIN
assets/bad_apple.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
64
src/main.rs
64
src/main.rs
@@ -1,42 +1,47 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use cortex_m::prelude::*;
|
use cortex_m::delay::Delay;
|
||||||
use cortex_m_rt::entry;
|
use cortex_m_rt::entry;
|
||||||
|
use embedded_graphics::mono_font::MonoTextStyleBuilder;
|
||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
mono_font::ascii::FONT_4X6,
|
mono_font::ascii::FONT_4X6,
|
||||||
pixelcolor::BinaryColor,
|
pixelcolor::BinaryColor,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
text::{Baseline, Text},
|
text::{Baseline, Text},
|
||||||
};
|
};
|
||||||
use panic_halt as _;
|
use embedded_time::fixed_point::FixedPoint as _;
|
||||||
|
use hal::{i2c::I2c, timer::MonoTimer};
|
||||||
|
use pac::{CorePeripherals, Peripherals};
|
||||||
|
use panic_rtt_target as _;
|
||||||
use rtt_target::{rprintln, rtt_init_print};
|
use rtt_target::{rprintln, rtt_init_print};
|
||||||
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
|
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
|
||||||
use stm32f3xx_hal::{self as hal, pac, prelude::*};
|
|
||||||
use stm32f3xx_hal::prelude::_embedded_hal_digital_InputPin;
|
use stm32f3xx_hal::prelude::_embedded_hal_digital_InputPin;
|
||||||
|
use stm32f3xx_hal::{self as hal, pac, prelude::*};
|
||||||
|
|
||||||
const IMAGE_LEN: usize = 220; // size of a single ascii image on screen
|
const IMAGE_WIDTH: usize = 21;
|
||||||
const IMAGE_END: usize = 1093 * 220; // position of the last ascii char
|
const IMAGE_HEIGHT: usize = 10;
|
||||||
|
const TOTAL_FRAMES: usize = 1749;
|
||||||
|
const DRAW_TIME: usize = 125; // ms for a single screen draw
|
||||||
|
|
||||||
|
const IMAGE_LEN: usize = IMAGE_HEIGHT * (IMAGE_WIDTH + 1); // adjust for newline char
|
||||||
|
const IMAGE_END: usize = TOTAL_FRAMES * IMAGE_LEN; // position of the last ascii char
|
||||||
|
|
||||||
#[entry]
|
#[entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
rtt_init_print!();
|
rtt_init_print!();
|
||||||
let peripherals = pac::Peripherals::take().unwrap();
|
let peripherals = Peripherals::take().unwrap();
|
||||||
let mut core_peripherals = pac::CorePeripherals::take().unwrap();
|
let mut core_peripherals = CorePeripherals::take().unwrap();
|
||||||
|
|
||||||
let mut rcc = peripherals.RCC.constrain();
|
let mut rcc = peripherals.RCC.constrain();
|
||||||
let mut flash = peripherals.FLASH.constrain();
|
let mut flash = peripherals.FLASH.constrain();
|
||||||
let clocks = rcc.cfgr.freeze(&mut flash.acr);
|
let clocks = rcc.cfgr.freeze(&mut flash.acr);
|
||||||
let mut gpiob = peripherals.GPIOB.split(&mut rcc.ahb);
|
let mut gpiob = peripherals.GPIOB.split(&mut rcc.ahb);
|
||||||
let mut gpioc = peripherals.GPIOC.split(&mut rcc.ahb);
|
let mut gpioc = peripherals.GPIOC.split(&mut rcc.ahb);
|
||||||
let monotimer = hal::timer::MonoTimer::new(
|
let monotimer = MonoTimer::new(core_peripherals.DWT, clocks, &mut core_peripherals.DCB);
|
||||||
core_peripherals.DWT,
|
let mut delay_provider = Delay::new(core_peripherals.SYST, clocks.hclk().integer());
|
||||||
clocks,
|
|
||||||
&mut core_peripherals.DCB);
|
|
||||||
// TODO: dont use hardcoded val here
|
|
||||||
let mut delay = cortex_m::delay::Delay::new(core_peripherals.SYST, 8000000);
|
|
||||||
|
|
||||||
let mut button1 = gpioc
|
let button1 = gpioc
|
||||||
.pc13
|
.pc13
|
||||||
.into_pull_down_input(&mut gpioc.moder, &mut gpioc.pupdr);
|
.into_pull_down_input(&mut gpioc.moder, &mut gpioc.pupdr);
|
||||||
|
|
||||||
@@ -47,7 +52,7 @@ fn main() -> ! {
|
|||||||
.pb9
|
.pb9
|
||||||
.into_af_open_drain(&mut gpiob.moder, &mut gpiob.otyper, &mut gpiob.afrh);
|
.into_af_open_drain(&mut gpiob.moder, &mut gpiob.otyper, &mut gpiob.afrh);
|
||||||
|
|
||||||
let i2c = hal::i2c::I2c::new(
|
let i2c = I2c::new(
|
||||||
peripherals.I2C1,
|
peripherals.I2C1,
|
||||||
(scl, sda),
|
(scl, sda),
|
||||||
1000.kHz().try_into().unwrap(),
|
1000.kHz().try_into().unwrap(),
|
||||||
@@ -60,40 +65,47 @@ fn main() -> ! {
|
|||||||
.into_buffered_graphics_mode();
|
.into_buffered_graphics_mode();
|
||||||
display.init().unwrap();
|
display.init().unwrap();
|
||||||
|
|
||||||
let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new()
|
let text_style = MonoTextStyleBuilder::new()
|
||||||
.font(&FONT_4X6)
|
.font(&FONT_4X6)
|
||||||
.text_color(BinaryColor::On)
|
.text_color(BinaryColor::On)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let bad_apple = core::str::from_utf8(include_bytes!("../assets/gen.txt")).unwrap();
|
let ascii_txt = core::str::from_utf8(include_bytes!("../assets/ascii.txt")).unwrap();
|
||||||
|
|
||||||
// start indexing at 0, draw IMAGE_LEN ascii chars to display
|
// start indexing at 0, draw IMAGE_LEN ascii chars to display
|
||||||
let mut index: usize = 0;
|
let mut index: usize = 0;
|
||||||
loop {
|
loop {
|
||||||
let instant = monotimer.now();
|
let monotimer_instant = monotimer.now();
|
||||||
|
|
||||||
display.clear();
|
display.clear();
|
||||||
|
|
||||||
rprintln!("draw: {} to {}", index, index + IMAGE_LEN);
|
|
||||||
let text = Text::with_baseline(
|
let text = Text::with_baseline(
|
||||||
&bad_apple[index..index + IMAGE_LEN],
|
&ascii_txt[index..index + IMAGE_LEN],
|
||||||
Point::new(0, 0),
|
Point::new(0, 0),
|
||||||
text_style,
|
text_style,
|
||||||
Baseline::Top,
|
Baseline::Top,
|
||||||
);
|
);
|
||||||
text.draw(&mut display).unwrap();
|
text.draw(&mut display).unwrap();
|
||||||
|
// draw out to physical screen
|
||||||
display.flush().unwrap();
|
display.flush().unwrap();
|
||||||
|
|
||||||
// go to next frame or reset to start
|
// go to next frame or reset to start
|
||||||
index = (index + IMAGE_LEN) % IMAGE_END;
|
index = (index + IMAGE_LEN) % IMAGE_END;
|
||||||
|
|
||||||
let elapsed = instant.elapsed();
|
// adjust for desired framerate
|
||||||
rprintln!("draw took: {}", elapsed);
|
// TODO: chip is too fast for some reason
|
||||||
rprintln!("freq: {}", monotimer.frequency());
|
let freq = monotimer.frequency().0;
|
||||||
delay.delay_ms(100);
|
// maybe use float calc here?
|
||||||
|
let ms_per_draw = 1000 / (freq / monotimer_instant.elapsed());
|
||||||
|
rprintln!("Draw took {}ms", ms_per_draw);
|
||||||
|
match DRAW_TIME.checked_sub(ms_per_draw as usize) {
|
||||||
|
Some(result) => delay_provider.delay_ms(result as u32),
|
||||||
|
None => (),
|
||||||
|
};
|
||||||
|
|
||||||
// kill switch
|
// reset
|
||||||
if button1.is_high().unwrap() {
|
if button1.is_high().unwrap() {
|
||||||
loop {}
|
index = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
"""
|
"""
|
||||||
Convert a video to a txt in a format, which i need
|
Convert a video to a textfile in a format, which i need:
|
||||||
|
|
||||||
|
ASCII_TXT:
|
||||||
|
---
|
||||||
|
IMAGE
|
||||||
|
IMAGE
|
||||||
|
IMAGE
|
||||||
|
...
|
||||||
|
---
|
||||||
|
|
||||||
Using: https://github.com/ivanl-exe/image-to-ascii/
|
Using: https://github.com/ivanl-exe/image-to-ascii/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
OUTPUT = 'assets/output'
|
OUTPUT = 'assets/output'
|
||||||
ASCII_TXT = 'assets/ascii.txt'
|
ASCII_TXT = 'assets/ascii.txt'
|
||||||
|
TOOL_PATH = '../image-to-ascii/target/release/image_to_ascii'
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
"""
|
"""
|
||||||
@@ -22,6 +35,7 @@ def parse_args():
|
|||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""
|
"""
|
||||||
Convert frames to jpgs, convert each to ascii, and append it to the ascii.txt
|
Convert frames to jpgs, convert each to ascii, and append it to the ascii.txt
|
||||||
@@ -59,11 +73,21 @@ def main():
|
|||||||
for f in os.listdir(OUTPUT):
|
for f in os.listdir(OUTPUT):
|
||||||
filename = os.path.abspath(OUTPUT + '/' + f)
|
filename = os.path.abspath(OUTPUT + '/' + f)
|
||||||
print(f'Processing: {filename}')
|
print(f'Processing: {filename}')
|
||||||
os.system(f'../image-to-ascii/target/release/image_to_ascii {args.width} {args.height} true {filename} >> {ASCII_TXT}')
|
os.system(f'{TOOL_PATH} {args.width} {args.height} true {filename} >> {ASCII_TXT}')
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
video.release()
|
video.release()
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
|
||||||
|
def check_deps():
|
||||||
|
"""
|
||||||
|
Checks if the deps, used in this script, are present
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(TOOL_PATH):
|
||||||
|
print("Missing image-to-ascii at expected location!")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
check_deps()
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user