Major rewrites, signalk support almost there

Font and forms refactored, but not working yet, will explore
Rusttype lifetime issues in seperate project
This commit is contained in:
2020-09-26 09:33:20 +00:00
parent 9abffb71ba
commit ea695afc99
8 changed files with 778 additions and 267 deletions

634
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,9 +14,12 @@ arrayref = "*"
chrono = "*"
chrono-tz = "*"
euclid = "*"
log = "*"
log4rs = { version = "*", features = ["file", "yaml_format", "console_appender"] }
xml-rs = "0.8"
text_io = "0.1.8"
serde = { version = "*", features = ["derive"] }
serde_json = "*"
rusttype = "0.8.3"
rusttype = "0.9.2"
tungstenite = "*"
url = "*"

View File

@@ -12,7 +12,7 @@ The idea is to find a cheap, small, GPIO connected monitor and have it display u
* Very cheap cockpit display software for Openplotter on the Raspberry PI.
* Bought on eBay, for size, and use of less GPIO pins. Thus ended up buying an ili9488 based 3.5 inch TFT LCD. I would love to get something transflective in the future. It turned out the ili9488 is very poorly supported in the linux fb world; I've ordered a 2.8 inch ili9341 based display to try that one out too.
* Reference platform is the Raspberry PI 3 Model B; reasons being:
* It seems to be powerful enough, it's a 1.2 GHz 64 bit qtad-core ARM Cortex-A53 processor and 1 GiB of memory. Furthermore is has 4 USB ports, making it less likely that I'll need a USB hub; also, it has WiFi should I want to play around with that. Newer, more powerful models are very neat, but they also have a significantly higher power consumption, model B idles at about 230 mA (1.2 W) where the B+, for example, idles at 350 mA (1.7 W)
* It seems to be powerful enough, it's a 1.2 GHz 64 bit quad-core ARM Cortex-A53 processor and 1 GiB of memory. Furthermore is has 4 USB ports, making it less likely that I'll need a USB hub; also, it has WiFi should I want to play around with that. Newer, more powerful models are very neat, but they also have a significantly higher power consumption, model B idles at about 230 mA (1.2 W) where the B+, for example, idles at 350 mA (1.7 W)
# Software considerations

8
log4rs.yml Normal file
View File

@@ -0,0 +1,8 @@
refresh_rate: 30 seconds
appenders:
stdout:
kind: console
root:
level: info
appenders:
- stdout

View File

