Adding support for os display

This commit is contained in:
Jakob Dalsgaard
2022-03-04 07:25:54 +01:00
parent 5d3f465666
commit 1501729d82
10 changed files with 1189 additions and 315 deletions

1231
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,21 +6,26 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[target.armv7-unknown-linux-gnueabihf.dependencies]
rppal = "0.13.1"
[target.x86_64-apple-darwin.dependencies]
minifb = "0.20"
[dependencies]
rppal = "0.11.3"
image = "*"
imageproc = "*"
image = "0.23.14"
imageproc = "0.22.0"
arrayref = "*"
chrono = { version = "*", features = [ "serde" ] }
chrono-tz = "*"
euclid = "*"
log = "*"
log4rs = { version = "*", features = ["file", "yaml_format", "console_appender"] }
xml-rs = "0.8"
text_io = "0.1.8"
xml-rs = "*"
text_io = "*"
serde = { version = "*", features = ["derive"] }
serde_json = "*"
rusttype = "0.9.2"
rusttype = "*"
tungstenite = { version = "*", default-features = false }
url = "*"

View File

@@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="tiny" height="1000" version="1.2" width="1000" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><line stroke="rgb(255,255,255)" stroke-width="6" x1="500.0" x2="500.0" y1="800.0" y2="900.0" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="712.132" x2="782.8427" y1="712.132" y2="782.8427" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="800.0" x2="900.0" y1="500.0" y2="500.0" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="712.132" x2="782.8427" y1="287.868" y2="217.1573" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="500.0" x2="500.0" y1="200.0" y2="100.0" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="287.868" x2="217.1573" y1="287.868" y2="217.1573" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="200.0" x2="100.0" y1="500.0" y2="500.0" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="287.868" x2="217.1573" y1="712.132" y2="782.8427" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="614.805" x2="637.766" y1="777.1639" y2="832.5966" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="777.1639" x2="832.5966" y1="614.805" y2="637.766" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="777.1639" x2="832.5966" y1="385.195" y2="362.234" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="614.805" x2="637.766" y1="222.8361" y2="167.4034" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="385.195" x2="362.234" y1="222.8361" y2="167.4034" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="222.8361" x2="167.4034" y1="385.195" y2="362.234" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="222.8361" x2="167.4034" y1="614.805" y2="637.766" /><line stroke="rgb(255,255,255)" stroke-width="6" x1="385.195" x2="362.234" y1="777.1639" y2="832.5966" /></svg>
<svg baseProfile="tiny" height="1000" version="1.2" width="1000" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><line stroke="rgb(255,255,255)" stroke-width="2" x1="500.0" x2="500.0" y1="750.0" y2="830.0" /><line stroke="rgb(255,255,255)" stroke-width="2" x1="676.7767" x2="733.3452" y1="676.7767" y2="733.3452" /><line stroke="rgb(255,255,255)" stroke-width="2" x1="750.0" x2="830.0" y1="500.0" y2="500.0" /><line stroke="rgb(255,255,255)" stroke-width="2" x1="676.7767" x2="733.3452" y1="323.2233" y2="266.6548" /><line stroke="rgb(255,255,255)" stroke-width="2" x1="500.0" x2="500.0" y1="250.0" y2="170.0" /><line stroke="rgb(255,255,255)" stroke-width="2" x1="323.2233" x2="266.6548" y1="323.2233" y2="266.6548" /><line stroke="rgb(255,255,255)" stroke-width="2" x1="250.0" x2="170.0" y1="500.0" y2="500.0" /><line stroke="rgb(255,255,255)" stroke-width="2" x1="323.2233" x2="266.6548" y1="676.7767" y2="733.3452" /><line stroke="rgb(205,205,205)" stroke-width="2" x1="595.6709" x2="618.6319" y1="730.9699" y2="786.4027" /><line stroke="rgb(205,205,205)" stroke-width="2" x1="730.9699" x2="786.4027" y1="595.6709" y2="618.6319" /><line stroke="rgb(205,205,205)" stroke-width="2" x1="730.9699" x2="786.4027" y1="404.3291" y2="381.3681" /><line stroke="rgb(205,205,205)" stroke-width="2" x1="595.6709" x2="618.6319" y1="269.0301" y2="213.5973" /><line stroke="rgb(205,205,205)" stroke-width="2" x1="404.3291" x2="381.3681" y1="269.0301" y2="213.5973" /><line stroke="rgb(205,205,205)" stroke-width="2" x1="269.0301" x2="213.5973" y1="404.3291" y2="381.3681" /><line stroke="rgb(205,205,205)" stroke-width="2" x1="269.0301" x2="213.5973" y1="595.6709" y2="618.6319" /><line stroke="rgb(205,205,205)" stroke-width="2" x1="404.3291" x2="381.3681" y1="730.9699" y2="786.4027" /><polyline points="476,10 476,82" stroke="rgb(255,255,255)" stroke-width="2" /><polyline points="476,10 524,82" stroke="rgb(255,255,255)" stroke-width="2" /><polyline points="524,10 524,82" stroke="rgb(255,255,255)" stroke-width="2" /><polyline points="524,918 508,910 492,910 476,918 476,926 484,934 516,950 524,958 524,974 508,982 492,982 476,974" stroke="rgb(205,205,205)" stroke-width="2" /><polyline points="926,460 926,532" stroke="rgb(205,205,205)" stroke-width="2" /><polyline points="926,460 974,460" stroke="rgb(205,205,205)" stroke-width="2" /><polyline points="926,492 958,492" stroke="rgb(205,205,205)" stroke-width="2" /><polyline points="926,532 974,532" stroke="rgb(205,205,205)" stroke-width="2" /><polyline points="18,460 34,532" stroke="rgb(205,205,205)" stroke-width="2" /><polyline points="50,460 34,532" stroke="rgb(205,205,205)" stroke-width="2" /><polyline points="50,460 66,532" stroke="rgb(205,205,205)" stroke-width="2" /><polyline points="82,460 66,532" stroke="rgb(205,205,205)" stroke-width="2" /></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

