Compare commits

...

6 commits

Author SHA1 Message Date
jane400
8791b08c4f feat: ble data parsing: version 2024-08-28 15:21:29 +02:00
jane400
edc2da5d9e chore: mark packstation api as unstable 2024-08-28 15:17:16 +02:00
jane400
5cf693b7bb chore: move primitive{reader,build} to utils 2024-08-28 15:14:42 +02:00
jane400
890aee8f72 chore: remove unused imports 2024-08-28 15:11:55 +02:00
jane400
ae64f73176 feat: more details button in SendungsVerfolgung 2024-08-28 15:09:18 +02:00
jane400
f3732051af fix: ui tweaks for advices 2024-08-28 15:07:32 +02:00
11 changed files with 337 additions and 118 deletions

3
icons.toml Normal file
View file

@ -0,0 +1,3 @@
app_id = "de.j4ne.Paket"
icons = ["plus", "minus"]

View file

@ -1,5 +1,6 @@
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use uuid::Uuid; use uuid::Uuid;
use super::utils::{PrimitiveBuilder, PrimitiveReader};
use crate::{LibraryError, LibraryResult}; use crate::{LibraryError, LibraryResult};
@ -81,104 +82,6 @@ pub struct Command {
metadata: Vec<u8> metadata: Vec<u8>
} }
struct PrimitiveBuilder {
bin: Vec<u8>,
}
impl PrimitiveBuilder {
fn new() -> Self {
PrimitiveBuilder {
bin: Vec::new()
}
}
fn write_u8(self, number: u8) -> Self {
self.write_array(&[number])
}
fn write_u16(self, number: u16) -> Self {
self.write_array(&number.to_be_bytes())
}
fn write_u32(self, number: u32) -> Self {
self.write_array(&number.to_be_bytes())
}
fn write_array(mut self, new: &[u8]) -> Self {
for b in new {
self.bin.push(*b);
}
self
}
fn write_array_with_len(self, bin: &[u8]) -> Self {
self
.write_u32(bin.len() as u32)
.write_array(bin)
}
fn finish(self) -> Vec<u8> {
self.bin
}
}
struct PrimitiveReader<'a> {
offset: usize,
vec: &'a [u8],
}
// Yes, I know Cursor exists and eio exists. from_be_bytes should be a stdlib trait tho.
impl<'a> PrimitiveReader<'a> {
fn read_u8(&mut self) -> u8 {
let number = self.vec[self.offset];
self.offset += 1;
number
}
fn read_u16(&mut self) -> u16 {
let arr: [u8; 2] = self.read_arr_const();
u16::from_be_bytes(arr)
}
fn read_u32(&mut self) -> u32 {
let arr: [u8; 4] = self.read_arr_const();
u32::from_be_bytes(arr)
}
fn read_u64(&mut self) -> u64 {
let arr: [u8; 8] = self.read_arr_const();
u64::from_be_bytes(arr)
}
fn read_arr_const<const N: usize>(&mut self) -> [u8; N] {
let mut arr = [0u8; N];
for n in 0..N {
arr[n] = self.read_u8();
}
return arr;
}
fn read_arr(&mut self, n: usize) -> Vec<u8> {
let mut arr: Vec<u8> = vec![];
for _ in 0..n {
arr.push(self.read_u8());
}
arr
}
fn read_arr_from_len(&mut self) -> Vec<u8> {
let size = self.read_u32() as usize;
self.read_arr(size)
}
fn left_to_process(&self) -> usize {
self.vec.len() - self.offset
}
}
impl Command { impl Command {
fn checksum(bin: &[u8]) -> u16 { fn checksum(bin: &[u8]) -> u16 {
// CRC16 of some kind... // CRC16 of some kind...

View file

@ -1,13 +1,24 @@
#[cfg(feature = "locker_ble")] #[cfg(feature = "locker_ble")]
#[cfg(feature = "unstable")]
mod command; mod command;
#[cfg(feature = "locker_ble")] #[cfg(feature = "locker_ble")]
#[cfg(feature = "unstable")]
mod types; mod types;
#[cfg(feature = "locker_ble")] #[cfg(feature = "locker_ble")]
#[cfg(feature = "unstable")]
pub use types::*; pub use types::*;
#[cfg(feature = "locker_ble")]
#[cfg(feature = "unstable")]
mod api; mod api;
#[cfg(feature = "locker_ble")]
#[cfg(feature = "unstable")]
pub use api::*;
pub mod crypto; pub mod crypto;
pub(crate) mod utils;
#[cfg(feature = "locker_register_base")] #[cfg(feature = "locker_register_base")]
mod register_base; mod register_base;
#[cfg(feature = "locker_register_regtoken")] #[cfg(feature = "locker_register_regtoken")]

View file

@ -1,23 +1,107 @@
use crate::LibraryError; use btleplug::platform::PeripheralId;
use super::utils::PrimitiveReader;
use crate::{LibraryError, LibraryResult};
// 601e7028-0565- // 601e7028-0565-
pub static LOCKER_SERVICE_UUID_PREFIX: (u32, u16) = (0x601e7028, 0x0565); pub static LOCKER_SERVICE_UUID_PREFIX: (u32, u16) = (0x601e7028, 0x0565);
pub enum LockerVendor {
Keba,
Snbc,
Unknown,
}
impl From<u8> for LockerVendor {
fn from(value: u8) -> Self {
if value == 1 {
Self::Keba
} else if value == 2 {
Self::Snbc
} else {
Self::Unknown
}
}
}
#[derive(Debug)]
pub struct LockerVersion {
pub tlv_version: u8,
pub vendor: u8,
pub major: u8,
pub minor: u8,
}
#[derive(Debug)]
pub struct LockerDevice {
pub id: PeripheralId,
pub service_uuid: LockerServiceUUID,
pub version: LockerVersion,
}
impl LockerDevice {
pub(crate) fn new(
id: PeripheralId,
service_uuid: LockerServiceUUID,
service_data: &Vec<u8>,
) -> LibraryResult<Self> {
mini_assert_inval!(service_data.len() == 14);
let mut reader = PrimitiveReader {
offset: 0,
vec: &service_data,
};
let version = LockerVersion {
tlv_version: reader.read_u8(),
vendor: reader.read_u8(),
major: reader.read_u8(),
minor: reader.read_u8(),
};
let part1: u16 = reader.read_u16();
let part2: u16 = reader.read_u16();
let part_last = reader.read_u32();
assert_eq!(reader.left_to_process(), 0);
println!(
"decoded: {:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>8x}",
LOCKER_SERVICE_UUID_PREFIX.0, LOCKER_SERVICE_UUID_PREFIX.1, part1, part2, part_last
);
println!("expected: {:?}", service_uuid);
Ok(LockerDevice {
id,
service_uuid,
version,
})
}
}
#[derive(Debug)]
pub struct LockerServiceUUID { pub struct LockerServiceUUID {
service_uuid: uuid::Uuid, service_uuid: uuid::Uuid,
} }
impl LockerServiceUUID {
pub fn get(&self) -> &uuid::Uuid {
&self.service_uuid
}
}
impl TryFrom<uuid::Uuid> for LockerServiceUUID { impl TryFrom<uuid::Uuid> for LockerServiceUUID {
type Error = crate::LibraryError; type Error = crate::LibraryError;
fn try_from(value: uuid::Uuid) -> Result<Self, Self::Error> { fn try_from(value: uuid::Uuid) -> LibraryResult<Self> {
let fields = value.as_fields(); let fields = value.as_fields();
if fields.0 != LOCKER_SERVICE_UUID_PREFIX.0 || fields.1 != LOCKER_SERVICE_UUID_PREFIX.1 { if fields.0 != LOCKER_SERVICE_UUID_PREFIX.0 || fields.1 != LOCKER_SERVICE_UUID_PREFIX.1 {
return Err(LibraryError::InvalidArgument("TryFrom<Uuid> for LockerServiceUUID: prefix mismatch (expected 601e7028-0565-)".to_string())) return Err(LibraryError::InvalidArgument(
"TryFrom<Uuid> for LockerServiceUUID: prefix mismatch (expected 601e7028-0565-)"
.to_string(),
));
} }
Ok(LockerServiceUUID { Ok(LockerServiceUUID {
service_uuid: value service_uuid: value,
}) })
} }
} }
@ -26,4 +110,4 @@ impl Into<uuid::Uuid> for LockerServiceUUID {
fn into(self) -> uuid::Uuid { fn into(self) -> uuid::Uuid {
self.service_uuid self.service_uuid
} }
} }

View file

@ -0,0 +1,93 @@
pub(crate) struct PrimitiveBuilder {
pub bin: Vec<u8>,
}
impl PrimitiveBuilder {
pub(crate) fn new() -> Self {
PrimitiveBuilder { bin: Vec::new() }
}
pub(crate) fn write_u8(self, number: u8) -> Self {
self.write_array(&[number])
}
pub(crate) fn write_u16(self, number: u16) -> Self {
self.write_array(&number.to_be_bytes())
}
pub(crate) fn write_u32(self, number: u32) -> Self {
self.write_array(&number.to_be_bytes())
}
pub(crate) fn write_array(mut self, new: &[u8]) -> Self {
for b in new {
self.bin.push(*b);
}
self
}
pub(crate) fn write_array_with_len(self, bin: &[u8]) -> Self {
self.write_u32(bin.len() as u32).write_array(bin)
}
pub(crate) fn finish(self) -> Vec<u8> {
self.bin
}
}
pub(crate) struct PrimitiveReader<'a> {
pub offset: usize,
pub vec: &'a [u8],
}
// Yes, I know Cursor exists and eio exists. from_be_bytes should be a stdlib trait tho.
impl<'a> PrimitiveReader<'a> {
pub(crate) fn read_u8(&mut self) -> u8 {
let number = self.vec[self.offset];
self.offset += 1;
number
}
pub(crate) fn read_u16(&mut self) -> u16 {
let arr: [u8; 2] = self.read_arr_const();
u16::from_be_bytes(arr)
}
pub(crate) fn read_u32(&mut self) -> u32 {
let arr: [u8; 4] = self.read_arr_const();
u32::from_be_bytes(arr)
}
pub(crate) fn read_u64(&mut self) -> u64 {
let arr: [u8; 8] = self.read_arr_const();
u64::from_be_bytes(arr)
}
pub(crate) fn read_arr_const<const N: usize>(&mut self) -> [u8; N] {
let mut arr = [0u8; N];
for n in 0..N {
arr[n] = self.read_u8();
}
return arr;
}
pub(crate) fn read_arr(&mut self, n: usize) -> Vec<u8> {
let mut arr: Vec<u8> = vec![];
for _ in 0..n {
arr.push(self.read_u8());
}
arr
}
pub(crate) fn read_arr_from_len(&mut self) -> Vec<u8> {
let size = self.read_u32() as usize;
self.read_arr(size)
}
pub(crate) fn left_to_process(&self) -> usize {
self.vec.len() - self.offset
}
}

View file

@ -9,7 +9,7 @@ pub use self::openid_response::{RefreshToken, TokenResponse};
pub use self::utils::{create_nonce, CodeVerfier}; pub use self::utils::{create_nonce, CodeVerfier};
use super::common::APIResult; use super::common::APIResult;
use crate::{LibraryError, LibraryResult}; use crate::LibraryResult;
pub struct OpenIdClient { pub struct OpenIdClient {
client: reqwest::Client, client: reqwest::Client,

View file

@ -82,9 +82,9 @@ struct SendungsInfo {
sendungsliste: Option<String>, sendungsliste: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct SendungsVerlaufEvent { pub struct SendungsVerlaufEvent {
pub datum: String, pub datum: String,
ort: Option<String>, ort: Option<String>,
pub ruecksendung: bool, pub ruecksendung: bool,
@ -102,18 +102,18 @@ impl SendungsVerlaufEvent {
} }
} }
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct SendungsVerlauf { pub struct SendungsVerlauf {
kurz_status: Option<String>, kurz_status: Option<String>,
icon_id: Option<String>, icon_id: Option<String>,
datum_aktueller_status: Option<String>, datum_aktueller_status: Option<String>,
aktueller_status: Option<String>, aktueller_status: Option<String>,
events: Option<Vec<SendungsVerlaufEvent>>, pub events: Option<Vec<SendungsVerlaufEvent>>,
farbe: u32, farbe: u32,
fortschritt: u32, pub fortschritt: u32,
maximal_fortschritt: u32, pub maximal_fortschritt: u32,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -321,6 +321,7 @@ pub struct Shipment {
pub special: ShipmentSpecialDetails, pub special: ShipmentSpecialDetails,
pub history: SendungsVerlauf,
pub error: Option<ShipmentNotFoundError>, pub error: Option<ShipmentNotFoundError>,
} }
@ -367,6 +368,7 @@ impl From<Sendung> for Shipment {
None None
} }
}, },
history: value.sendungsdetails.sendungsverlauf,
special: ShipmentSpecialDetails { special: ShipmentSpecialDetails {
abholcode_available, abholcode_available,
benachrichtigt_in_filiale, benachrichtigt_in_filiale,

View file

@ -16,3 +16,4 @@ reqwest = "0.12"
libpaket = { path = "../libpaket" } libpaket = { path = "../libpaket" }
glycin = { version = "2.0.0-beta", features = ["gdk4"] } glycin = { version = "2.0.0-beta", features = ["gdk4"] }
oo7 = { version = "0.3" } oo7 = { version = "0.3" }
relm4-icons = { version = "0.9" }

View file

@ -49,6 +49,7 @@ impl FactoryComponent for AppAdvice {
add_overlay = &gtk::Box { add_overlay = &gtk::Box {
add_css_class: relm4::css::OSD, add_css_class: relm4::css::OSD,
add_css_class: relm4::css::TOOLBAR,
add_css_class: relm4::css::NUMERIC, add_css_class: relm4::css::NUMERIC,
set_valign: gtk::Align::End, set_valign: gtk::Align::End,
@ -57,8 +58,6 @@ impl FactoryComponent for AppAdvice {
set_margin_all: 8, set_margin_all: 8,
gtk::Label { gtk::Label {
set_margin_all: 4,
set_label: self.metadata.date.as_str(), set_label: self.metadata.date.as_str(),
}, },
}, },

View file

@ -3,6 +3,7 @@ use std::sync::Arc;
use login::{Login, LoginOutput, LoginSharedState}; use login::{Login, LoginOutput, LoginSharedState};
use ready::{Ready, ReadyOutput}; use ready::{Ready, ReadyOutput};
use relm4::{ use relm4::{
RELM_THREADS,
adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex, adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex,
AsyncComponentSender, SharedState, AsyncComponentSender, SharedState,
}; };
@ -289,6 +290,13 @@ fn convert_ready_response(response: ReadyOutput) -> AppInput {
} }
fn main() { fn main() {
RELM_THREADS.set(4).unwrap();
gtk::init().unwrap();
let display = gtk::gdk::Display::default().unwrap();
let theme = gtk::IconTheme::for_display(&display);
theme.add_resource_path("/de/j4ne/Paket/icons/");
theme.add_resource_path("/de/j4ne/Paket/scalable/actions/");
relm4_icons::initialize_icons();
let app = RelmApp::new(constants::APP_ID); let app = RelmApp::new(constants::APP_ID);
app.run_async::<App>(()); app.run_async::<App>(());
} }

View file

@ -6,6 +6,20 @@ use relm4::{adw, gtk};
pub struct ShipmentView { pub struct ShipmentView {
model: Shipment, model: Shipment,
// model abstraction
have_events: bool,
// state
expanded: bool,
// workarounds
list_box_history: gtk::ListBox,
}
#[derive(Debug)]
pub enum ViewInput {
ToggleExpand,
} }
#[relm4::factory(pub)] #[relm4::factory(pub)]
@ -13,7 +27,7 @@ impl FactoryComponent for ShipmentView {
type CommandOutput = (); type CommandOutput = ();
type Init = Shipment; type Init = Shipment;
type Output = (); type Output = ();
type Input = (); type Input = ViewInput;
type ParentWidget = gtk::Box; type ParentWidget = gtk::Box;
type Index = String; type Index = String;
@ -23,14 +37,26 @@ impl FactoryComponent for ShipmentView {
add_css_class: relm4::css::CARD, add_css_class: relm4::css::CARD,
set_hexpand: true, set_hexpand: true,
set_margin_all: 8, set_margin_all: 8,
set_orientation: gtk::Orientation::Vertical,
gtk::ProgressBar {
add_css_class: relm4::css::OSD,
set_margin_start: 8,
set_margin_end: 8,
set_margin_bottom: 1,
set_fraction: f64::from(self.model.history.maximal_fortschritt) / f64::from(self.model.history.fortschritt),
},
// title box // title box
gtk::Box { gtk::Box {
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_margin_all: 8, set_margin_all: 8,
gtk::Box { gtk::Box {
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_halign: gtk::Align::Start, set_halign: gtk::Align::Start,
gtk::Label { gtk::Label {
@ -61,9 +87,52 @@ impl FactoryComponent for ShipmentView {
} }
} }
} }
},
gtk::Button {
set_halign: gtk::Align::End,
add_css_class: relm4::css::FLAT,
#[watch]
set_icon_name: {
if self.expanded {
relm4_icons::icon_names::MINUS
} else {
relm4_icons::icon_names::PLUS
}
},
connect_clicked => ViewInput::ToggleExpand,
} }
} }, // title box end
gtk::Revealer {
#[watch]
set_reveal_child: self.expanded,
#[wrap(Some)]
set_child = &gtk::Box {
// history viewstack
adw::StatusPage {
set_visible: !self.have_events,
set_title: "No events",
},
append = &self.list_box_history.clone() {
set_visible: self.have_events,
add_css_class: relm4::css::BOXED_LIST,
set_selection_mode: gtk::SelectionMode::None,
},
}
},
} }
} }
fn init_model( fn init_model(
@ -71,8 +140,54 @@ impl FactoryComponent for ShipmentView {
index: &Self::Index, index: &Self::Index,
sender: relm4::FactorySender<Self>, sender: relm4::FactorySender<Self>,
) -> Self { ) -> Self {
let model = ShipmentView { model: init }; let have_events = init.history.events.as_ref().is_some_and(|a| a.len() > 0);
model let list_box_history = gtk::ListBox::new();
let _self = ShipmentView {
have_events,
model: init,
list_box_history,
expanded: false,
};
for elem in _self.model.history.events.as_ref().unwrap() {
let label_datum = gtk::Label::builder()
.css_classes([relm4::css::NUMERIC])
.label(&elem.datum)
.halign(gtk::Align::Start)
.valign(gtk::Align::Start)
.build();
// TODO: is html, parse it
let label_status = gtk::Label::builder()
.wrap(true)
.halign(gtk::Align::Start)
.valign(gtk::Align::Start)
.build();
label_status.set_markup(&elem.status);
let boxie = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.margin_start(8)
.margin_end(8)
.spacing(8)
.build();
boxie.append(&label_datum);
boxie.append(&label_status);
_self.list_box_history.append(&boxie);
}
_self
}
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message {
ViewInput::ToggleExpand => {
self.expanded = !self.expanded;
}
}
} }
} }