@@ -11,6 +11,7 @@ use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::io::Read;
use std::rc::Rc;
use std::fs;
use xml::reader::{EventReader, XmlEvent};
use euclid::Point2D;
@@ -72,20 +73,13 @@ impl Loader {
pub fn load_font (&mut self, path: &str) -> Option<Font> {
let mut file = match self.make_file(path) {
Some(f) => f,
None => return None,
let fullpath = match self.make_path(path) {
Some(p) => p,
None => return None,
};
let mut data = Vec::new();
if file.read_to_end(&mut data).is_err() {
return None;
}
let font = match Font::from_bytes(data) {
Ok(data) => data,
Err(_) => return None,
};
Some(font)
let data = fs::read(fullpath).expect("File read error");
let font = move || Font::try_from_bytes(&data).unwrap();
Some(font())
}
pub fn load_form(&mut self, path: &str) -> Option<Form> {
@@ -155,7 +149,7 @@ impl Screen {
pub fn render (&mut self, form: &Form, angle: f32, x: u16, y: u16) {
let transform : Transform2D<f32, ModelSpace, ModelSpace> = Transform2D::create_rotation(Angle::radians(angle)).post_translate(Vector2D::new(500.0, 500.0));
let transform : Transform2D<f32, ModelSpace, ModelSpace> = Transform2D::rotation(Angle::radians(angle)).then_translate(Vector2D::new(500.0, 500.0));
let tx_points = form.points.iter().map(|p| transform.transform_point(*p)).collect::<Vec<_>>();
let screen_points = tx_points.iter().map(|p| self.model_scale.transform_point(*p)).collect::<Vec<_>>();
for (indices, weight, (r, g, b)) in &form.lines {

View File

@@ -6,7 +6,11 @@ use std::time::Duration;
use std::f32::consts::PI;
use chrono::{DateTime, Utc};
use chrono_tz::Tz;
use rusttype::Font;
#[macro_use]
extern crate log;
use log4rs;
use image::{
Rgb,
@@ -18,12 +22,134 @@ use imageproc::drawing::draw_filled_rect_mut;
mod ilidisplay;
mod forms;
mod signalk;
mod vesseldata;
use vesseldata::VesselDataEventSource;
pub struct HelmsDisplay {
font: Box<Font<'static>>,
boat: forms::Form,
compassrose: forms::Form,
cog: forms::Form,
wind: forms::Form,
gps_screen: forms::Screen,
time_screen: forms::Screen,
course_screen: forms::Screen,
}
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 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 f = loader.load_font("font.ttf").unwrap();
let font = Box::new(f);
HelmsDisplay {
font: font,
boat: boat,
compassrose: compassrose,
cog: cog,
wind: wind,
gps_screen: forms::Screen::new(400, 40),
time_screen: forms::Screen::new(200, 60),
course_screen: forms::Screen::new(200, 200),
}
}
pub fn render_gps (&mut self, loc: Option<vesseldata::VesselDataEvent>) {
self.gps_screen.clear();
match loc {
None => self.gps_screen.text_c(&self.font, " --\u{00B0}--\u{2032}--\u{2033} --\u{00B0}--\u{2032}--\u{2033}", 32.0, 200, 5),
Some(vesseldata::VesselDataEvent::Location(lat,lon)) => {
// format with unicodes for degrees, minutes and seconds.
let (latitude, lat_ew) = {
if lat < 0.0 {
(-lat, 'W')
} else if lat > 0.0 {
(lat, 'E')
} else {
(0.0, '-')
}
};
let (longitude, long_ns) = {
if lon < 0.0 {
(-lon, 'S')
} else if lon > 0.0 {
(lon, 'N')
} else {
(0.0, '-')
}
};
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);
},
_ => (),
}
}
pub fn render_time (&mut self) {
let tz: Tz = "Europe/Copenhagen".parse().unwrap();
let now = Utc::now().with_timezone(&tz);
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);
}
pub fn render_course (&mut self, cog: Option<vesseldata::VesselDataEvent>, magnetic: Option<vesseldata::VesselDataEvent>, wind: Option<vesseldata::VesselDataEvent>) {
let rad = (6 as f32) * 2.0*PI / 100.0;
self.course_screen.clear();
// render course over ground if present
match cog {
Some(vesseldata::VesselDataEvent::CourseOverGround(c)) => {
self.course_screen.render(&self.cog, c, 500, 500);
},
_ => (),
};
// render wind if present
match wind {
Some(vesseldata::VesselDataEvent::WindOrigin(o)) => {
self.course_screen.render(&self.wind, o, 500, 500);
},
_ => (),
};
// render compass rose
self.course_screen.render(&self.compassrose, rad, 500, 500);
self.course_screen.render(&self.boat, 0.0, 500, 500);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// init log library
log4rs::init_file("log4rs.yml", Default::default()).unwrap();
let mut course_screen = forms::Screen::new(200, 200);
let mut sog_screen = forms::Screen::new(160,60);
let mut gps_screen = forms::Screen::new(400, 40);
let mut time_screen = forms::Screen::new(200, 60);
let mut loader = forms::Loader::new("/root/helms-display".to_string());
@@ -32,8 +158,10 @@ fn main() -> Result<(), Box<dyn Error>> {
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 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;
@@ -54,8 +182,9 @@ fn main() -> Result<(), Box<dyn Error>> {
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), (160, (160-100)));
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);
@@ -63,41 +192,21 @@ fn main() -> Result<(), Box<dyn Error>> {
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, 0));
e.put_image(&(sog_screen.image), (0, 50));
*/
gps_screen.clear();
// format with unicodes for degrees, minutes and seconds.
let longitude: f32 = 55.658863;
let latitude: f32 = 12.480960;
helms.render_gps(None);
e.put_image(&(helms.gps_screen.image), (40,0));
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 long_ns: char = 'N';
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 lat_ew: char = 'E';
gps_screen.text_c(&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);
e.put_image(&(gps_screen.image), (240,370));
time_screen.clear();
let tz: Tz = "Europe/Copenhagen".parse().unwrap();
let now = Utc::now().with_timezone(&tz);
time_screen.text_c(&font, now.format("%H:%M:%S %Z").to_string().as_str(), 24.0, 100, 1);
time_screen.text_c(&font, "Europe/Copenhagen", 16.0, 100, 25);
time_screen.text_c(&font, now.format("%Y-%m-%d").to_string().as_str(), 16.0, 100, 43);
time_screen.save();
helms.render_time();
e.put_image(&(helms.time_screen.image), (140,260));
println!("Display has been rendered now, sleeping for 5s");
signalk::SignalKData::connect();
thread::sleep(Duration::from_millis(5000));
let vd = signalk::SignalK::connect();
thread::sleep(Duration::from_millis(15000));
e.turn_off();
Ok(())
}

View File

@@ -1,32 +1,65 @@
use std::thread;
use std::io::{BufReader, Read};
use tungstenite::connect;
use url::Url;
use serde_json::{Result, Value};
use serde::Deserialize;
use serde_json::Value as JValue;
use std::sync::mpsc::{Sender, Receiver};
use std::sync::mpsc;
use crate::vesseldata::{VesselDataEvent, VesselDataEventSource};
// Model the data sent over SignalK Websocket
// that I'm interested in
pub enum SignalKEvent {
SpeedOverGround(f32),
SpeedThroughWater(f32),
Location(f32, f32),
CourseOverGround(f32),
TrueCompassCourse(f32),
AISVessel(String, f32, f32, f32), // Name, lat, long, speed
BatteryLevel(u8, u8), // Bank#, percentage
FuelLevel(u8), // percentage
#[derive(Debug, Deserialize)]
#[serde()]
struct Source {
sentence: String,
talker: String,
#[serde(rename = "type")]
k_type: String,
label: String,
}
pub struct SignalKData {
latitude: f32,
longitude: f32,
position_timestamp: u64,
#[derive(Debug, Deserialize)]
#[serde()]
struct Value {
path: String,
value: JValue,
}
impl SignalKData {
#[derive(Debug, Deserialize)]
#[serde()]
struct Update {
#[serde(rename = "$source")]
dsource: String,
timestamp: String,
values: Vec<Value>,
}
pub fn parseJson (json: String) -> Vec<SignalKEvent> {
let res = Vec::<SignalKEvent>::new();
#[derive(Debug, Deserialize)]
#[serde()]
struct SignalKEventData {
context: String,
updates: Vec<Update>,
}
#[derive(Debug, Deserialize)]
#[serde()]
struct SignalKHeader {
name: String,
version: String,
#[serde(rename="self")]
k_self: String,
roles: Vec<String>,
timestamp: String,
}
pub struct SignalK {
}
impl SignalK {
fn parse_json (json: String) -> Vec<SignalKEventData> {
let res = Vec::<SignalKEventData>::new();
let v: Value = match serde_json::from_str(&json) {
Ok(value) => value,
Err(_) => return res,
@@ -37,22 +70,43 @@ impl SignalKData {
res
}
pub fn connect() -> SignalKData {
let mut data = SignalKData {
latitude: f32::NAN,
longitude: f32::NAN,
position_timestamp: 0,
};
let handle = thread::spawn(|| {
fn value_to_vesseldata (val: Value) -> Option<VesselDataEvent> {
match val.path.as_str() {
"navigation.position" => return Some(VesselDataEvent::Location(val.value["latitude"].as_f64().unwrap() as f32, val.value["longitude"].as_f64().unwrap() as f32)),
"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)),
_ => return None,
}
}
}
impl VesselDataEventSource for SignalK {
fn connect() -> 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 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");
}
loop {
let msg = socket.read_message().expect("Error reading message");
if msg.is_text() {
println!("{}", msg.into_text().unwrap());
let message = socket.read_message().unwrap();
if message.is_text() {
let data: SignalKEventData = serde_json::from_str(message.to_text().unwrap()).unwrap();
for u in data.updates {
for v in u.values {
match SignalK::value_to_vesseldata(v) {
Some(vd) => tx.send(vd).unwrap(),
_ => (),
}
}
}
}
}
});
data
rx
}
}

83
src/vesseldata.rs Normal file
View File

@@ -0,0 +1,83 @@
use std::fmt;
use std::thread;
use tungstenite::connect;
use url::Url;
use serde_json::{Result, Value};
use std::sync::mpsc::Receiver;
// Model the data sent over SignalK Websocket
// that I'm interested in
pub enum VesselDataEvent {
SpeedOverGround(f32),
SpeedThroughWater(f32),
Location(f32, f32),
CourseOverGround(f32),
WindOrigin(f32),
TrueCompassCourse(f32),
AISVessel(String, f32, f32, f32), // Name, lat, long, speed
BatteryLevel(u8, u8), // Bank#, percentage
FuelLevel(u8), // percentage
}
pub trait VesselDataEventSource {
fn connect() -> Receiver<VesselDataEvent>;
}
impl fmt::Debug for VesselDataEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VesselDataEvent::SpeedOverGround(s) => f.debug_tuple("SpeedOverGround").field(s).finish(),
VesselDataEvent::SpeedThroughWater(s) => f.debug_tuple("SpeedThroughWater").field(s).finish(),
VesselDataEvent::Location(lat, lon) => f.debug_tuple("Location").field(lat).field(lon).finish(),
VesselDataEvent::CourseOverGround(c) => f.debug_tuple("CourseOverGround").field(c).finish(),
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::FuelLevel(level) => f.debug_tuple("FuelLevel").field(level).finish(),
VesselDataEvent::WindOrigin(c) => f.debug_tuple("WindOrigin").field(c).finish(),
}
}
}
pub struct VesselDataState {
source: Receiver<VesselDataEvent>,
pub latitude: f32,
pub longitude: f32,
pub speed_over_ground: f32,
pub speed_through_water: f32,
pub true_compass_course: f32,
pub course_over_ground: f32,
pub position_timestamp: u64,
}
impl VesselDataState {
pub fn init (source: Receiver<VesselDataEvent>) -> VesselDataState {
VesselDataState {
source: source,
latitude: 0.0,
longitude: 0.0,
speed_over_ground: 0.0,
speed_through_water: 0.0,
true_compass_course: 0.0,
course_over_ground: 0.0,
position_timestamp: 0,
}
}
pub fn read_events(&mut self) -> () {
for vde in self.source.try_iter() {
match vde {
VesselDataEvent::SpeedOverGround(s) => self.speed_over_ground = s,
VesselDataEvent::SpeedThroughWater(s) => self.speed_through_water = s,
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,
_ => (),
}
}
}
}