mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
refactor: remove hardware-related components and streamline service management (#502)
* feat(config): introduce pre-login user directory structure - Added support for a pre-login user directory to encapsulate configuration, memory, and state before any user logs in. This ensures that all initial data is scoped under a dedicated user directory (`users/local`), preventing direct writes to the root `.openhuman` path. - Implemented the `pre_login_user_dir` function to return the appropriate path for the pre-login user. - Updated configuration loading logic to defer disk state creation until the first successful login, enhancing user data management and isolation. - Added tests to verify the correct behavior of the pre-login directory structure. * refactor: remove hardware-related components and streamline service management - Deleted hardware configuration and related tools from the codebase, including `HardwareConfig`, `HardwareTransport`, and associated memory management tools. - Introduced a new `service.ts` module for managing service and daemon commands, consolidating service-related functionalities. - Updated import paths across the application to reflect the removal of hardware references and the addition of the new service management module. - Refactored the `build_system_prompt` function to remove hardware access instructions, focusing on action instructions instead. - Cleaned up the Cargo.toml and Cargo.lock files by removing unused dependencies related to hardware management. * chore: apply formatting and tauri lockfile sync * refactor(tests): extract config file writing logic into a reusable function - Introduced a `write_config_file` function to encapsulate the logic for creating directories and writing configuration files, improving code reuse and readability. - Updated test cases to utilize the new function for writing configuration files, ensuring consistency and reducing duplication. - Added handling for pre-login user directory structure to ensure configuration is correctly written to the appropriate paths.
This commit is contained in:
Generated
+12
-734
File diff suppressed because it is too large
Load Diff
@@ -95,9 +95,6 @@ rdev = "0.5"
|
||||
matrix-sdk = { version = "0.16", optional = true, default-features = false, features = ["e2e-encryption", "rustls-tls", "markdown"] }
|
||||
fantoccini = { version = "0.22.0", optional = true, default-features = false, features = ["rustls-tls"] }
|
||||
serde-big-array = { version = "0.5", optional = true }
|
||||
nusb = { version = "0.2", default-features = false, optional = true }
|
||||
tokio-serial = { version = "5", default-features = false, optional = true }
|
||||
probe-rs = { version = "0.30", optional = true }
|
||||
pdf-extract = { version = "0.10", optional = true }
|
||||
wa-rs = { version = "0.2", optional = true, default-features = false }
|
||||
wa-rs-core = { version = "0.2", optional = true, default-features = false }
|
||||
@@ -115,13 +112,11 @@ rppal = { version = "0.22", optional = true }
|
||||
[features]
|
||||
sandbox-landlock = ["dep:landlock"]
|
||||
sandbox-bubblewrap = []
|
||||
hardware = ["dep:nusb", "dep:tokio-serial"]
|
||||
channel-matrix = ["dep:matrix-sdk"]
|
||||
peripheral-rpi = ["dep:rppal"]
|
||||
browser-native = ["dep:fantoccini"]
|
||||
fantoccini = ["browser-native"]
|
||||
landlock = ["sandbox-landlock"]
|
||||
probe = ["dep:probe-rs"]
|
||||
rag-pdf = ["dep:pdf-extract"]
|
||||
whatsapp-web = ["dep:wa-rs", "dep:wa-rs-core", "dep:wa-rs-binary", "dep:wa-rs-proto", "dep:wa-rs-ureq-http", "dep:wa-rs-tokio-transport", "serde-big-array"]
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "OpenHuman"
|
||||
version = "0.52.3"
|
||||
version = "0.52.4"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"log",
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { User } from '../../types/api';
|
||||
import type { TeamInvite, TeamMember, TeamWithRole } from '../../types/team';
|
||||
import type { AccessibilityStatus } from '../../utils/tauriCommands/accessibility';
|
||||
import type { AutocompleteStatus } from '../../utils/tauriCommands/autocomplete';
|
||||
import type { ServiceStatus } from '../../utils/tauriCommands/hardware';
|
||||
import type { LocalAiStatus } from '../../utils/tauriCommands/localAi';
|
||||
import type { ServiceStatus } from '../../utils/tauriCommands/service';
|
||||
|
||||
export interface CoreOnboardingTasks {
|
||||
accessibilityPermissionGranted: boolean;
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { User } from '../types/api';
|
||||
import type { TeamInvite, TeamMember, TeamWithRole } from '../types/team';
|
||||
import type { AccessibilityStatus } from '../utils/tauriCommands/accessibility';
|
||||
import type { AutocompleteStatus } from '../utils/tauriCommands/autocomplete';
|
||||
import type { ServiceStatus } from '../utils/tauriCommands/hardware';
|
||||
import type { LocalAiStatus } from '../utils/tauriCommands/localAi';
|
||||
import type { ServiceStatus } from '../utils/tauriCommands/service';
|
||||
import { callCoreRpc } from './coreRpcClient';
|
||||
|
||||
export interface OnboardingTasks {
|
||||
|
||||
@@ -12,7 +12,7 @@ export * from './subconscious';
|
||||
export * from './localAi';
|
||||
export * from './config';
|
||||
export * from './cron';
|
||||
export * from './hardware';
|
||||
export * from './service';
|
||||
export * from './accessibility';
|
||||
export * from './autocomplete';
|
||||
export * from './skills';
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
/**
|
||||
* Hardware and service management commands.
|
||||
* Service and daemon management commands.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { callCoreRpc } from '../../services/coreRpcClient';
|
||||
import { CommandResponse, isTauri, parseServiceCliOutput } from './common';
|
||||
|
||||
export type HardwareTransport = 'Native' | 'Serial' | 'Probe' | 'None';
|
||||
export type ServiceState = 'Running' | 'Stopped' | 'NotInstalled' | { Unknown: string };
|
||||
|
||||
export interface DiscoveredDevice {
|
||||
name: string;
|
||||
detail?: string | null;
|
||||
device_path?: string | null;
|
||||
transport: HardwareTransport;
|
||||
}
|
||||
|
||||
export interface HardwareIntrospect {
|
||||
path: string;
|
||||
vid?: number | null;
|
||||
pid?: number | null;
|
||||
board_name?: string | null;
|
||||
architecture?: string | null;
|
||||
memory_map_note: string;
|
||||
}
|
||||
|
||||
export interface ServiceStatus {
|
||||
state: ServiceState;
|
||||
unit_path?: string | null;
|
||||
@@ -47,27 +30,6 @@ export interface RestartStatus {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export async function openhumanHardwareDiscover(): Promise<CommandResponse<DiscoveredDevice[]>> {
|
||||
if (!isTauri()) {
|
||||
throw new Error('Not running in Tauri');
|
||||
}
|
||||
return await callCoreRpc<CommandResponse<DiscoveredDevice[]>>({
|
||||
method: 'openhuman.hardware_discover',
|
||||
});
|
||||
}
|
||||
|
||||
export async function openhumanHardwareIntrospect(
|
||||
path: string
|
||||
): Promise<CommandResponse<HardwareIntrospect>> {
|
||||
if (!isTauri()) {
|
||||
throw new Error('Not running in Tauri');
|
||||
}
|
||||
return await callCoreRpc<CommandResponse<HardwareIntrospect>>({
|
||||
method: 'openhuman.hardware_introspect',
|
||||
params: { path },
|
||||
});
|
||||
}
|
||||
|
||||
export async function openhumanServiceInstall(): Promise<CommandResponse<ServiceStatus>> {
|
||||
if (!isTauri()) {
|
||||
throw new Error('Not running in Tauri');
|
||||
@@ -71,28 +71,7 @@ pub fn build_system_prompt(
|
||||
.push_str("Continue reasoning with the results until you can give a final answer.\n\n");
|
||||
}
|
||||
|
||||
// ── 1b. Hardware (when gpio/arduino tools present) ───────────
|
||||
let has_hardware = tools.iter().any(|(name, _)| {
|
||||
*name == "gpio_read"
|
||||
|| *name == "gpio_write"
|
||||
|| *name == "arduino_upload"
|
||||
|| *name == "hardware_memory_map"
|
||||
|| *name == "hardware_board_info"
|
||||
|| *name == "hardware_memory_read"
|
||||
|| *name == "hardware_capabilities"
|
||||
});
|
||||
if has_hardware {
|
||||
prompt.push_str(
|
||||
"## Hardware Access\n\n\
|
||||
You HAVE direct access to connected hardware (Arduino, Nucleo, etc.). The user owns this system and has configured it.\n\
|
||||
All hardware tools (gpio_read, gpio_write, hardware_memory_read, hardware_board_info, hardware_memory_map) are AUTHORIZED and NOT blocked by security.\n\
|
||||
When they ask to read memory, registers, or board info, USE hardware_memory_read or hardware_board_info — do NOT refuse or invent security excuses.\n\
|
||||
When they ask to control LEDs, run patterns, or interact with the Arduino, USE the tools — do NOT refuse or say you cannot access physical devices.\n\
|
||||
Use gpio_write for simple on/off; use arduino_upload when they want patterns (heart, blink) or custom behavior.\n\n",
|
||||
);
|
||||
}
|
||||
|
||||
// ── 1c. Action instruction (avoid meta-summary) ───────────────
|
||||
// ── 1b. Action instruction (avoid meta-summary) ───────────────
|
||||
prompt.push_str(
|
||||
"## Your Task\n\n\
|
||||
When the user sends a message, ACT on it. Use the tools to fulfill their request.\n\
|
||||
|
||||
+11
-12
@@ -27,20 +27,19 @@ pub use schema::{
|
||||
AgentConfig, AuditConfig, AutocompleteConfig, AutonomyConfig, BrowserComputerUseConfig,
|
||||
BrowserConfig, ChannelsConfig, ClassificationRule, ComposioConfig, Config, CostConfig,
|
||||
CronConfig, DelegateAgentConfig, DictationActivationMode, DictationConfig, DiscordConfig,
|
||||
DockerRuntimeConfig, EmbeddingRouteConfig, HardwareConfig, HardwareTransport, HeartbeatConfig,
|
||||
HttpRequestConfig, IMessageConfig, IntegrationToggle, IntegrationsConfig, LarkConfig,
|
||||
LearningConfig, LocalAiConfig, MatrixConfig, MemoryConfig, ModelRouteConfig, MultimodalConfig,
|
||||
ObservabilityConfig, OrchestratorConfig, PeripheralBoardConfig, PeripheralsConfig, ProxyConfig,
|
||||
ProxyScope, QueryClassificationConfig, ReflectionSource, ReliabilityConfig,
|
||||
ResourceLimitsConfig, RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig,
|
||||
ScreenIntelligenceConfig, SecretsConfig, SecurityConfig, SlackConfig, StorageConfig,
|
||||
StorageProviderConfig, StorageProviderSection, StreamMode, TelegramConfig, UpdateConfig,
|
||||
VoiceActivationMode, VoiceServerConfig, WebSearchConfig, WebhookConfig, DEFAULT_MODEL,
|
||||
MODEL_AGENTIC_V1, MODEL_CODING_V1, MODEL_REASONING_V1,
|
||||
DockerRuntimeConfig, EmbeddingRouteConfig, HeartbeatConfig, HttpRequestConfig, IMessageConfig,
|
||||
IntegrationToggle, IntegrationsConfig, LarkConfig, LearningConfig, LocalAiConfig, MatrixConfig,
|
||||
MemoryConfig, ModelRouteConfig, MultimodalConfig, ObservabilityConfig, OrchestratorConfig,
|
||||
PeripheralBoardConfig, PeripheralsConfig, ProxyConfig, ProxyScope, QueryClassificationConfig,
|
||||
ReflectionSource, ReliabilityConfig, ResourceLimitsConfig, RuntimeConfig, SandboxBackend,
|
||||
SandboxConfig, SchedulerConfig, ScreenIntelligenceConfig, SecretsConfig, SecurityConfig,
|
||||
SlackConfig, StorageConfig, StorageProviderConfig, StorageProviderSection, StreamMode,
|
||||
TelegramConfig, UpdateConfig, VoiceActivationMode, VoiceServerConfig, WebSearchConfig,
|
||||
WebhookConfig, DEFAULT_MODEL, MODEL_AGENTIC_V1, MODEL_CODING_V1, MODEL_REASONING_V1,
|
||||
};
|
||||
pub use schema::{
|
||||
clear_active_user, default_root_openhuman_dir, read_active_user_id, user_openhuman_dir,
|
||||
write_active_user_id,
|
||||
clear_active_user, default_root_openhuman_dir, pre_login_user_dir, read_active_user_id,
|
||||
user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID,
|
||||
};
|
||||
pub use schemas::{
|
||||
all_controller_schemas as all_config_controller_schemas,
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
//! Hardware (wizard-driven) configuration.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Hardware transport mode.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
|
||||
pub enum HardwareTransport {
|
||||
#[default]
|
||||
None,
|
||||
Native,
|
||||
Serial,
|
||||
Probe,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HardwareTransport {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "none"),
|
||||
Self::Native => write!(f, "native"),
|
||||
Self::Serial => write!(f, "serial"),
|
||||
Self::Probe => write!(f, "probe"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_baud_rate() -> u32 {
|
||||
115_200
|
||||
}
|
||||
|
||||
/// Wizard-driven hardware configuration for physical world interaction.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct HardwareConfig {
|
||||
/// Whether hardware access is enabled
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Transport mode
|
||||
#[serde(default)]
|
||||
pub transport: HardwareTransport,
|
||||
/// Serial port path (e.g. "/dev/ttyACM0")
|
||||
#[serde(default)]
|
||||
pub serial_port: Option<String>,
|
||||
/// Serial baud rate
|
||||
#[serde(default = "default_baud_rate")]
|
||||
pub baud_rate: u32,
|
||||
/// Probe target chip (e.g. "STM32F401RE")
|
||||
#[serde(default)]
|
||||
pub probe_target: Option<String>,
|
||||
/// Enable workspace datasheet RAG (index PDF schematics for AI pin lookups)
|
||||
#[serde(default)]
|
||||
pub workspace_datasheets: bool,
|
||||
}
|
||||
|
||||
impl HardwareConfig {
|
||||
/// Return the active transport mode.
|
||||
pub fn transport_mode(&self) -> HardwareTransport {
|
||||
self.transport.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HardwareConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
transport: HardwareTransport::None,
|
||||
serial_port: None,
|
||||
baud_rate: default_baud_rate(),
|
||||
probe_target: None,
|
||||
workspace_datasheets: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,10 +240,21 @@ async fn resolve_runtime_config_dirs(
|
||||
));
|
||||
}
|
||||
|
||||
// 4. Default.
|
||||
// 4. Default: no login yet. Encapsulate config/memory/state under the
|
||||
// pre-login user directory so everything is user-scoped from the very
|
||||
// first init. On first real login, this directory is migrated to the
|
||||
// authenticated user id (see `credentials::ops::store_session`).
|
||||
let user_dir = pre_login_user_dir(default_openhuman_dir);
|
||||
let user_workspace = user_dir.join("workspace");
|
||||
tracing::debug!(
|
||||
user_id = %PRE_LOGIN_USER_ID,
|
||||
user_dir = %user_dir.display(),
|
||||
default_workspace_dir = %default_workspace_dir.display(),
|
||||
"Config dirs resolved to pre-login user directory (no active user, no workspace marker)"
|
||||
);
|
||||
Ok((
|
||||
default_openhuman_dir.to_path_buf(),
|
||||
default_workspace_dir.to_path_buf(),
|
||||
user_dir,
|
||||
user_workspace,
|
||||
ConfigResolutionSource::DefaultConfigDir,
|
||||
))
|
||||
}
|
||||
@@ -334,6 +345,21 @@ pub fn user_openhuman_dir(default_openhuman_dir: &Path, user_id: &str) -> PathBu
|
||||
default_openhuman_dir.join("users").join(user_id)
|
||||
}
|
||||
|
||||
/// Stable id used to scope the openhuman directory before any user has
|
||||
/// logged in. All memory, state, config, sessions and workspace files
|
||||
/// created on first init land under `{root}/users/{PRE_LOGIN_USER_ID}`
|
||||
/// so nothing is ever written directly at the root `.openhuman` path.
|
||||
///
|
||||
/// On first successful login, this directory is migrated into the real
|
||||
/// user-scoped directory (see `credentials::ops::store_session`).
|
||||
pub const PRE_LOGIN_USER_ID: &str = "local";
|
||||
|
||||
/// Returns the pre-login (unauthenticated) user directory:
|
||||
/// `{default_openhuman_dir}/users/local`.
|
||||
pub fn pre_login_user_dir(default_openhuman_dir: &Path) -> PathBuf {
|
||||
user_openhuman_dir(default_openhuman_dir, PRE_LOGIN_USER_ID)
|
||||
}
|
||||
|
||||
fn migrate_legacy_autocomplete_disabled_apps(config: &mut Config) {
|
||||
// Legacy defaults blocked both terminal and code, which prevented Codex/CLI usage.
|
||||
// Migrate only the exact legacy default so custom user preferences remain untouched.
|
||||
@@ -377,6 +403,29 @@ impl Config {
|
||||
|
||||
let config_path = openhuman_dir.join("config.toml");
|
||||
|
||||
// Pre-login path: no active user, no workspace marker, no env override,
|
||||
// and no existing config.toml on disk. Return an in-memory default
|
||||
// config without creating any directories or writing any files — disk
|
||||
// state is deferred until the first successful login in
|
||||
// `credentials::ops::store_session`, which writes `active_user.toml`
|
||||
// and triggers a reload that materializes the user-scoped directory.
|
||||
if resolution_source == ConfigResolutionSource::DefaultConfigDir && !config_path.exists() {
|
||||
let mut config = Config::default();
|
||||
config.config_path = config_path.clone();
|
||||
config.workspace_dir = workspace_dir.clone();
|
||||
config.apply_env_overrides();
|
||||
|
||||
tracing::debug!(
|
||||
path = %config.config_path.display(),
|
||||
workspace = %config.workspace_dir.display(),
|
||||
source = resolution_source.as_str(),
|
||||
initialized = false,
|
||||
persisted = false,
|
||||
"Config loaded (pre-login, in-memory only — no dirs or files written)"
|
||||
);
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
fs::create_dir_all(&openhuman_dir)
|
||||
.await
|
||||
.context("Failed to create config directory")?;
|
||||
@@ -1014,12 +1063,14 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
let default_workspace = root.join("workspace");
|
||||
|
||||
// No active user → uses default.
|
||||
// No active user → falls back to the pre-login user directory so
|
||||
// memory/state/config are still encapsulated under users/.
|
||||
let (oh_dir, ws_dir, source) = resolve_runtime_config_dirs(root, &default_workspace)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(oh_dir, root);
|
||||
assert_eq!(ws_dir, default_workspace);
|
||||
let expected_pre_login_dir = root.join("users").join(PRE_LOGIN_USER_ID);
|
||||
assert_eq!(oh_dir, expected_pre_login_dir);
|
||||
assert_eq!(ws_dir, expected_pre_login_dir.join("workspace"));
|
||||
assert_eq!(source, ConfigResolutionSource::DefaultConfigDir);
|
||||
|
||||
// With active user → scopes to user dir.
|
||||
@@ -1032,4 +1083,14 @@ mod tests {
|
||||
assert_eq!(ws_dir, expected_user_dir.join("workspace"));
|
||||
assert_eq!(source, ConfigResolutionSource::ActiveUser);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_login_user_dir_is_under_users_tree() {
|
||||
let root = PathBuf::from("/home/test/.openhuman");
|
||||
let dir = pre_login_user_dir(&root);
|
||||
assert_eq!(
|
||||
dir,
|
||||
PathBuf::from("/home/test/.openhuman/users").join(PRE_LOGIN_USER_ID)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,13 @@ mod autonomy;
|
||||
mod channels;
|
||||
mod defaults;
|
||||
mod dictation;
|
||||
mod hardware;
|
||||
mod heartbeat_cron;
|
||||
mod identity_cost;
|
||||
mod learning;
|
||||
mod load;
|
||||
pub use load::{
|
||||
clear_active_user, default_root_openhuman_dir, read_active_user_id, user_openhuman_dir,
|
||||
write_active_user_id,
|
||||
clear_active_user, default_root_openhuman_dir, pre_login_user_dir, read_active_user_id,
|
||||
user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID,
|
||||
};
|
||||
mod local_ai;
|
||||
mod observability;
|
||||
@@ -39,7 +38,6 @@ pub use channels::{
|
||||
TelegramConfig, WebhookConfig, WhatsAppConfig,
|
||||
};
|
||||
pub use dictation::{DictationActivationMode, DictationConfig};
|
||||
pub use hardware::{HardwareConfig, HardwareTransport};
|
||||
pub use heartbeat_cron::{CronConfig, HeartbeatConfig};
|
||||
pub use identity_cost::{CostConfig, ModelPricing, PeripheralBoardConfig, PeripheralsConfig};
|
||||
pub use learning::{LearningConfig, ReflectionSource};
|
||||
|
||||
@@ -111,9 +111,6 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub agents: HashMap<String, DelegateAgentConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub hardware: HardwareConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub local_ai: LocalAiConfig,
|
||||
|
||||
@@ -178,7 +175,6 @@ impl Default for Config {
|
||||
cost: CostConfig::default(),
|
||||
peripherals: PeripheralsConfig::default(),
|
||||
agents: HashMap::new(),
|
||||
hardware: HardwareConfig::default(),
|
||||
local_ai: LocalAiConfig::default(),
|
||||
voice_server: VoiceServerConfig::default(),
|
||||
query_classification: QueryClassificationConfig::default(),
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
//! Hardware board info tool — returns chip name, architecture, memory map for Telegram/agent.
|
||||
//!
|
||||
//! Use when user asks "what board do I have?", "board info", "connected hardware", etc.
|
||||
//! Uses probe-rs for Nucleo when available; otherwise static datasheet info.
|
||||
|
||||
use crate::openhuman::tools::traits::{Tool, ToolResult};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
|
||||
/// Static board info (datasheets). Used when probe-rs is unavailable.
|
||||
const BOARD_INFO: &[(&str, &str, &str)] = &[
|
||||
(
|
||||
"nucleo-f401re",
|
||||
"STM32F401RET6",
|
||||
"ARM Cortex-M4, 84 MHz. Flash: 512 KB, RAM: 128 KB. User LED on PA5 (pin 13).",
|
||||
),
|
||||
(
|
||||
"nucleo-f411re",
|
||||
"STM32F411RET6",
|
||||
"ARM Cortex-M4, 100 MHz. Flash: 512 KB, RAM: 128 KB. User LED on PA5 (pin 13).",
|
||||
),
|
||||
(
|
||||
"arduino-uno",
|
||||
"ATmega328P",
|
||||
"8-bit AVR, 16 MHz. Flash: 16 KB, SRAM: 2 KB. Built-in LED on pin 13.",
|
||||
),
|
||||
(
|
||||
"arduino-uno-q",
|
||||
"STM32U585 + Qualcomm",
|
||||
"Dual-core: STM32 (MCU) + Linux (aarch64). GPIO via Bridge app on port 9999.",
|
||||
),
|
||||
(
|
||||
"esp32",
|
||||
"ESP32",
|
||||
"Dual-core Xtensa LX6, 240 MHz. Flash: 4 MB typical. Built-in LED on GPIO 2.",
|
||||
),
|
||||
(
|
||||
"rpi-gpio",
|
||||
"Raspberry Pi",
|
||||
"ARM Linux. Native GPIO via sysfs/rppal. No fixed LED pin.",
|
||||
),
|
||||
];
|
||||
|
||||
/// Tool: return full board info (chip, architecture, memory map) for agent/Telegram.
|
||||
pub struct HardwareBoardInfoTool {
|
||||
boards: Vec<String>,
|
||||
}
|
||||
|
||||
impl HardwareBoardInfoTool {
|
||||
pub fn new(boards: Vec<String>) -> Self {
|
||||
Self { boards }
|
||||
}
|
||||
|
||||
fn static_info_for_board(&self, board: &str) -> Option<String> {
|
||||
BOARD_INFO
|
||||
.iter()
|
||||
.find(|(b, _, _)| *b == board)
|
||||
.map(|(_, chip, desc)| {
|
||||
format!(
|
||||
"**Board:** {}\n**Chip:** {}\n**Description:** {}",
|
||||
board, chip, desc
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for HardwareBoardInfoTool {
|
||||
fn name(&self) -> &str {
|
||||
"hardware_board_info"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Return full board info (chip, architecture, memory map) for connected hardware. Use when: user asks for 'board info', 'what board do I have', 'connected hardware', 'chip info', 'what hardware', or 'memory map'."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"board": {
|
||||
"type": "string",
|
||||
"description": "Optional board name (e.g. nucleo-f401re). If omitted, returns info for first configured board."
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||
let board = args
|
||||
.get("board")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.or_else(|| self.boards.first().cloned());
|
||||
|
||||
let board = board.as_deref().unwrap_or("unknown");
|
||||
|
||||
if self.boards.is_empty() {
|
||||
return Ok(ToolResult::error(
|
||||
"No peripherals configured. Add boards to config.toml [peripherals.boards].",
|
||||
));
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
#[cfg(feature = "probe")]
|
||||
if board == "nucleo-f401re" || board == "nucleo-f411re" {
|
||||
let chip = if board == "nucleo-f411re" {
|
||||
"STM32F411RETx"
|
||||
} else {
|
||||
"STM32F401RETx"
|
||||
};
|
||||
match probe_board_info(chip) {
|
||||
Ok(info) => {
|
||||
return Ok(ToolResult::success(info));
|
||||
}
|
||||
Err(e) => {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(
|
||||
output,
|
||||
"probe-rs attach failed: {e}. Using static info.\n\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(info) = self.static_info_for_board(board) {
|
||||
output.push_str(&info);
|
||||
if let Some(mem) = memory_map_static(board) {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(output, "\n\n**Memory map:**\n{mem}");
|
||||
}
|
||||
} else {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(
|
||||
output,
|
||||
"Board '{board}' configured. No static info available."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(ToolResult::success(output))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "probe")]
|
||||
fn probe_board_info(chip: &str) -> anyhow::Result<String> {
|
||||
use probe_rs::config::MemoryRegion;
|
||||
use probe_rs::{Session, SessionConfig};
|
||||
|
||||
let session = Session::auto_attach(chip, SessionConfig::default())
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
let target = session.target();
|
||||
let arch = session.architecture();
|
||||
|
||||
let mut out = format!(
|
||||
"**Board:** {}\n**Chip:** {}\n**Architecture:** {:?}\n\n**Memory map:**\n",
|
||||
chip, target.name, arch
|
||||
);
|
||||
for region in target.memory_map.iter() {
|
||||
match region {
|
||||
MemoryRegion::Ram(ram) => {
|
||||
let (start, end) = (ram.range.start, ram.range.end);
|
||||
out.push_str(&format!(
|
||||
"RAM: 0x{:08X} - 0x{:08X} ({} KB)\n",
|
||||
start,
|
||||
end,
|
||||
(end - start) / 1024
|
||||
));
|
||||
}
|
||||
MemoryRegion::Nvm(flash) => {
|
||||
let (start, end) = (flash.range.start, flash.range.end);
|
||||
out.push_str(&format!(
|
||||
"Flash: 0x{:08X} - 0x{:08X} ({} KB)\n",
|
||||
start,
|
||||
end,
|
||||
(end - start) / 1024
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.push_str("\n(Info read via USB/SWD — no firmware on target needed.)");
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn memory_map_static(board: &str) -> Option<&'static str> {
|
||||
match board {
|
||||
"nucleo-f401re" | "nucleo-f411re" => Some(
|
||||
"Flash: 0x0800_0000 - 0x0807_FFFF (512 KB)\nRAM: 0x2000_0000 - 0x2001_FFFF (128 KB)",
|
||||
),
|
||||
"arduino-uno" => Some("Flash: 16 KB, SRAM: 2 KB, EEPROM: 1 KB"),
|
||||
"esp32" => Some("Flash: 4 MB, IRAM/DRAM per ESP-IDF layout"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
//! Hardware memory map tool — returns flash/RAM address ranges for connected boards.
|
||||
//!
|
||||
//! Phase B: When user asks "what are the upper and lower memory addresses?", this tool
|
||||
//! returns the memory map. Uses probe-rs for Nucleo/STM32 when available; otherwise
|
||||
//! returns static maps from datasheets.
|
||||
|
||||
use crate::openhuman::tools::traits::{Tool, ToolResult};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
|
||||
/// Known memory maps (from datasheets). Used when probe-rs is unavailable.
|
||||
const MEMORY_MAPS: &[(&str, &str)] = &[
|
||||
(
|
||||
"nucleo-f401re",
|
||||
"Flash: 0x0800_0000 - 0x0807_FFFF (512 KB)\nRAM: 0x2000_0000 - 0x2001_FFFF (128 KB)\nSTM32F401RET6, ARM Cortex-M4",
|
||||
),
|
||||
(
|
||||
"nucleo-f411re",
|
||||
"Flash: 0x0800_0000 - 0x0807_FFFF (512 KB)\nRAM: 0x2000_0000 - 0x2001_FFFF (128 KB)\nSTM32F411RET6, ARM Cortex-M4",
|
||||
),
|
||||
(
|
||||
"arduino-uno",
|
||||
"Flash: 0x0000 - 0x3FFF (16 KB, ATmega328P)\nSRAM: 0x0100 - 0x08FF (2 KB)\nEEPROM: 0x0000 - 0x03FF (1 KB)",
|
||||
),
|
||||
(
|
||||
"arduino-mega",
|
||||
"Flash: 0x0000 - 0x3FFFF (256 KB, ATmega2560)\nSRAM: 0x0200 - 0x21FF (8 KB)\nEEPROM: 0x0000 - 0x0FFF (4 KB)",
|
||||
),
|
||||
(
|
||||
"esp32",
|
||||
"Flash: 0x3F40_0000 - 0x3F7F_FFFF (4 MB typical)\nIRAM: 0x4000_0000 - 0x4005_FFFF\nDRAM: 0x3FFB_0000 - 0x3FFF_FFFF",
|
||||
),
|
||||
];
|
||||
|
||||
/// Tool: report hardware memory map for connected boards.
|
||||
pub struct HardwareMemoryMapTool {
|
||||
boards: Vec<String>,
|
||||
}
|
||||
|
||||
impl HardwareMemoryMapTool {
|
||||
pub fn new(boards: Vec<String>) -> Self {
|
||||
Self { boards }
|
||||
}
|
||||
|
||||
fn static_map_for_board(&self, board: &str) -> Option<&'static str> {
|
||||
MEMORY_MAPS
|
||||
.iter()
|
||||
.find(|(b, _)| *b == board)
|
||||
.map(|(_, m)| *m)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for HardwareMemoryMapTool {
|
||||
fn name(&self) -> &str {
|
||||
"hardware_memory_map"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Return the memory map (flash and RAM address ranges) for connected hardware. Use when: user asks for 'upper and lower memory addresses', 'memory map', 'address space', or 'readable addresses'. Returns flash/RAM ranges from datasheets."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"board": {
|
||||
"type": "string",
|
||||
"description": "Optional board name (e.g. nucleo-f401re, arduino-uno). If omitted, returns map for first configured board."
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||
let board = args
|
||||
.get("board")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.or_else(|| self.boards.first().cloned());
|
||||
|
||||
let board = board.as_deref().unwrap_or("unknown");
|
||||
|
||||
if self.boards.is_empty() {
|
||||
return Ok(ToolResult::error(
|
||||
"No peripherals configured. Add boards to config.toml [peripherals.boards].",
|
||||
));
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
#[cfg(feature = "probe")]
|
||||
let probe_ok = {
|
||||
if board == "nucleo-f401re" || board == "nucleo-f411re" {
|
||||
let chip = if board == "nucleo-f411re" {
|
||||
"STM32F411RETx"
|
||||
} else {
|
||||
"STM32F401RETx"
|
||||
};
|
||||
match probe_rs_memory_map(chip) {
|
||||
Ok(probe_msg) => {
|
||||
output.push_str(&format!("**{}** (via probe-rs):\n{}\n", board, probe_msg));
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
output.push_str(&format!("Probe-rs failed: {}. ", e));
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "probe"))]
|
||||
let probe_ok = false;
|
||||
|
||||
if !probe_ok {
|
||||
if let Some(map) = self.static_map_for_board(board) {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(output, "**{board}** (from datasheet):\n{map}");
|
||||
} else {
|
||||
use std::fmt::Write;
|
||||
let known: Vec<&str> = MEMORY_MAPS.iter().map(|(b, _)| *b).collect();
|
||||
let _ = write!(
|
||||
output,
|
||||
"No memory map for board '{board}'. Known boards: {}",
|
||||
known.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ToolResult::success(output))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "probe")]
|
||||
fn probe_rs_memory_map(chip: &str) -> anyhow::Result<String> {
|
||||
use probe_rs::config::MemoryRegion;
|
||||
use probe_rs::{Session, SessionConfig};
|
||||
|
||||
let session = Session::auto_attach(chip, SessionConfig::default())
|
||||
.map_err(|e| anyhow::anyhow!("probe-rs attach failed: {}", e))?;
|
||||
|
||||
let target = session.target();
|
||||
let mut out = String::new();
|
||||
|
||||
for region in target.memory_map.iter() {
|
||||
match region {
|
||||
MemoryRegion::Ram(ram) => {
|
||||
let start = ram.range.start;
|
||||
let end = ram.range.end;
|
||||
let size_kb = (end - start) / 1024;
|
||||
out.push_str(&format!(
|
||||
"RAM: 0x{:08X} - 0x{:08X} ({} KB)\n",
|
||||
start, end, size_kb
|
||||
));
|
||||
}
|
||||
MemoryRegion::Nvm(flash) => {
|
||||
let start = flash.range.start;
|
||||
let end = flash.range.end;
|
||||
let size_kb = (end - start) / 1024;
|
||||
out.push_str(&format!(
|
||||
"Flash: 0x{:08X} - 0x{:08X} ({} KB)\n",
|
||||
start, end, size_kb
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if out.is_empty() {
|
||||
out = "Could not read memory regions from probe.".to_string();
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn static_map_nucleo() {
|
||||
let tool = HardwareMemoryMapTool::new(vec!["nucleo-f401re".into()]);
|
||||
assert!(tool.static_map_for_board("nucleo-f401re").is_some());
|
||||
assert!(tool
|
||||
.static_map_for_board("nucleo-f401re")
|
||||
.unwrap()
|
||||
.contains("Flash"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_map_arduino() {
|
||||
let tool = HardwareMemoryMapTool::new(vec!["arduino-uno".into()]);
|
||||
assert!(tool.static_map_for_board("arduino-uno").is_some());
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
//! Hardware memory read tool — read actual memory/register values from Nucleo via probe-rs.
|
||||
//!
|
||||
//! Use when user asks to "read register values", "read memory at address", "dump lower memory", etc.
|
||||
//! Requires probe feature and Nucleo connected via USB.
|
||||
|
||||
use crate::openhuman::tools::traits::{Tool, ToolResult};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
|
||||
/// RAM base for Nucleo-F401RE (STM32F401)
|
||||
const NUCLEO_RAM_BASE: u64 = 0x2000_0000;
|
||||
|
||||
/// Tool: read memory at address from connected Nucleo via probe-rs.
|
||||
pub struct HardwareMemoryReadTool {
|
||||
boards: Vec<String>,
|
||||
}
|
||||
|
||||
impl HardwareMemoryReadTool {
|
||||
pub fn new(boards: Vec<String>) -> Self {
|
||||
Self { boards }
|
||||
}
|
||||
|
||||
fn chip_for_board(board: &str) -> Option<&'static str> {
|
||||
match board {
|
||||
"nucleo-f401re" => Some("STM32F401RETx"),
|
||||
"nucleo-f411re" => Some("STM32F411RETx"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for HardwareMemoryReadTool {
|
||||
fn name(&self) -> &str {
|
||||
"hardware_memory_read"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Read actual memory/register values from Nucleo via USB. Use when: user asks to 'read register values', 'read memory at address', 'dump memory', 'lower memory 0-126', or 'give address and value'. Returns hex dump. Requires Nucleo connected via USB and probe feature. Params: address (hex, e.g. 0x20000000 for RAM start), length (bytes, default 128)."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "Memory address in hex (e.g. 0x20000000 for RAM start). Default: 0x20000000 (RAM base)."
|
||||
},
|
||||
"length": {
|
||||
"type": "integer",
|
||||
"description": "Number of bytes to read (default 128, max 256)."
|
||||
},
|
||||
"board": {
|
||||
"type": "string",
|
||||
"description": "Board name (nucleo-f401re). Optional if only one configured."
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||
if self.boards.is_empty() {
|
||||
return Ok(ToolResult::error(
|
||||
"No peripherals configured. Add nucleo-f401re to config.toml [peripherals.boards].",
|
||||
));
|
||||
}
|
||||
|
||||
let board = args
|
||||
.get("board")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.or_else(|| self.boards.first().cloned())
|
||||
.unwrap_or_else(|| "nucleo-f401re".into());
|
||||
|
||||
let chip = Self::chip_for_board(&board);
|
||||
if chip.is_none() {
|
||||
return Ok(ToolResult::error(format!(
|
||||
"Memory read only supports nucleo-f401re, nucleo-f411re. Got: {}",
|
||||
board
|
||||
)));
|
||||
}
|
||||
|
||||
let address_str = args
|
||||
.get("address")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0x20000000");
|
||||
let _address = parse_hex_address(address_str).unwrap_or(NUCLEO_RAM_BASE);
|
||||
|
||||
let requested_length = args.get("length").and_then(|v| v.as_u64()).unwrap_or(128);
|
||||
let _length = usize::try_from(requested_length)
|
||||
.unwrap_or(256)
|
||||
.clamp(1, 256);
|
||||
|
||||
#[cfg(feature = "probe")]
|
||||
{
|
||||
match probe_read_memory(chip.unwrap(), _address, _length) {
|
||||
Ok(output) => {
|
||||
return Ok(ToolResult::success(output));
|
||||
}
|
||||
Err(e) => {
|
||||
return Ok(ToolResult::error(format!(
|
||||
"probe-rs read failed: {}. Ensure Nucleo is connected via USB and built with --features probe.",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "probe"))]
|
||||
{
|
||||
Ok(ToolResult::error(
|
||||
"Memory read requires probe feature. Build with: cargo build --features hardware,probe",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hex_address(s: &str) -> Option<u64> {
|
||||
let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
|
||||
u64::from_str_radix(s, 16).ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "probe")]
|
||||
fn probe_read_memory(chip: &str, address: u64, length: usize) -> anyhow::Result<String> {
|
||||
use probe_rs::MemoryInterface;
|
||||
use probe_rs::Session;
|
||||
use probe_rs::SessionConfig;
|
||||
|
||||
let mut session = Session::auto_attach(chip, SessionConfig::default())
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
|
||||
let mut core = session.core(0)?;
|
||||
let mut buf = vec![0u8; length];
|
||||
core.read_8(address, &mut buf)
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
|
||||
// Format as hex dump: address | bytes (16 per line)
|
||||
let mut out = format!("Memory read from 0x{:08X} ({} bytes):\n\n", address, length);
|
||||
const COLS: usize = 16;
|
||||
for (i, chunk) in buf.chunks(COLS).enumerate() {
|
||||
let addr = address + (i * COLS) as u64;
|
||||
let hex: String = chunk
|
||||
.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let ascii: String = chunk
|
||||
.iter()
|
||||
.map(|&b| {
|
||||
if b.is_ascii_graphic() || b == b' ' {
|
||||
b as char
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
out.push_str(&format!("0x{:08X} {:48} {}\n", addr, hex, ascii));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod board_info;
|
||||
mod memory_map;
|
||||
mod memory_read;
|
||||
|
||||
pub use board_info::HardwareBoardInfoTool;
|
||||
pub use memory_map::HardwareMemoryMapTool;
|
||||
pub use memory_read::HardwareMemoryReadTool;
|
||||
@@ -2,7 +2,6 @@ pub mod agent;
|
||||
pub mod browser;
|
||||
pub mod cron;
|
||||
pub mod filesystem;
|
||||
pub mod hardware;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod system;
|
||||
@@ -11,7 +10,6 @@ pub use agent::*;
|
||||
pub use browser::*;
|
||||
pub use cron::*;
|
||||
pub use filesystem::*;
|
||||
pub use hardware::*;
|
||||
pub use memory::*;
|
||||
pub use network::*;
|
||||
pub use system::*;
|
||||
|
||||
+33
-6
@@ -542,9 +542,24 @@ default_temperature = 0.7
|
||||
encrypt = false
|
||||
"#
|
||||
);
|
||||
std::fs::create_dir_all(openhuman_dir).expect("mkdir openhuman");
|
||||
let path = openhuman_dir.join("config.toml");
|
||||
std::fs::write(&path, &cfg).expect("write config");
|
||||
fn write_config_file(config_dir: &Path, cfg: &str) {
|
||||
std::fs::create_dir_all(config_dir).expect("mkdir openhuman");
|
||||
let path = config_dir.join("config.toml");
|
||||
std::fs::write(&path, cfg).expect("write config");
|
||||
}
|
||||
|
||||
write_config_file(openhuman_dir, &cfg);
|
||||
|
||||
// Runtime config resolution is user-scoped before login, so tests that seed
|
||||
// the root `~/.openhuman` directory also need the equivalent pre-login
|
||||
// config under `~/.openhuman/users/local`.
|
||||
if openhuman_dir
|
||||
.file_name()
|
||||
.is_some_and(|name| name == std::ffi::OsStr::new(".openhuman"))
|
||||
{
|
||||
write_config_file(&openhuman_dir.join("users").join("local"), &cfg);
|
||||
}
|
||||
|
||||
let _: openhuman_core::openhuman::config::Config =
|
||||
toml::from_str(&cfg).expect("config toml must match Config schema");
|
||||
}
|
||||
@@ -563,9 +578,21 @@ encrypt = false
|
||||
enabled = false
|
||||
"#
|
||||
);
|
||||
std::fs::create_dir_all(openhuman_dir).expect("mkdir openhuman");
|
||||
let path = openhuman_dir.join("config.toml");
|
||||
std::fs::write(&path, &cfg).expect("write config");
|
||||
fn write_config_file(config_dir: &Path, cfg: &str) {
|
||||
std::fs::create_dir_all(config_dir).expect("mkdir openhuman");
|
||||
let path = config_dir.join("config.toml");
|
||||
std::fs::write(&path, cfg).expect("write config");
|
||||
}
|
||||
|
||||
write_config_file(openhuman_dir, &cfg);
|
||||
|
||||
if openhuman_dir
|
||||
.file_name()
|
||||
.is_some_and(|name| name == std::ffi::OsStr::new(".openhuman"))
|
||||
{
|
||||
write_config_file(&openhuman_dir.join("users").join("local"), &cfg);
|
||||
}
|
||||
|
||||
let _: openhuman_core::openhuman::config::Config =
|
||||
toml::from_str(&cfg).expect("config toml must match Config schema");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user