mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-08 14:11:52 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2226071d2 |
@@ -1,9 +0,0 @@
|
|||||||
# Files that are distributed onto the Rayhunter device always have to have
|
|
||||||
# Unix-style line endings, even if the installer is built on Windows with
|
|
||||||
# autocrlf enabled.
|
|
||||||
# Using CRLF for the init scripts will make them fail to execute on TP-Link.
|
|
||||||
# See https://github.com/EFForg/rayhunter/issues/489
|
|
||||||
|
|
||||||
dist/config.toml.in eol=lf
|
|
||||||
dist/scripts/misc-daemon eol=lf
|
|
||||||
dist/scripts/rayhunter_daemon eol=lf
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
name: Installer Issue
|
|
||||||
description: File an bug related to an installer issue.
|
|
||||||
labels: ["bug", "installer"]
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Rayhunter Version
|
|
||||||
placeholder: 'v0.5.0'
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Device
|
|
||||||
description: |
|
|
||||||
What device are you trying to install Rayhunter on?
|
|
||||||
options:
|
|
||||||
- Orbic RC400L
|
|
||||||
- Tplink HW7350
|
|
||||||
- Tplink HW7310
|
|
||||||
- Tmobile TMOHS1
|
|
||||||
- Wingtech CT2MHS0
|
|
||||||
- Pinephone
|
|
||||||
- Other / I'm not sure
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Installer OS
|
|
||||||
description: What operating system are running the installer from
|
|
||||||
multiple: false
|
|
||||||
options:
|
|
||||||
- Linux
|
|
||||||
- macOS
|
|
||||||
- Windows
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Describe the Issue
|
|
||||||
description: |
|
|
||||||
Please describe the issue you're having installing Rayhunter.
|
|
||||||
Include the logs outputed by the installer program. If the installer
|
|
||||||
is crashing, please try running the installer with `RUST_BACKTRACE=1`
|
|
||||||
environment variable set so we can see exactly where the installer is
|
|
||||||
crashing.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
Generated
+6
-6
@@ -1489,7 +1489,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "installer"
|
name = "installer"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adb_client",
|
"adb_client",
|
||||||
"aes",
|
"aes",
|
||||||
@@ -2384,7 +2384,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayhunter"
|
name = "rayhunter"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2405,7 +2405,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayhunter-check"
|
name = "rayhunter-check"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -2419,7 +2419,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayhunter-daemon"
|
name = "rayhunter-daemon"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2537,7 +2537,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rootshell"
|
name = "rootshell"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nix",
|
"nix",
|
||||||
]
|
]
|
||||||
@@ -2919,7 +2919,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "telcom-parser"
|
name = "telcom-parser"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"asn1-codecs",
|
"asn1-codecs",
|
||||||
"asn1-compiler",
|
"asn1-compiler",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter-check"
|
name = "rayhunter-check"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter-daemon"
|
name = "rayhunter-daemon"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
+123
-241
@@ -1,4 +1,3 @@
|
|||||||
use std::ops::DerefMut;
|
|
||||||
use std::pin::pin;
|
use std::pin::pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -10,261 +9,135 @@ use axum::response::{IntoResponse, Response};
|
|||||||
use futures::{StreamExt, TryStreamExt};
|
use futures::{StreamExt, TryStreamExt};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||||
use rayhunter::diag::{DataType, MessagesContainer};
|
use rayhunter::diag::DataType;
|
||||||
use rayhunter::diag_device::DiagDevice;
|
use rayhunter::diag_device::DiagDevice;
|
||||||
use rayhunter::qmdl::QmdlWriter;
|
use rayhunter::qmdl::QmdlWriter;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::sync::{RwLock, oneshot};
|
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
|
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
|
||||||
use crate::display;
|
use crate::display;
|
||||||
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
use crate::qmdl_store::{EntryType, RecordingStore, RecordingStoreError};
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
|
|
||||||
pub enum DiagDeviceCtrlMessage {
|
pub enum DiagDeviceCtrlMessage {
|
||||||
StopRecording,
|
StopRecording,
|
||||||
StartRecording,
|
StartRecording,
|
||||||
DeleteEntry {
|
|
||||||
name: String,
|
|
||||||
response_tx: oneshot::Sender<Result<(), RecordingStoreError>>,
|
|
||||||
},
|
|
||||||
DeleteAllEntries {
|
|
||||||
response_tx: oneshot::Sender<Result<(), RecordingStoreError>>,
|
|
||||||
},
|
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DiagTask {
|
|
||||||
ui_update_sender: Sender<display::DisplayState>,
|
|
||||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
|
||||||
analyzer_config: AnalyzerConfig,
|
|
||||||
state: DiagState,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DiagState {
|
|
||||||
Recording {
|
|
||||||
qmdl_writer: QmdlWriter<File>,
|
|
||||||
analysis_writer: Box<AnalysisWriter>,
|
|
||||||
},
|
|
||||||
Stopped,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiagTask {
|
|
||||||
fn new(
|
|
||||||
ui_update_sender: Sender<display::DisplayState>,
|
|
||||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
|
||||||
analyzer_config: AnalyzerConfig,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
ui_update_sender,
|
|
||||||
analysis_sender,
|
|
||||||
analyzer_config,
|
|
||||||
state: DiagState::Stopped,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start recording
|
|
||||||
async fn start(&mut self, qmdl_store: &mut RecordingStore) {
|
|
||||||
let (qmdl_file, analysis_file) = qmdl_store
|
|
||||||
.new_entry()
|
|
||||||
.await
|
|
||||||
.expect("failed creating QMDL file entry");
|
|
||||||
self.stop_current_recording().await;
|
|
||||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
|
||||||
let analysis_writer = AnalysisWriter::new(analysis_file, &self.analyzer_config)
|
|
||||||
.await
|
|
||||||
.map(Box::new)
|
|
||||||
.expect("failed to write to analysis file");
|
|
||||||
self.state = DiagState::Recording {
|
|
||||||
qmdl_writer,
|
|
||||||
analysis_writer,
|
|
||||||
};
|
|
||||||
if let Err(e) = self
|
|
||||||
.ui_update_sender
|
|
||||||
.send(display::DisplayState::Recording)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("couldn't send ui update message: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stop recording
|
|
||||||
async fn stop(&mut self, qmdl_store: &mut RecordingStore) {
|
|
||||||
self.stop_current_recording().await;
|
|
||||||
if let Some((_, entry)) = qmdl_store.get_current_entry() {
|
|
||||||
if let Err(e) = self
|
|
||||||
.analysis_sender
|
|
||||||
.send(AnalysisCtrlMessage::RecordingFinished(
|
|
||||||
entry.name.to_string(),
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("couldn't send analysis message: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(e) = qmdl_store.close_current_entry().await {
|
|
||||||
error!("couldn't close current entry: {e}");
|
|
||||||
}
|
|
||||||
if let Err(e) = self
|
|
||||||
.ui_update_sender
|
|
||||||
.send(display::DisplayState::Paused)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("couldn't send ui update message: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_entry(
|
|
||||||
&mut self,
|
|
||||||
qmdl_store: &mut RecordingStore,
|
|
||||||
name: &str,
|
|
||||||
) -> Result<(), RecordingStoreError> {
|
|
||||||
if qmdl_store.is_current_entry(name) {
|
|
||||||
self.stop(qmdl_store).await;
|
|
||||||
}
|
|
||||||
let res = qmdl_store.delete_entry(name).await;
|
|
||||||
if let Err(e) = res.as_ref() {
|
|
||||||
error!("Error deleting QMDL entry {e}");
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_all_entries(
|
|
||||||
&mut self,
|
|
||||||
qmdl_store: &mut RecordingStore,
|
|
||||||
) -> Result<(), RecordingStoreError> {
|
|
||||||
self.stop(qmdl_store).await;
|
|
||||||
let res = qmdl_store.delete_all_entries().await;
|
|
||||||
if let Err(e) = res.as_ref() {
|
|
||||||
error!("Error deleting QMDL entries {e}");
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stop_current_recording(&mut self) {
|
|
||||||
let mut state = DiagState::Stopped;
|
|
||||||
std::mem::swap(&mut self.state, &mut state);
|
|
||||||
if let DiagState::Recording {
|
|
||||||
analysis_writer, ..
|
|
||||||
} = state
|
|
||||||
{
|
|
||||||
analysis_writer
|
|
||||||
.close()
|
|
||||||
.await
|
|
||||||
.expect("failed to close analysis writer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn process_container(
|
|
||||||
&mut self,
|
|
||||||
qmdl_store: &mut RecordingStore,
|
|
||||||
container: MessagesContainer,
|
|
||||||
) {
|
|
||||||
if container.data_type != DataType::UserSpace {
|
|
||||||
debug!("skipping non-userspace diag messages...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// keep track of how many bytes were written to the QMDL file so we can read
|
|
||||||
// a valid block of data from it in the HTTP server
|
|
||||||
if let DiagState::Recording {
|
|
||||||
qmdl_writer,
|
|
||||||
analysis_writer,
|
|
||||||
} = &mut self.state
|
|
||||||
{
|
|
||||||
qmdl_writer
|
|
||||||
.write_container(&container)
|
|
||||||
.await
|
|
||||||
.expect("failed to write to QMDL writer");
|
|
||||||
debug!(
|
|
||||||
"total QMDL bytes written: {}, updating manifest...",
|
|
||||||
qmdl_writer.total_written
|
|
||||||
);
|
|
||||||
let index = qmdl_store
|
|
||||||
.current_entry
|
|
||||||
.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
|
||||||
qmdl_store
|
|
||||||
.update_entry_qmdl_size(index, qmdl_writer.total_written)
|
|
||||||
.await
|
|
||||||
.expect("failed to update qmdl file size");
|
|
||||||
debug!("done!");
|
|
||||||
let heuristic_warning = analysis_writer
|
|
||||||
.analyze(container)
|
|
||||||
.await
|
|
||||||
.expect("failed to analyze container");
|
|
||||||
if heuristic_warning {
|
|
||||||
info!("a heuristic triggered on this run!");
|
|
||||||
self.ui_update_sender
|
|
||||||
.send(display::DisplayState::WarningDetected)
|
|
||||||
.await
|
|
||||||
.expect("couldn't send ui update message: {}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!("no qmdl_writer set, continuing...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn run_diag_read_thread(
|
pub fn run_diag_read_thread(
|
||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
mut dev: DiagDevice,
|
mut dev: DiagDevice,
|
||||||
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||||
qmdl_file_tx: Sender<DiagDeviceCtrlMessage>,
|
|
||||||
ui_update_sender: Sender<display::DisplayState>,
|
ui_update_sender: Sender<display::DisplayState>,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
analyzer_config: AnalyzerConfig,
|
analyzer_config: AnalyzerConfig,
|
||||||
) {
|
) {
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
|
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
||||||
|
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
|
||||||
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
||||||
let mut diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config);
|
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, &analyzer_config).await
|
||||||
qmdl_file_tx
|
.expect("failed to create analysis writer"));
|
||||||
.send(DiagDeviceCtrlMessage::StartRecording)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
msg = qmdl_file_rx.recv() => {
|
msg = qmdl_file_rx.recv() => {
|
||||||
match msg {
|
match msg {
|
||||||
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
diag_task.start(qmdl_store.deref_mut()).await;
|
let (qmdl_file, new_analysis_file) = match qmdl_store.new_entry().await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => {
|
||||||
|
error!("couldn't create new qmdl entry: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
maybe_qmdl_writer = Some(QmdlWriter::new(qmdl_file));
|
||||||
|
|
||||||
|
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||||
|
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, &analyzer_config).await
|
||||||
|
.expect("failed to write to analysis file"));
|
||||||
|
|
||||||
|
if let Err(e) = ui_update_sender.send(display::DisplayState::Recording).await {
|
||||||
|
warn!("couldn't send ui update message: {e}");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
diag_task.stop(qmdl_store.deref_mut()).await;
|
if let Some((_, entry)) = qmdl_store.get_current_entry() {
|
||||||
|
if let Err(e) = analysis_sender
|
||||||
|
.send(AnalysisCtrlMessage::RecordingFinished(
|
||||||
|
entry.name.to_string(),
|
||||||
|
))
|
||||||
|
.await {
|
||||||
|
warn!("couldn't send analysis message: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) = qmdl_store.close_current_entry().await {
|
||||||
|
error!("couldn't close current entry: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_qmdl_writer = None;
|
||||||
|
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||||
|
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||||
|
}
|
||||||
|
maybe_analysis_writer = None;
|
||||||
|
|
||||||
|
if let Err(e) = ui_update_sender.send(display::DisplayState::Paused).await {
|
||||||
|
warn!("couldn't send ui update message: {e}");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// None means all the Senders have been dropped, so it's
|
// None means all the Senders have been dropped, so it's
|
||||||
// time to go
|
// time to go
|
||||||
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
||||||
info!("Diag reader thread exiting...");
|
info!("Diag reader thread exiting...");
|
||||||
diag_task.stop_current_recording().await;
|
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||||
|
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||||
|
}
|
||||||
return Ok(())
|
return Ok(())
|
||||||
},
|
},
|
||||||
Some(DiagDeviceCtrlMessage::DeleteEntry { name, response_tx }) => {
|
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
|
||||||
let resp = diag_task.delete_entry(qmdl_store.deref_mut(), name.as_str()).await;
|
|
||||||
if response_tx.send(resp).is_err() {
|
|
||||||
error!("Failed to send delete entry respons, receiver dropped");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(DiagDeviceCtrlMessage::DeleteAllEntries { response_tx }) => {
|
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
|
||||||
let resp = diag_task.delete_all_entries(qmdl_store.deref_mut()).await;
|
|
||||||
if response_tx.send(resp).is_err() {
|
|
||||||
error!("Failed to send delete all entries respons, receiver dropped");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maybe_container = diag_stream.next() => {
|
maybe_container = diag_stream.next() => {
|
||||||
match maybe_container.unwrap() {
|
match maybe_container.unwrap() {
|
||||||
Ok(container) => {
|
Ok(container) => {
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
if container.data_type != DataType::UserSpace {
|
||||||
diag_task.process_container(qmdl_store.deref_mut(), container).await
|
debug!("skipping non-userspace diag messages...");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// keep track of how many bytes were written to the QMDL file so we can read
|
||||||
|
// a valid block of data from it in the HTTP server
|
||||||
|
if let Some(qmdl_writer) = maybe_qmdl_writer.as_mut() {
|
||||||
|
qmdl_writer.write_container(&container).await.expect("failed to write to QMDL writer");
|
||||||
|
debug!("total QMDL bytes written: {}, updating manifest...", qmdl_writer.total_written);
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
||||||
|
qmdl_store.update_entry_qmdl_size(index, qmdl_writer.total_written).await
|
||||||
|
.expect("failed to update qmdl file size");
|
||||||
|
debug!("done!");
|
||||||
|
} else {
|
||||||
|
debug!("no qmdl_writer set, continuing...");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(analysis_writer) = maybe_analysis_writer.as_mut() {
|
||||||
|
let heuristic_warning = analysis_writer.analyze(container).await
|
||||||
|
.expect("failed to analyze container");
|
||||||
|
if heuristic_warning {
|
||||||
|
info!("a heuristic triggered on this run!");
|
||||||
|
ui_update_sender.send(display::DisplayState::WarningDetected).await
|
||||||
|
.expect("couldn't send ui update message: {}");
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("error reading diag device: {err}");
|
error!("error reading diag device: {err}");
|
||||||
@@ -277,7 +150,6 @@ pub fn run_diag_read_thread(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start recording API for web thread
|
|
||||||
pub async fn start_recording(
|
pub async fn start_recording(
|
||||||
State(state): State<Arc<ServerState>>,
|
State(state): State<Arc<ServerState>>,
|
||||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||||
@@ -299,7 +171,6 @@ pub async fn start_recording(
|
|||||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop recording API for web thread
|
|
||||||
pub async fn stop_recording(
|
pub async fn stop_recording(
|
||||||
State(state): State<Arc<ServerState>>,
|
State(state): State<Arc<ServerState>>,
|
||||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||||
@@ -326,27 +197,8 @@ pub async fn delete_recording(
|
|||||||
if state.config.debug_mode {
|
if state.config.debug_mode {
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
}
|
}
|
||||||
let (response_tx, response_rx) = oneshot::channel();
|
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||||
state
|
match qmdl_store.delete_entry(&qmdl_name).await {
|
||||||
.diag_device_ctrl_sender
|
|
||||||
.send(DiagDeviceCtrlMessage::DeleteEntry {
|
|
||||||
name: qmdl_name.clone(),
|
|
||||||
response_tx,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("couldn't send delete entry message: {e}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
match response_rx.await.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("failed to receive delete response: {e}"),
|
|
||||||
)
|
|
||||||
})? {
|
|
||||||
Ok(_) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
|
|
||||||
Err(RecordingStoreError::NoSuchEntryError) => Err((
|
Err(RecordingStoreError::NoSuchEntryError) => Err((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
format!("no recording with name {qmdl_name}"),
|
format!("no recording with name {qmdl_name}"),
|
||||||
@@ -355,6 +207,31 @@ pub async fn delete_recording(
|
|||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("couldn't delete recording: {e}"),
|
format!("couldn't delete recording: {e}"),
|
||||||
)),
|
)),
|
||||||
|
Ok(entry_type) => {
|
||||||
|
if entry_type == EntryType::Current {
|
||||||
|
state
|
||||||
|
.diag_device_ctrl_sender
|
||||||
|
.send(DiagDeviceCtrlMessage::StopRecording)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't send stop recording message: {e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
state
|
||||||
|
.ui_update_sender
|
||||||
|
.send(display::DisplayState::Paused)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't send ui update message: {e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,29 +241,34 @@ pub async fn delete_all_recordings(
|
|||||||
if state.config.debug_mode {
|
if state.config.debug_mode {
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
}
|
}
|
||||||
let (response_tx, response_rx) = oneshot::channel();
|
|
||||||
state
|
state
|
||||||
.diag_device_ctrl_sender
|
.diag_device_ctrl_sender
|
||||||
.send(DiagDeviceCtrlMessage::DeleteAllEntries { response_tx })
|
.send(DiagDeviceCtrlMessage::StopRecording)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("couldn't send delete all entries message: {e}"),
|
format!("couldn't send stop recording message: {e}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
match response_rx.await.map_err(|e| {
|
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||||
|
qmdl_store.delete_all_entries().await.map_err(|e| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("failed to receive delete all response: {e}"),
|
format!("couldn't delete all recordings: {e}"),
|
||||||
)
|
)
|
||||||
})? {
|
})?;
|
||||||
Ok(_) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
|
state
|
||||||
Err(e) => Err((
|
.ui_update_sender
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
.send(display::DisplayState::Paused)
|
||||||
format!("couldn't delete recordings: {e}"),
|
.await
|
||||||
)),
|
.map_err(|e| {
|
||||||
}
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't send ui update message: {e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_analysis_report(
|
pub async fn get_analysis_report(
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use fb_utils::{determine_format, get_var_screeninfo};
|
||||||
use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::FilterType};
|
use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::FilterType};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::os::fd::{AsFd, BorrowedFd};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::display::DisplayState;
|
use crate::display::DisplayState;
|
||||||
@@ -20,6 +24,20 @@ pub struct Dimensions {
|
|||||||
pub width: u32,
|
pub width: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum FbFormat {
|
||||||
|
ARGB888,
|
||||||
|
ABGR888,
|
||||||
|
RGB888,
|
||||||
|
BGR888,
|
||||||
|
RGB666,
|
||||||
|
RGB565,
|
||||||
|
BGR565,
|
||||||
|
RGB555,
|
||||||
|
BGR555,
|
||||||
|
RGB444,
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum Color {
|
pub enum Color {
|
||||||
@@ -131,6 +149,109 @@ pub trait GenericFramebuffer: Send + 'static {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempt to determine the FB dimensions from FB vinfo.
|
||||||
|
pub fn read_fb_dimentions(fb: BorrowedFd<'_>) -> std::io::Result<Dimensions> {
|
||||||
|
let vinfo = get_var_screeninfo(fb)?;
|
||||||
|
Ok(Dimensions {
|
||||||
|
height: vinfo.yres,
|
||||||
|
width: vinfo.xres,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to determine the FBs format
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` if the format cannot be determined
|
||||||
|
pub fn read_fb_format(fb: BorrowedFd<'_>) -> std::io::Result<Option<FbFormat>> {
|
||||||
|
let vinfo = get_var_screeninfo(fb)?;
|
||||||
|
Ok(determine_format(vinfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_to_fb_format(
|
||||||
|
buffer: &Vec<(u8, u8, u8)>,
|
||||||
|
format: &FbFormat,
|
||||||
|
big_endian: bool,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut raw_buffer = Vec::new();
|
||||||
|
for (r, g, b) in buffer {
|
||||||
|
match format {
|
||||||
|
FbFormat::RGB565 => {
|
||||||
|
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
||||||
|
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
||||||
|
rgb565 |= (*b as u16) >> 3;
|
||||||
|
if big_endian {
|
||||||
|
raw_buffer.extend(rgb565.to_be_bytes());
|
||||||
|
} else {
|
||||||
|
raw_buffer.extend(rgb565.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("This display uses a format we haven't implemneted yet {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type CallBack = Box<dyn FnMut(&mut FbInner, &[(u8, u8, u8)]) + Send + 'static>;
|
||||||
|
|
||||||
|
pub struct FramebufferDevice {
|
||||||
|
data: FbInner,
|
||||||
|
pre_write_fn: Option<CallBack>,
|
||||||
|
post_write_fn: Option<CallBack>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FbInner {
|
||||||
|
pub fd: File,
|
||||||
|
pub dims: Dimensions,
|
||||||
|
pub format: FbFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FramebufferDevice {
|
||||||
|
pub fn new(
|
||||||
|
path: &str,
|
||||||
|
pre_write_fn: Option<CallBack>,
|
||||||
|
post_write_fn: Option<CallBack>,
|
||||||
|
) -> Self {
|
||||||
|
// This is done as a blocking call to prevent all of the UI init code from having to
|
||||||
|
// be made async, making it more verbose. This is a single syscall that would have been
|
||||||
|
// done via spawn_blocking anyway, and it's done once on startup.
|
||||||
|
let fb = std::fs::File::create(path).expect("Failed to open /dev/fb0");
|
||||||
|
let dims = read_fb_dimentions(fb.as_fd()).expect("Failed to read FB dimensions");
|
||||||
|
let format = read_fb_format(fb.as_fd())
|
||||||
|
.expect("Failed to read FB format")
|
||||||
|
.expect("FB retruned unexpected format");
|
||||||
|
Self {
|
||||||
|
data: FbInner {
|
||||||
|
fd: File::from_std(fb),
|
||||||
|
dims,
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
pre_write_fn,
|
||||||
|
post_write_fn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl GenericFramebuffer for FramebufferDevice {
|
||||||
|
fn dimensions(&self) -> Dimensions {
|
||||||
|
self.data.dims
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_buffer(
|
||||||
|
&mut self,
|
||||||
|
buffer: Vec<(u8, u8, u8)>, // rgb, row-wise, left-to-right, top-to-bottom
|
||||||
|
) {
|
||||||
|
if let Some(func) = self.pre_write_fn.as_mut() {
|
||||||
|
func(&mut self.data, &buffer);
|
||||||
|
}
|
||||||
|
let raw_buffer = buffer_to_fb_format(&buffer, &self.data.format, false);
|
||||||
|
self.data.fd.write_all(&raw_buffer).await.unwrap();
|
||||||
|
self.data.fd.rewind().await.unwrap();
|
||||||
|
if let Some(func) = self.post_write_fn.as_mut() {
|
||||||
|
func(&mut self.data, &buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_ui(
|
pub fn update_ui(
|
||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
@@ -201,3 +322,184 @@ pub fn update_ui(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod fb_utils {
|
||||||
|
use std::io::{Error, Result};
|
||||||
|
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||||
|
|
||||||
|
use libc::ioctl;
|
||||||
|
|
||||||
|
use super::FbFormat;
|
||||||
|
|
||||||
|
const FBIOGET_VSCREENINFO: libc::c_ulong = 0x4600;
|
||||||
|
// const FBIOGET_FSCREENINFO: libc::c_ulong = 0x4602;
|
||||||
|
|
||||||
|
/// Bitfield which is a part of VarScreeninfo.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct Bitfield {
|
||||||
|
pub offset: u32,
|
||||||
|
pub length: u32,
|
||||||
|
pub msb_right: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Struct as defined in /usr/include/linux/fb.h
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct VarScreeninfo {
|
||||||
|
pub xres: u32,
|
||||||
|
pub yres: u32,
|
||||||
|
pub xres_virtual: u32,
|
||||||
|
pub yres_virtual: u32,
|
||||||
|
pub xoffset: u32,
|
||||||
|
pub yoffset: u32,
|
||||||
|
pub bits_per_pixel: u32,
|
||||||
|
pub grayscale: u32,
|
||||||
|
pub red: Bitfield,
|
||||||
|
pub green: Bitfield,
|
||||||
|
pub blue: Bitfield,
|
||||||
|
pub transp: Bitfield,
|
||||||
|
pub nonstd: u32,
|
||||||
|
pub activate: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub width: u32,
|
||||||
|
pub accel_flags: u32,
|
||||||
|
pub pixclock: u32,
|
||||||
|
pub left_margin: u32,
|
||||||
|
pub right_margin: u32,
|
||||||
|
pub upper_margin: u32,
|
||||||
|
pub lower_margin: u32,
|
||||||
|
pub hsync_len: u32,
|
||||||
|
pub vsync_len: u32,
|
||||||
|
pub sync: u32,
|
||||||
|
pub vmode: u32,
|
||||||
|
pub rotate: u32,
|
||||||
|
pub colorspace: u32,
|
||||||
|
pub reserved: [u32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
// /// Struct as defined in /usr/include/linux/fb.h
|
||||||
|
// /// Note: type is a keyword in Rust and therefore has been changed to fb_type.
|
||||||
|
// #[repr(C)]
|
||||||
|
// #[derive(Clone, Debug, Default)]
|
||||||
|
// pub struct FixScreeninfo {
|
||||||
|
// pub id: [u8; 16],
|
||||||
|
// pub smem_start: usize,
|
||||||
|
// pub smem_len: u32,
|
||||||
|
// pub fb_type: u32,
|
||||||
|
// pub type_aux: u32,
|
||||||
|
// pub visual: u32,
|
||||||
|
// pub xpanstep: u16,
|
||||||
|
// pub ypanstep: u16,
|
||||||
|
// pub ywrapstep: u16,
|
||||||
|
// pub line_length: u32,
|
||||||
|
// pub mmio_start: usize,
|
||||||
|
// pub mmio_len: u32,
|
||||||
|
// pub accel: u32,
|
||||||
|
// pub capabilities: u16,
|
||||||
|
// pub reserved: [u16; 2],
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn get_fix_screeninfo(fb: BorrowedFd<'_>) -> Result<FixScreeninfo> {
|
||||||
|
// let mut info: FixScreeninfo = Default::default();
|
||||||
|
// let result = unsafe { ioctl(fb.as_raw_fd(), FBIOGET_FSCREENINFO as _, &mut info) };
|
||||||
|
// match result {
|
||||||
|
// -1 => Err(Error::last_os_error()),
|
||||||
|
// _ => Ok(info),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn get_var_screeninfo(fb: BorrowedFd<'_>) -> Result<VarScreeninfo> {
|
||||||
|
let mut info: VarScreeninfo = Default::default();
|
||||||
|
let result = unsafe { ioctl(fb.as_raw_fd(), FBIOGET_VSCREENINFO as _, &mut info) };
|
||||||
|
match result {
|
||||||
|
-1 => Err(Error::last_os_error()),
|
||||||
|
_ => Ok(info),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
struct RgbaBitfield {
|
||||||
|
red: Bitfield,
|
||||||
|
green: Bitfield,
|
||||||
|
blue: Bitfield,
|
||||||
|
transp: Bitfield,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&VarScreeninfo> for RgbaBitfield {
|
||||||
|
fn from(value: &VarScreeninfo) -> Self {
|
||||||
|
Self {
|
||||||
|
red: value.red.clone(),
|
||||||
|
green: value.green.clone(),
|
||||||
|
blue: value.blue.clone(),
|
||||||
|
transp: value.transp.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BitfieldShort = (u32, u32);
|
||||||
|
type FbInfoShort = (BitfieldShort, BitfieldShort, BitfieldShort, BitfieldShort);
|
||||||
|
|
||||||
|
const fn tuple_to_bitfield(v: BitfieldShort) -> Bitfield {
|
||||||
|
let (offset, length) = v;
|
||||||
|
// None of formats we support have msb_right set.
|
||||||
|
Bitfield {
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
msb_right: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a tuple of 4 tuples `(r, g, b, a)`. Each color tuple is a tuple of `(offset, length)`.
|
||||||
|
const fn rgba_bitfield(v: FbInfoShort) -> RgbaBitfield {
|
||||||
|
let (r, g, b, a) = v;
|
||||||
|
RgbaBitfield {
|
||||||
|
red: tuple_to_bitfield(r),
|
||||||
|
green: tuple_to_bitfield(g),
|
||||||
|
blue: tuple_to_bitfield(b),
|
||||||
|
transp: tuple_to_bitfield(a),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic borrowed from QT https://github.com/qt/qtbase/blob/498ae026e98ed181d1480fe5f6f2f1453a725e78/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp
|
||||||
|
|
||||||
|
const ARGB888: RgbaBitfield = rgba_bitfield(((16, 8), (8, 8), (0, 8), (24, 8)));
|
||||||
|
const ABGR888: RgbaBitfield = rgba_bitfield(((0, 8), (8, 8), (16, 8), (24, 8)));
|
||||||
|
const RGB888: RgbaBitfield = rgba_bitfield(((16, 8), (8, 8), (0, 8), (0, 0)));
|
||||||
|
const BGR888: RgbaBitfield = rgba_bitfield(((0, 8), (8, 8), (16, 8), (0, 0)));
|
||||||
|
const RGB666: RgbaBitfield = rgba_bitfield(((12, 6), (6, 6), (0, 6), (0, 0)));
|
||||||
|
const RGB565: RgbaBitfield = rgba_bitfield(((11, 5), (5, 6), (0, 5), (0, 0)));
|
||||||
|
const BGR565: RgbaBitfield = rgba_bitfield(((0, 5), (5, 6), (11, 5), (0, 0)));
|
||||||
|
const RGB555: RgbaBitfield = rgba_bitfield(((10, 5), (5, 5), (0, 5), (0, 0)));
|
||||||
|
const BGR555: RgbaBitfield = rgba_bitfield(((0, 5), (5, 5), (10, 5), (0, 0)));
|
||||||
|
const RGB444: RgbaBitfield = rgba_bitfield(((8, 4), (4, 4), (0, 4), (0, 0)));
|
||||||
|
|
||||||
|
fn determine_depth(vinfo: &VarScreeninfo) -> u32 {
|
||||||
|
let depth = vinfo.red.length + vinfo.green.length + vinfo.blue.length;
|
||||||
|
match vinfo.bits_per_pixel {
|
||||||
|
24 if depth == 0 => 24,
|
||||||
|
16 if depth == 0 => 16,
|
||||||
|
24 | 16 => depth,
|
||||||
|
v => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn determine_format(vinfo: VarScreeninfo) -> Option<FbFormat> {
|
||||||
|
let rgba = RgbaBitfield::from(&vinfo);
|
||||||
|
let depth = determine_depth(&vinfo);
|
||||||
|
|
||||||
|
match (depth, rgba) {
|
||||||
|
(32, ARGB888) => Some(FbFormat::ARGB888),
|
||||||
|
(32, ABGR888) => Some(FbFormat::ABGR888),
|
||||||
|
(24, RGB888) => Some(FbFormat::RGB888),
|
||||||
|
(24, BGR888) => Some(FbFormat::BGR888),
|
||||||
|
(18, RGB666) => Some(FbFormat::RGB666),
|
||||||
|
(16, RGB565) => Some(FbFormat::RGB565),
|
||||||
|
(16, BGR565) => Some(FbFormat::BGR565),
|
||||||
|
(15, RGB555) => Some(FbFormat::RGB555),
|
||||||
|
(15, BGR555) => Some(FbFormat::BGR555),
|
||||||
|
(12, RGB444) => Some(FbFormat::RGB444),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,40 +1,15 @@
|
|||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::display::DisplayState;
|
use crate::display::DisplayState;
|
||||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
use crate::display::generic_framebuffer;
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
|
use super::generic_framebuffer::FramebufferDevice;
|
||||||
|
|
||||||
const FB_PATH: &str = "/dev/fb0";
|
const FB_PATH: &str = "/dev/fb0";
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
|
||||||
struct Framebuffer;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl GenericFramebuffer for Framebuffer {
|
|
||||||
fn dimensions(&self) -> Dimensions {
|
|
||||||
// TODO actually poll for this, maybe w/ fbset?
|
|
||||||
Dimensions {
|
|
||||||
height: 128,
|
|
||||||
width: 128,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
|
||||||
let mut raw_buffer = Vec::new();
|
|
||||||
for (r, g, b) in buffer {
|
|
||||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
|
||||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
|
||||||
rgb565 |= (b as u16) >> 3;
|
|
||||||
raw_buffer.extend(rgb565.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_ui(
|
pub fn update_ui(
|
||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
@@ -44,7 +19,7 @@ pub fn update_ui(
|
|||||||
generic_framebuffer::update_ui(
|
generic_framebuffer::update_ui(
|
||||||
task_tracker,
|
task_tracker,
|
||||||
config,
|
config,
|
||||||
Framebuffer,
|
FramebufferDevice::new(FB_PATH, None, None),
|
||||||
ui_shutdown_rx,
|
ui_shutdown_rx,
|
||||||
ui_update_rx,
|
ui_update_rx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsRawFd;
|
||||||
use tokio::fs::OpenOptions;
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::display::DisplayState;
|
use crate::display::DisplayState;
|
||||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
use crate::display::generic_framebuffer;
|
||||||
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
const FB_PATH: &str = "/dev/fb0";
|
use super::generic_framebuffer::{FbInner, FramebufferDevice};
|
||||||
|
|
||||||
struct Framebuffer;
|
const FB_PATH: &str = "/dev/fb0";
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct fb_fillrect {
|
struct fb_fillrect {
|
||||||
@@ -25,54 +22,28 @@ struct fb_fillrect {
|
|||||||
rop: u32,
|
rop: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
fn update_display(fb: &mut FbInner, buffer: &[(u8, u8, u8)]) {
|
||||||
impl GenericFramebuffer for Framebuffer {
|
let width = fb.dims.width;
|
||||||
fn dimensions(&self) -> Dimensions {
|
let height = buffer.len() as u32 / width;
|
||||||
// TODO actually poll for this, maybe w/ fbset?
|
let mut arg = fb_fillrect {
|
||||||
Dimensions {
|
dx: 0,
|
||||||
height: 128,
|
dy: 0,
|
||||||
width: 128,
|
width,
|
||||||
}
|
height,
|
||||||
}
|
color: 0xffff, // not sure what this is
|
||||||
|
rop: 0,
|
||||||
|
};
|
||||||
|
|
||||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
unsafe {
|
||||||
// for how to write to the buffer, consult M7350v5_en_gpl/bootable/recovery/recovery_color_oled.c
|
let res = libc::ioctl(
|
||||||
let dimensions = self.dimensions();
|
fb.fd.as_raw_fd(),
|
||||||
let width = dimensions.width;
|
0x4619, // FBIORECT_DISPLAY
|
||||||
let height = buffer.len() as u32 / width;
|
&mut arg as *mut _,
|
||||||
let mut f = OpenOptions::new().write(true).open(FB_PATH).await.unwrap();
|
std::mem::size_of::<fb_fillrect>(),
|
||||||
let mut arg = fb_fillrect {
|
);
|
||||||
dx: 0,
|
|
||||||
dy: 0,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
color: 0xffff, // not sure what this is
|
|
||||||
rop: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut raw_buffer = Vec::new();
|
if res < 0 {
|
||||||
for (r, g, b) in buffer {
|
panic!("failed to send FBIORECT_DISPLAY ioctl, {res}");
|
||||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
|
||||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
|
||||||
rgb565 |= (b as u16) >> 3;
|
|
||||||
// note: big-endian!
|
|
||||||
raw_buffer.extend(rgb565.to_be_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
f.write_all(&raw_buffer).await.unwrap();
|
|
||||||
|
|
||||||
// ioctl is a synchronous operation, but it's fast enough that it shouldn't block
|
|
||||||
unsafe {
|
|
||||||
let res = libc::ioctl(
|
|
||||||
f.as_raw_fd(),
|
|
||||||
0x4619, // FBIORECT_DISPLAY
|
|
||||||
&mut arg as *mut _,
|
|
||||||
std::mem::size_of::<fb_fillrect>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if res < 0 {
|
|
||||||
panic!("failed to send FBIORECT_DISPLAY ioctl, {res}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +57,7 @@ pub fn update_ui(
|
|||||||
generic_framebuffer::update_ui(
|
generic_framebuffer::update_ui(
|
||||||
task_tracker,
|
task_tracker,
|
||||||
config,
|
config,
|
||||||
Framebuffer,
|
FramebufferDevice::new(FB_PATH, None, Some(Box::new(update_display))),
|
||||||
ui_shutdown_rx,
|
ui_shutdown_rx,
|
||||||
ui_update_rx,
|
ui_update_rx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,45 +1,21 @@
|
|||||||
use crate::config;
|
|
||||||
use crate::display::DisplayState;
|
|
||||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
|
||||||
/// Display support for the Wingtech CT2MHS01 hotspot.
|
/// Display support for the Wingtech CT2MHS01 hotspot.
|
||||||
///
|
///
|
||||||
/// Tested on (from `/etc/wt_version`):
|
/// Tested on (from `/etc/wt_version`):
|
||||||
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
||||||
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
||||||
/// WT_HARDWARE_VERSION=89323_1_20
|
/// WT_HARDWARE_VERSION=89323_1_20
|
||||||
use async_trait::async_trait;
|
use crate::config;
|
||||||
|
use crate::display::DisplayState;
|
||||||
|
use crate::display::generic_framebuffer;
|
||||||
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
|
use super::generic_framebuffer::FramebufferDevice;
|
||||||
|
|
||||||
const FB_PATH: &str = "/dev/fb0";
|
const FB_PATH: &str = "/dev/fb0";
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
|
||||||
struct Framebuffer;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl GenericFramebuffer for Framebuffer {
|
|
||||||
fn dimensions(&self) -> Dimensions {
|
|
||||||
Dimensions {
|
|
||||||
height: 128,
|
|
||||||
width: 160,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
|
||||||
let mut raw_buffer = Vec::new();
|
|
||||||
for (r, g, b) in buffer {
|
|
||||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
|
||||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
|
||||||
rgb565 |= (b as u16) >> 3;
|
|
||||||
raw_buffer.extend(rgb565.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_ui(
|
pub fn update_ui(
|
||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
@@ -49,7 +25,7 @@ pub fn update_ui(
|
|||||||
generic_framebuffer::update_ui(
|
generic_framebuffer::update_ui(
|
||||||
task_tracker,
|
task_tracker,
|
||||||
config,
|
config,
|
||||||
Framebuffer,
|
FramebufferDevice::new(FB_PATH, None, None),
|
||||||
ui_shutdown_rx,
|
ui_shutdown_rx,
|
||||||
ui_update_rx,
|
ui_update_rx,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -229,7 +229,6 @@ async fn run_with_config(
|
|||||||
&task_tracker,
|
&task_tracker,
|
||||||
dev,
|
dev,
|
||||||
diag_rx,
|
diag_rx,
|
||||||
diag_tx.clone(),
|
|
||||||
ui_update_tx.clone(),
|
ui_update_tx.clone(),
|
||||||
qmdl_store_lock.clone(),
|
qmdl_store_lock.clone(),
|
||||||
analysis_tx.clone(),
|
analysis_tx.clone(),
|
||||||
@@ -285,6 +284,7 @@ async fn run_with_config(
|
|||||||
config,
|
config,
|
||||||
qmdl_store_lock: qmdl_store_lock.clone(),
|
qmdl_store_lock: qmdl_store_lock.clone(),
|
||||||
diag_device_ctrl_sender: diag_tx,
|
diag_device_ctrl_sender: diag_tx,
|
||||||
|
ui_update_sender: ui_update_tx,
|
||||||
analysis_status_lock,
|
analysis_status_lock,
|
||||||
analysis_sender: analysis_tx,
|
analysis_sender: analysis_tx,
|
||||||
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
|
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
|
||||||
|
|||||||
+12
-14
@@ -56,6 +56,12 @@ pub struct ManifestEntry {
|
|||||||
pub arch: Option<String>,
|
pub arch: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub enum EntryType {
|
||||||
|
Current,
|
||||||
|
Past,
|
||||||
|
}
|
||||||
|
|
||||||
impl ManifestEntry {
|
impl ManifestEntry {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
@@ -341,31 +347,23 @@ impl RecordingStore {
|
|||||||
Some((entry_index, &self.manifest.entries[entry_index]))
|
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_current_entry(&self, name: &str) -> bool {
|
pub async fn delete_entry(&mut self, name: &str) -> Result<EntryType, RecordingStoreError> {
|
||||||
match self.current_entry {
|
|
||||||
Some(idx) => match self.manifest.entries.get(idx) {
|
|
||||||
Some(entry) => entry.name == name,
|
|
||||||
None => false,
|
|
||||||
},
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_entry(&mut self, name: &str) -> Result<(), RecordingStoreError> {
|
|
||||||
let entry_to_delete_idx = self
|
let entry_to_delete_idx = self
|
||||||
.manifest
|
.manifest
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|entry| entry.name == name)
|
.position(|entry| entry.name == name)
|
||||||
.ok_or(RecordingStoreError::NoSuchEntryError)?;
|
.ok_or(RecordingStoreError::NoSuchEntryError)?;
|
||||||
match self.current_entry {
|
let is_current = match self.current_entry {
|
||||||
Some(current_entry) if current_entry == entry_to_delete_idx => {
|
Some(current_entry) if current_entry == entry_to_delete_idx => {
|
||||||
self.close_current_entry().await?;
|
self.close_current_entry().await?;
|
||||||
|
EntryType::Current
|
||||||
}
|
}
|
||||||
Some(current_entry) => {
|
Some(current_entry) => {
|
||||||
self.current_entry = Some(current_entry - 1);
|
self.current_entry = Some(current_entry - 1);
|
||||||
|
EntryType::Past
|
||||||
}
|
}
|
||||||
None => {}
|
None => EntryType::Past,
|
||||||
};
|
};
|
||||||
let entry_to_delete = self.manifest.entries.remove(entry_to_delete_idx);
|
let entry_to_delete = self.manifest.entries.remove(entry_to_delete_idx);
|
||||||
self.write_manifest().await?;
|
self.write_manifest().await?;
|
||||||
@@ -377,7 +375,7 @@ impl RecordingStore {
|
|||||||
remove_file_if_exists(&analysis_filepath)
|
remove_file_if_exists(&analysis_filepath)
|
||||||
.await
|
.await
|
||||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||||
Ok(())
|
Ok(is_current)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_all_entries(&mut self) -> Result<(), RecordingStoreError> {
|
pub async fn delete_all_entries(&mut self) -> Result<(), RecordingStoreError> {
|
||||||
|
|||||||
@@ -18,17 +18,18 @@ use tokio::sync::{RwLock, oneshot};
|
|||||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
use crate::DiagDeviceCtrlMessage;
|
|
||||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::pcap::generate_pcap_data;
|
use crate::pcap::generate_pcap_data;
|
||||||
use crate::qmdl_store::RecordingStore;
|
use crate::qmdl_store::RecordingStore;
|
||||||
|
use crate::{DiagDeviceCtrlMessage, display};
|
||||||
|
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub config_path: String,
|
pub config_path: String,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
|
pub ui_update_sender: Sender<display::DisplayState>,
|
||||||
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||||
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
||||||
@@ -292,6 +293,7 @@ mod tests {
|
|||||||
store_lock: Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
store_lock: Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
||||||
) -> Arc<ServerState> {
|
) -> Arc<ServerState> {
|
||||||
let (tx, _rx) = tokio::sync::mpsc::channel(1);
|
let (tx, _rx) = tokio::sync::mpsc::channel(1);
|
||||||
|
let (ui_tx, _ui_rx) = tokio::sync::mpsc::channel(1);
|
||||||
let (analysis_tx, _analysis_rx) = tokio::sync::mpsc::channel(1);
|
let (analysis_tx, _analysis_rx) = tokio::sync::mpsc::channel(1);
|
||||||
|
|
||||||
let analysis_status = {
|
let analysis_status = {
|
||||||
@@ -304,6 +306,7 @@ mod tests {
|
|||||||
config: Config::default(),
|
config: Config::default(),
|
||||||
qmdl_store_lock: store_lock,
|
qmdl_store_lock: store_lock,
|
||||||
diag_device_ctrl_sender: tx,
|
diag_device_ctrl_sender: tx,
|
||||||
|
ui_update_sender: ui_tx,
|
||||||
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
|
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
|
||||||
analysis_sender: analysis_tx,
|
analysis_sender: analysis_tx,
|
||||||
daemon_restart_tx: Arc::new(RwLock::new(None)),
|
daemon_restart_tx: Arc::new(RwLock::new(None)),
|
||||||
|
|||||||
@@ -33,49 +33,45 @@
|
|||||||
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
|
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
|
||||||
<p>Nothing to show!</p>
|
<p>Nothing to show!</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-x-scroll">
|
<table class="table-auto text-left">
|
||||||
<table class="table-auto text-left">
|
<thead class="p-2">
|
||||||
<thead class="p-2">
|
<tr class="bg-gray-300">
|
||||||
<tr class="bg-gray-300">
|
<th class="p-2">Timestamp</th>
|
||||||
<th class="p-2">Timestamp</th>
|
<th class="p-2">Heuristic</th>
|
||||||
<th class="p-2">Heuristic</th>
|
<th class="p-2">Warning</th>
|
||||||
<th class="p-2">Warning</th>
|
<th class="p-2">Severity</th>
|
||||||
<th class="p-2">Severity</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
{#each report.rows as row}
|
||||||
{#each report.rows as row}
|
{#if row.type === AnalysisRowType.Analysis}
|
||||||
{#if row.type === AnalysisRowType.Analysis}
|
{@const parsed_date = new Date(row.packet_timestamp)}
|
||||||
{@const parsed_date = new Date(row.packet_timestamp)}
|
{#each row.events.filter((e) => e !== null) as event, i}
|
||||||
{#each row.events.filter((e) => e !== null) as event, i}
|
{@const analyzer = analyzers[i]}
|
||||||
{@const analyzer = analyzers[i]}
|
<tr class="even:bg-gray-200 odd:bg-white">
|
||||||
<tr class="even:bg-gray-200 odd:bg-white">
|
{#if event.type === EventType.Warning}
|
||||||
{#if event.type === EventType.Warning}
|
{@const severity = ['Low', 'Medium', 'High'][event.severity]}
|
||||||
{@const severity = ['Low', 'Medium', 'High'][
|
{@const severity_class = [
|
||||||
event.severity
|
'bg-red-200',
|
||||||
]}
|
'bg-red-400',
|
||||||
{@const severity_class = [
|
'bg-red-600',
|
||||||
'bg-red-200',
|
][event.severity]}
|
||||||
'bg-red-400',
|
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||||
'bg-red-600',
|
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||||
][event.severity]}
|
<td class="p-2">{event.message}</td>
|
||||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
<td class="p-2 {severity_class} text-center">{severity}</td>
|
||||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
{:else if event.type === EventType.Informational}
|
||||||
<td class="p-2">{event.message}</td>
|
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||||
<td class="p-2 {severity_class} text-center">{severity}</td>
|
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||||
{:else if event.type === EventType.Informational}
|
<td class="p-2">{event.message}</td>
|
||||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
<td class="p-2">Info</td>
|
||||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
{/if}
|
||||||
<td class="p-2">{event.message}</td>
|
</tr>
|
||||||
<td class="p-2">Info</td>
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</tr>
|
{/each}
|
||||||
{/each}
|
</tbody>
|
||||||
{/if}
|
</table>
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if report.statistics.num_skipped_packets > 0}
|
{#if report.statistics.num_skipped_packets > 0}
|
||||||
@@ -85,23 +81,21 @@
|
|||||||
These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a
|
These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a
|
||||||
problem.
|
problem.
|
||||||
</p>
|
</p>
|
||||||
<div class="overflow-x-scroll">
|
<table class="table-auto text-left">
|
||||||
<table class="table-auto text-left">
|
<thead class="p-2">
|
||||||
<thead class="p-2">
|
<tr class="bg-gray-300">
|
||||||
<tr class="bg-gray-300">
|
<th scope="col" class="p-2">Total Msgs Affected</th>
|
||||||
<th scope="col" class="p-2">Total Msgs Affected</th>
|
<th scope="col">Reason/Error</th>
|
||||||
<th scope="col">Reason/Error</th>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each skipped_messages.entries() as [message, count]}
|
||||||
|
<tr class="even:bg-gray-200 odd:bg-white">
|
||||||
|
<td class="text-center">{count}</td>
|
||||||
|
<td>{message}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
{/each}
|
||||||
<tbody>
|
</tbody>
|
||||||
{#each skipped_messages.entries() as [message, count]}
|
</table>
|
||||||
<tr class="even:bg-gray-200 odd:bg-white">
|
|
||||||
<td class="text-center">{count}</td>
|
|
||||||
<td>{message}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row"
|
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md flex flex-row"
|
||||||
onclick={confirmDelete}
|
onclick={confirmDelete}
|
||||||
aria-label="delete"
|
aria-label="delete"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex flex-row {full_button
|
class="flex flex-row {full_button
|
||||||
? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md'
|
? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md'
|
||||||
: 'text-blue-600 underline'}"
|
: 'text-blue-600 underline'}"
|
||||||
onclick={download}
|
onclick={download}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 overflow-x-scroll overflow-y-hidden"
|
class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1"
|
||||||
>
|
>
|
||||||
{#if current}
|
{#if current}
|
||||||
<div class="flex flex-row justify-between gap-2">
|
<div class="flex flex-row justify-between gap-2">
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
'N/A'}</span
|
'N/A'}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-scroll">
|
<div class="flex flex-row justify-between lg:justify-end gap-2 mt-2">
|
||||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
|
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
|
||||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
|
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
|
||||||
<DownloadLink url={entry.get_zip_url()} text="zip" full_button />
|
<DownloadLink url={entry.get_zip_url()} text="zip" full_button />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const recording_button_classes =
|
const recording_button_classes =
|
||||||
'text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row gap-1';
|
'text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1';
|
||||||
const stop_recording_classes = `${recording_button_classes} bg-red-500 opacity-50 cursor-not-allowed`;
|
const stop_recording_classes = `${recording_button_classes} bg-red-500 opacity-50 cursor-not-allowed`;
|
||||||
const start_recording_classes = `${recording_button_classes} bg-blue-500 opacity-50 cursor-not-allowed`;
|
const start_recording_classes = `${recording_button_classes} bg-blue-500 opacity-50 cursor-not-allowed`;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+4
-4
@@ -25,15 +25,15 @@ If you want to use a non-Verizon SIM card you will probably need an unlocked dev
|
|||||||
Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
|
Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./installer util shell "echo 9 > /usrdata/mode.cfg"
|
installer util shell "echo 9 > /usrdata/mode.cfg"
|
||||||
./installer util shell reboot
|
installer util shell reboot
|
||||||
```
|
```
|
||||||
|
|
||||||
To disable tethering again:
|
To disable tethering again:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./installer util shell "echo 3 > /usrdata/mode.cfg"
|
installer util shell "echo 3 > /usrdata/mode.cfg"
|
||||||
./installer util shell reboot
|
installer util shell reboot
|
||||||
```
|
```
|
||||||
|
|
||||||
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
|
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
|
||||||
|
|||||||
+2
-4
@@ -35,14 +35,12 @@ The modem is fully capable of running Rayhunter, but lacks both a screen and a n
|
|||||||
Note that the Quectel EG25-G does not support LTE band 48 (CBRS 3500MHz), used in the US for unlicensed 4G/5G connectivity.
|
Note that the Quectel EG25-G does not support LTE band 48 (CBRS 3500MHz), used in the US for unlicensed 4G/5G connectivity.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
Download and extract the installer *on a shell on the PinePhone itself*. Unlike other Rayhunter installers, this has to be run on the device itself. Then run:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./installer pinephone
|
./installer pinephone
|
||||||
```
|
```
|
||||||
|
|
||||||
## Accessing Rayhunter
|
## Accessing rayhunter
|
||||||
Because the modem does not have its own display or network interface, Rayhunter is only accessible on the pinephone by forwarding tcp over adb.
|
Because the modem does not have its own display or network interface, rayhunter is only accessible on the pinephone by forwarding tcp over adb.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
adb forward tcp:8080 tcp:8080
|
adb forward tcp:8080 tcp:8080
|
||||||
|
|||||||
@@ -67,38 +67,3 @@ WT_HARDWARE_VERSION=89323_1_20
|
|||||||
```
|
```
|
||||||
|
|
||||||
Please consider sharing the contents of your device's /etc/wt_version file here.
|
Please consider sharing the contents of your device's /etc/wt_version file here.
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### My hotspot won't turn on after rebooting when installing over WiFi
|
|
||||||
|
|
||||||
Reinsert the battery and turn the device back on, Rayhunter should be installed and running. Sometimes the Wingtech hotspot gets stuck off and ignores the power button after a reboot until the battery is reseated.
|
|
||||||
|
|
||||||
You do not need to run the installer again.
|
|
||||||
|
|
||||||
You'll likely see the following messages, where the installer is stuck at `Testing rayhunter ... `.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
Starting telnet ... ok
|
|
||||||
Connecting via telnet to 192.168.1.1 ... ok
|
|
||||||
Sending file /data/rayhunter/config.toml ... ok
|
|
||||||
Sending file /data/rayhunter/rayhunter-daemon ... ok
|
|
||||||
Sending file /etc/init.d/rayhunter_daemon ... ok
|
|
||||||
Rebooting device and waiting 30 seconds for it to start up.
|
|
||||||
Testing rayhunter ...
|
|
||||||
```
|
|
||||||
|
|
||||||
If you eventually see:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
Testing rayhunter ...
|
|
||||||
Failed to install rayhunter on the Wingtech CT2MHS01
|
|
||||||
|
|
||||||
Caused by:
|
|
||||||
0: error sending request for url (http://192.168.1.1:8080/index.html)
|
|
||||||
1: client error (Connect)
|
|
||||||
2: tcp connect error: Network is unreachable (os error 101)
|
|
||||||
3: Network is unreachable (os error 101)
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure your computer is connected to the hotspot's wifi network.
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "installer"
|
name = "installer"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#[cfg(target_os = "windows")]
|
|
||||||
use std::io::stdin;
|
|
||||||
|
|
||||||
use std::io::{ErrorKind, Write};
|
use std::io::{ErrorKind, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -33,13 +30,6 @@ On macOS or windows this might be caused by another program using the Orbic.
|
|||||||
Please close any program that might be using your Orbic.
|
Please close any program that might be using your Orbic.
|
||||||
If you have adb installed you may need to kill the adb daemon"#;
|
If you have adb installed you may need to kill the adb daemon"#;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
const WINDOWS_WARNING: &str = r#""WINDOWS IS NOT FULLY SUPPORTED
|
|
||||||
|
|
||||||
THIS MAY BRICK YOUR DEVICE
|
|
||||||
|
|
||||||
PLEASE INSTALL FROM MACOS OR LINUX INSTEAD IF POSSIBLE"#;
|
|
||||||
|
|
||||||
const VENDOR_ID: u16 = 0x05c6;
|
const VENDOR_ID: u16 = 0x05c6;
|
||||||
const PRODUCT_ID: u16 = 0xf601;
|
const PRODUCT_ID: u16 = 0xf601;
|
||||||
|
|
||||||
@@ -51,25 +41,7 @@ const RNDIS_INTERFACE: u8 = 0;
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
const RNDIS_INTERFACE: u8 = 1;
|
const RNDIS_INTERFACE: u8 = 1;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn confirm() -> Result<bool> {
|
|
||||||
println!("{}", WINDOWS_WARNING);
|
|
||||||
echo!("Do you wish to proceed? Enter 'yes' to install> ");
|
|
||||||
let mut input = String::new();
|
|
||||||
stdin().read_line(&mut input)?;
|
|
||||||
Ok(input.trim() == "yes")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn install() -> Result<()> {
|
pub async fn install() -> Result<()> {
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let confirmation = confirm().await?;
|
|
||||||
if confirmation != true {
|
|
||||||
println!("Install aborted. Your device has not been modified.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut adb_device = force_debug_mode().await?;
|
let mut adb_device = force_debug_mode().await?;
|
||||||
echo!("Installing rootshell... ");
|
echo!("Installing rootshell... ");
|
||||||
setup_rootshell(&mut adb_device).await?;
|
setup_rootshell(&mut adb_device).await?;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ pub async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Res
|
|||||||
.context("login did not return a token in response")?;
|
.context("login did not return a token in response")?;
|
||||||
|
|
||||||
let command = client.post(&qcmap_web_cgi_endpoint)
|
let command = client.post(&qcmap_web_cgi_endpoint)
|
||||||
.body(format!("page=setFWMacFilter&cmd=del&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}"))
|
.body(format!("page=setFWMacFilter&cmd=add&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}"))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
if command.status() != 200 {
|
if command.status() != 200 {
|
||||||
@@ -135,7 +135,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul
|
|||||||
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
|
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
|
||||||
|
|
||||||
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||||
telnet_send_command(addr, "shutdown -r -t 1 now", "exit code 0").await?;
|
telnet_send_command(addr, "reboot", "exit code 0").await?;
|
||||||
sleep(Duration::from_secs(30)).await;
|
sleep(Duration::from_secs(30)).await;
|
||||||
|
|
||||||
echo!("Testing rayhunter ... ");
|
echo!("Testing rayhunter ... ");
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter"
|
name = "rayhunter"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rootshell"
|
name = "rootshell"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telcom-parser"
|
name = "telcom-parser"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
Reference in New Issue
Block a user