Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
eb5469d
refactor(camera_hub): replace OpenSSL dep with rustls; update Rust to…
jkaczman May 4, 2026
371c390
refactor(motion_ai): initialize ort with onnxruntime dylib
jkaczman May 4, 2026
f47d7d1
refactor(camera_hub): use /provision for camera_secret/wifi_password …
jkaczman May 6, 2026
e0362fe
refactor(releases): remove config_tool & updater from release profile
jkaczman May 6, 2026
c5a2019
feat(update): use top-level signatures for release-binaries
jkaczman May 6, 2026
ca9d13c
feat(deploy): use Secluso OS image via secret injection
jkaczman May 6, 2026
76dd738
fix(motion_ai): use onnxruntime dylib symlink
jkaczman May 6, 2026
7adc6f6
feat(camera_hub): continuously try to create wifi hotspot upon failur…
jkaczman May 6, 2026
d2dd96f
feat(camera_hub/app_native): pass Secluso OS version to app
jkaczman May 6, 2026
128233c
fix(camera_hub): emit 0 when Secluso OS version is unknown
jkaczman May 7, 2026
5d8106f
feat(server): ask to add ufw rule
jkaczman May 7, 2026
78d3394
feat(server): remove OpenSSL dependency in web-push
jkaczman May 7, 2026
00bfc55
refactor(update): use secluso-<tag>-sha256sums.txt format
jkaczman May 7, 2026
c1926e6
refactor(releases): don't emit IP-camera hub; use 'secluso-camera-hub…
jkaczman May 7, 2026
c5ff09d
feat(deploy/server): make auto-updater mandatory (for now); pass user…
jkaczman May 7, 2026
db8d0a2
refactor(camera_hub): use semantic versioning for Secluso OS version …
jkaczman May 7, 2026
7a1b8b0
feat(camera_hub): attempt to send pairing token multiple times
jkaczman May 7, 2026
511581c
fix(camera_hub): write component-scope version marker
jkaczman May 7, 2026
642955c
feat(deploy): functionality to show if latest; prompt if not sure
jkaczman May 8, 2026
773c4f4
refactor(releases): remove arm64 Linux deploy tool build from releases
jkaczman May 8, 2026
fa1c401
fix(example_app): use current_version in legacy places for test workflow
jkaczman May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
camera_hub/pending_videos/
camera_hub/state/
releases/builds/
release_work/
motion_ai/cli/output
camera_hub/output
**/target
**/target
*.wic
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test_data
cameras.yaml
credentials_full
*.sh
!update/scripts/secluso_sign_checksums.sh

# Firebase Service Files
google-services.json
Expand All @@ -41,3 +42,5 @@ secluso-v*
# Exclude local release staging outputs and imported signing material
/releases/release_assets/
*.p12
__pycache__
*.wic
16 changes: 4 additions & 12 deletions app_native/examples/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,19 +344,11 @@ fn heartbeat(
config_response.clone(),
timestamp,
) {
Ok(response) if response.contains("healthy") => {
Ok(response) if response.contains("\"status\":\"healthy\"") => {
println!("Healthy heartbeat");

if let Some((_, firmware_version)) = response.split_once('_') {
println!("firmware_version = {firmware_version}");
} else {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Error: unknown firmware version."),
));
}
println!("{response}");
}
Ok(response) if response == "invalid ciphertext".to_string() => {
Ok(response) if response.contains("\"status\":\"invalid ciphertext\"") => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("The connection to the camera is corrupted. Pair the app with the camera again."),
Expand Down Expand Up @@ -586,4 +578,4 @@ fn read_varying_len(stream: &mut TcpStream) -> io::Result<Vec<u8>> {
}

Ok(msg)
}
}
67 changes: 50 additions & 17 deletions app_native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use log::{debug, error, info};
use rand::distr::Alphanumeric;
use rand::Rng;
use secluso_client_lib::config::{
Heartbeat, HeartbeatRequest, HeartbeatResult, OPCODE_HEARTBEAT_REQUEST, OPCODE_HEARTBEAT_RESPONSE,
CameraVersionInfo, Heartbeat, HeartbeatRequest, HeartbeatResult, OPCODE_HEARTBEAT_REQUEST, OPCODE_HEARTBEAT_RESPONSE,
AddAppRequest, AddAppResponseCommon, AddAppResponseDedicated, OPCODE_ADD_APP_REQUEST, OPCODE_ADD_APP_RESPONSE,
};
use secluso_client_lib::mls_client::{Contact, MlsClient, ClientType};
Expand Down Expand Up @@ -44,6 +44,13 @@ const CAMERA_IO_TIMEOUT: Duration = Duration::from_secs(12);
const CAMERA_CONNECT_RETRIES: usize = 3;
const CAMERA_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(350);