69
src/fbdisplay.rs Normal file
View File

@@ -0,0 +1,69 @@
use std::error::Error;
use std::thread;
use std::time::Duration;
use minifb::{Key, ScaleMode, Window, WindowOptions};
use image::RgbImage;
pub struct MiniFB {
buffer: Vec<u32>,
window: Window,
}
impl MiniFB {
// Initialize an IliDisplay struct with defaults
pub fn init() -> Result<MiniFB, Box<dyn Error>> {
let mut buffer: Vec<u32> = Vec::with_capacity(480*320);
buffer.resize(480*320, 0u32);
let mut window = Window::new(
"Helms Display ⛵️",
480, 320,
WindowOptions {
resize: true,
scale_mode: ScaleMode::AspectRatioStretch,
..WindowOptions::default()
},
).unwrap();
return Ok(MiniFB {
buffer, window
});
}
pub fn init_chip(&mut self) {
}
pub fn turn_on (&mut self) {
}
pub fn turn_off (&mut self) {
}
pub fn put_image(&mut self, image: &RgbImage, (x, y): (u32, u32)) -> () {
let (w, h) = image.dimensions();
let imagedata = image.as_raw();
let mut offset: usize = x as usize + ((y as usize)*480);
let mut i = 0;
for line in 0..h {
let toffset = offset;
for p in 0..w {
self.buffer[offset] = ((imagedata[i] as u32) << 16) | ((imagedata[i+1] as u32) << 8) | imagedata[i+2] as u32;
i = i + 3;
offset += 1;
}
offset = toffset + 480;
}
}
pub fn update(&mut self) -> () {
self.window.update_with_buffer(&self.buffer, 480, 320);
}
}

View File