#[derive(Serialize)]
struct HeartbeatStatus {
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
version_info: Option<CameraVersionInfo>,
}

#[flutter_rust_bridge::frb]
pub struct Clients {
mls_clients: MlsClients,
Expand Down Expand Up @@ -199,14 +206,14 @@ fn send_credentials_full(
Ok(())
}

fn receive_firmware_version(
fn receive_camera_version_info(
stream: &mut TcpStream,
) -> anyhow::Result<String> {
info!("Sending credentials_full");
let firmware_version_bytes = read_varying_len(stream)?;
let firmware_version = String::from_utf8(firmware_version_bytes)?;
) -> anyhow::Result<CameraVersionInfo> {
info!("Receiving camera version info");
let version_info_bytes = read_varying_len(stream)?;
let version_info = serde_json::from_slice::<CameraVersionInfo>(&version_info_bytes)?;

Ok(firmware_version)
Ok(version_info)
}

fn send_timestamp(
Expand Down Expand Up @@ -371,18 +378,21 @@ pub fn add_camera(
}

info!("Waiting for firmware version from camera");
let firmware_version =
match receive_firmware_version(&mut stream) {
Ok(version) => version,
let version_info =
match receive_camera_version_info(&mut stream) {
Ok(version_info) => version_info,
Err(e) => {
info!("Error (firmware): {e}");
return "Error".to_string();
}
};

let app_native_version = format!("v{}", env!("CARGO_PKG_VERSION"));
info!("Camera version = {}, app native version = {}", firmware_version, app_native_version);
if app_native_version != firmware_version {
info!(
"Camera firmware version = {}, camera OS version = {}, app native version = {}",
version_info.firmware_version, version_info.os_version, app_native_version
);
if app_native_version != version_info.firmware_version {
return "PairVersionIncompatible".to_string();
}

Expand Down Expand Up @@ -425,7 +435,13 @@ pub fn add_camera(
}
}

firmware_version
match serde_json::to_string(&version_info) {
Ok(version_info_json) => version_info_json,
Err(e) => {
info!("Error (version-info-json): {e}");
"Error".to_string()
}
}
}

pub fn initialize(
Expand Down Expand Up @@ -725,11 +741,28 @@ pub fn process_heartbeat_config_response(

match heartbeat_result {
HeartbeatResult::HealthyHeartbeat(_timestamp) => {
Ok(format!("healthy_{}", heartbeat.firmware_version))
let status = HeartbeatStatus {
status: "healthy".to_string(),
version_info: Some(CameraVersionInfo {
firmware_version: heartbeat.firmware_version,
os_version: heartbeat.os_version,
}),
};
serde_json::to_string(&status)
.map_err(|e| io::Error::other(e.to_string()))
}
HeartbeatResult::InvalidTimestamp => Ok("invalid timestamp".to_string()),
HeartbeatResult::InvalidCiphertext => Ok("invalid ciphertext".to_string()),
HeartbeatResult::InvalidEpoch => Ok("invalid epoch".to_string()),
HeartbeatResult::InvalidTimestamp => Ok(serde_json::to_string(&HeartbeatStatus {
status: "invalid timestamp".to_string(),
version_info: None,
}).unwrap()),
HeartbeatResult::InvalidCiphertext => Ok(serde_json::to_string(&HeartbeatStatus {
status: "invalid ciphertext".to_string(),
version_info: None,
}).unwrap()),
HeartbeatResult::InvalidEpoch => Ok(serde_json::to_string(&HeartbeatStatus {
status: "invalid epoch".to_string(),
version_info: None,
}).unwrap()),
}
}
_ => {
Expand Down
Loading
Loading