@@ -1,6 +1,6 @@
use std::cmp;
use euclid::{Angle, Scale, Transform2D, Vector2D};
use image::{Rgb, RgbImage};
use image::{Rgb, GenericImage, RgbImage};
use imageproc::drawing::{draw_line_segment_mut, draw_text_mut};
use std::str::FromStr;
@@ -105,7 +105,7 @@ impl Screen {
pub fn text (&mut self, font: &Font, text: &str, scale: f32, x: u16, y: u16) {
let scale = rusttype::Scale { x: scale, y: scale };
draw_text_mut(&mut self.image, Rgb([255, 255, 255]), x as u32, y as u32, scale, &font, text);
draw_text_mut(&mut self.image, Rgb([255u8, 255u8, 255u8]), x as u32, y as u32, scale, &font, text);
}
/*

View File

@@ -155,4 +155,9 @@ impl IliDisplay {
self.spi.write(imagedata_ref).map_err(|err| println!("SPI error in send_image: {}", err.to_string())).unwrap();
}
}
pub fn update(&mut self) -> () {
}
}

View File

@@ -2,6 +2,7 @@
extern crate arrayref;
use std::error::Error;
use std::thread;
use std::time::SystemTime;
use std::time::Duration;
use std::f32::consts::PI;
use chrono::{DateTime, Utc};
@@ -19,11 +20,18 @@ use image::{
use imageproc::rect::Rect;
use imageproc::drawing::draw_filled_rect_mut;
#[cfg(target = "armv7-unknown-linux-gnueabihf")]
mod ilidisplay;
#[cfg(not(target = "armv7-unknown-linux-gnueabihf"))]
mod fbdisplay;
mod forms;
mod signalk;
mod vesseldata;
use vesseldata::VesselDataEventSource;
use vesseldata::VesselDataEvent;
use vesseldata::VesselDataState;
pub struct HelmsDisplay {
font: Font<'static>,
@@ -41,12 +49,12 @@ impl HelmsDisplay {
pub fn new() -> HelmsDisplay {
// in the future, this method should probably take some sort of
// configuration object.
let mut loader = forms::Loader::new("/root/helms-display".to_string());
let mut loader = forms::Loader::new("/Users/jdalsgaard/src/helms-display/media".to_string());
let boat = loader.load_form("boat.svg").unwrap();
let compassrose = loader.load_form("compass-rose.svg").unwrap();
let cog = loader.load_form("cog.svg").unwrap();
let wind = loader.load_form("wind.svg").unwrap();
let font = loader.load_font("font.ttf").unwrap();
let font = loader.load_font("DejaVuSans-2_37.ttf").unwrap();
HelmsDisplay {
font: font,
@@ -69,46 +77,48 @@ impl HelmsDisplay {
self.gps_screen.clear();
// format with unicodes for degrees, minutes and seconds.
let (latitude, lat_ew) = {
let (latitude, lat_ns) = {
if lat < 0.0 {
(-lat, 'W')
(-lat, 'S')
} else if lat > 0.0 {
(lat, 'E')
(lat, 'N')
} else {
(0.0, '-')
}
};
let (longitude, long_ns) = {
let (longitude, long_ew) = {
if lon < 0.0 {
(-lon, 'S')
(-lon, 'W')
} else if lon > 0.0 {
(lon, 'N')
(lon, 'E')
} else {
(0.0, '-')
}
};
let lat_d: u8 = latitude.trunc() as u8;
let latitude = latitude.fract() * 60.0;
let lat_m: u8 = latitude.trunc() as u8;
let latitude = latitude.fract() * 60.0;
let lat_s: u8 = latitude.round() as u8;
let long_d: u8 = longitude.trunc() as u8;
let longitude = longitude.fract() * 60.0;
let long_m: u8 = longitude.trunc() as u8;
let longitude = longitude.fract() * 60.0;
let long_s: u8 = longitude.round() as u8;
let lat_d: u8 = latitude.trunc() as u8;
let latitude = latitude.fract() * 60.0;
let lat_m: u8 = latitude.trunc() as u8;
let latitude = latitude.fract() * 60.0;
let lat_s: u8 = latitude.round() as u8;
self.gps_screen.text_c(&self.font,
format!("{}{:02}\u{00B0}{:02}\u{2032}{:02}\u{2033} {}{:02}\u{00B0}{:02}\u{2032}{:02}\u{2033}",
long_ns, long_d, long_m, long_s, lat_ew, lat_d, lat_m, lat_s).as_str(), 32.0, 200, 5);
lat_ns, lat_d, lat_m, lat_s, long_ew, long_d, long_m, long_s).as_str(), 32.0, 200, 5);
}
pub fn render_time (&mut self) {
let tz: Tz = "Europe/Copenhagen".parse().unwrap();
let now = Utc::now().with_timezone(&tz);
self.time_screen.clear();
self.time_screen.text_c(&self.font, now.format("%H:%M:%S %Z").to_string().as_str(), 24.0, 100, 1);
self.time_screen.text_c(&self.font, "Europe/Copenhagen", 16.0, 100, 25);
self.time_screen.text_c(&self.font, now.format("%Y-%m-%d").to_string().as_str(), 16.0, 100, 43);
@@ -156,48 +166,19 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut sog_screen = forms::Screen::new(160,60);
let mut time_screen = forms::Screen::new(200, 60);
let mut loader = forms::Loader::new("/root/helms-display".to_string());
let c = loader.load_form("compass-rose.svg").unwrap();
let b = loader.load_form("boat.svg").unwrap();
let cog = loader.load_form("cog.svg").unwrap();
let wind = loader.load_form("wind.svg").unwrap();
//let font = loader.load_font("font.ttf").unwrap();
let f2 = loader.load_font("font.ttf").unwrap();
let mut helms = HelmsDisplay::new();
/*
let mut img = RgbImage::new(480, 320);
let thickness = 10;
draw_filled_rect_mut(&mut img, Rect::at(0,0).of_size(240, 160), Rgb([255,0,0]));
draw_filled_rect_mut(&mut img, Rect::at(0,160).of_size(240, 160), Rgb([0, 255, 0]));
draw_filled_rect_mut(&mut img, Rect::at(240,0).of_size(240, 160), Rgb([0, 0, 255]));
*/
#[cfg(target = "armv7-unknown-linux-gnueabihf")]
let mut e = ilidisplay::IliDisplay::init()?;
#[cfg(not(target = "armv7-unknown-linux-gnueabihf"))]
let mut e = fbdisplay::MiniFB::init()?;
e.init_chip();
e.turn_on();
course_screen.clear();
let rad = (6 as f32) * 2.0*PI / 100.0;
course_screen.clear();
course_screen.render(&cog, -rad*2.0, 500, 500);
course_screen.render(&wind, rad*2.0, 500, 500);
course_screen.render(&c, rad, 500, 500);
course_screen.render(&b, 0.0, 500, 500);
e.put_image(&(course_screen.image), (140, (160-110)));
/*
sog_screen.clear();
sog_screen.text(&font, "SOG", 32.0, 5, 5);
sog_screen.text(&font, "speed over ground", 12.0, 5, 38);
let speed_over_ground = 5.0; // in m/s
let speed_over_ground = speed_over_ground * 1.9438612860586; // now in nautic miles per hour
sog_screen.text_rj(&font, format!("{:.1}", speed_over_ground).as_str(), 32.0, 138, 5);
sog_screen.fraction(&font, "nm", "h", 14.0, 140, 6);
e.put_image(&(sog_screen.image), (0, 50));
*/
helms.render_course(None, None, None);
e.put_image(&(helms.course_screen.image), (140, (160-110)));
helms.render_no_gps();
e.put_image(&(helms.gps_screen.image), (40,0));
@@ -207,13 +188,23 @@ fn main() -> Result<(), Box<dyn Error>> {
println!("Display has been rendered now, sleeping for 5s");
let vd = signalk::SignalK::connect();
for i in 0..9 {
let vd = signalk::SignalK::connect("109.57.77.80".to_string());
let mut state = VesselDataState::init(vd);
let mut cog: Option<f32> = None;
let mut mag: Option<f32> = None;
for i in 0..1000 {
state.read_events();
helms.render_gps(state.latitude, state.longitude);
e.put_image(&(helms.gps_screen.image), (40,0));
helms.render_course(Some(state.course_over_ground), Some(state.true_compass_course), Some(state.speed_through_water));
e.put_image(&(helms.course_screen.image), (140, (160-110)));
helms.render_time();
e.put_image(&(helms.time_screen.image), (140,260));
e.update();
let passed_millis = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().subsec_millis();
thread::sleep(Duration::from_millis(1000-passed_millis as u64))
};
thread::sleep(Duration::from_millis(15000));
e.turn_off();
Ok(())

59
src/screen.rs Normal file
View File

@@ -0,0 +1,59 @@
trait ScreenElement {
/**
* Returns whether this element has an updated value since last render.
*/
fn has_update(&self) -> bool;
/**
* Render this element and update into Display.
*/
fn update(&self, Display);
}
struct MeterElement {
label: String,
value: String,
height: u32,
width: u32,
x: u32,
y: u32,
has_update: bool,
}
impl MeterElement {
pub fn new (label: String, init_value: String, height: u32, width: u32, x: u32, y: u32) -> MeterElement {
MeterElement {
label, init_value, height, width, x, y, true
}
}
pub fn set_value (&self, new_value: String) {
if self.value == new_value {
// do nothing
return;
} else {
self.value = new_value;
self.has_update = true;
}
}
}
impl ScreenElement for MeterElement {
pub fn has_update(&self) -> bool {
self.has_update
}
pub fn update(&self, disp: Display) {
}
}

View File

@@ -77,6 +77,7 @@ impl SignalK {
"navigation.headingMagnetic" => return Some(VesselDataEvent::TrueCompassCourse(val.value.as_f64().unwrap() as f32)),
"navigation.courseOverGroundTrue" => return Some(VesselDataEvent::CourseOverGround(val.value.as_f64().unwrap() as f32)),
"navigation.speedOverGround" => return Some(VesselDataEvent::SpeedOverGround(val.value.as_f64().unwrap() as f32)),
"electrical.batteries.housebattery.voltage" => return Some(VesselDataEvent::BatteryVoltage(0, val.value.as_f64().unwrap() as f32)),
_ => return None,
}
}
@@ -84,13 +85,13 @@ impl SignalK {
impl VesselDataEventSource for SignalK {
fn connect() -> Receiver<VesselDataEvent> {
fn connect(host: String) -> Receiver<VesselDataEvent> {
let (tx, rx): (Sender<VesselDataEvent>, Receiver<VesselDataEvent>) = mpsc::channel();
let handle = thread::spawn(move || {
let (mut socket, response) = connect(Url::parse("ws://localhost:3000/signalk/v1/stream?subscribe=self").unwrap()).expect("Can't connect");
let (mut socket, response) = connect(Url::parse(format!("ws://{}:3000/signalk/v1/stream?subscribe=self", host).as_str()).unwrap()).expect("Can't connect");
let header: SignalKHeader = serde_json::from_str(socket.read_message().unwrap().to_text().unwrap()).unwrap();
if header.version != "1.33.0" {
warn!("SignalK parser has only been tested with Signal K Server version 1.33.0");
if header.version != "1.41.0" {
warn!("SignalK parser has only been tested with Signal K Server version 1.41.0");
}
loop {
let message = socket.read_message().unwrap();

View File

@@ -17,12 +17,13 @@ pub enum VesselDataEvent {
TrueCompassCourse(f32),
AISVessel(String, f32, f32, f32), // Name, lat, long, speed
BatteryLevel(u8, u8), // Bank#, percentage
BatteryVoltage(u8, f32), // Bank#, voltage
FuelLevel(u8), // percentage
}
pub trait VesselDataEventSource {
fn connect() -> Receiver<VesselDataEvent>;
fn connect(host: String) -> Receiver<VesselDataEvent>;
}
impl fmt::Debug for VesselDataEvent {
@@ -35,6 +36,7 @@ impl fmt::Debug for VesselDataEvent {
VesselDataEvent::TrueCompassCourse(c) => f.debug_tuple("TrueCompassCourse").field(c).finish(),
VesselDataEvent::AISVessel(name, lat, lon, speed) => f.debug_tuple("AISVessel").field(name).field(lat).field(lon).field(speed).finish(),
VesselDataEvent::BatteryLevel(bank, level) => f.debug_tuple("BatteryLevel").field(bank).field(level).finish(),
VesselDataEvent::BatteryVoltage(bank, voltage) => f.debug_tuple("BatteryVoltage").field(bank).field(voltage).finish(),
VesselDataEvent::FuelLevel(level) => f.debug_tuple("FuelLevel").field(level).finish(),
VesselDataEvent::WindOrigin(c) => f.debug_tuple("WindOrigin").field(c).finish(),
}
@@ -52,6 +54,7 @@ pub struct VesselDataState {
pub true_compass_course: f32,
pub course_over_ground: f32,
pub position_timestamp: u64,
pub battery_voltage: f32,
}
impl VesselDataState {
@@ -65,6 +68,7 @@ impl VesselDataState {
true_compass_course: 0.0,
course_over_ground: 0.0,
position_timestamp: 0,
battery_voltage: 0.0,
}
}
@@ -76,6 +80,7 @@ impl VesselDataState {
VesselDataEvent::Location(lat, lon) => { self.latitude = lat; self.longitude = lon; },
VesselDataEvent::CourseOverGround(c) => self.course_over_ground = c,
VesselDataEvent::TrueCompassCourse(c) => self.true_compass_course = c,
VesselDataEvent::BatteryVoltage(_, u) => self.battery_voltage = u,
_ => (),
}
}