GUI installer v2

This commit is contained in:
2026-03-21 22:10:13 +02:00
parent 18ab62a7c5
commit 3db6de2d2c
22 changed files with 9348 additions and 72 deletions

474
CONFIG_LOCATIONS.md Normal file
View File

@@ -0,0 +1,474 @@
# JiboOS V3.1 Config locations inventory (for SSH tooling)
This workspace is a filesystem tree under `build/`. Most paths below are **absolute paths as they exist on the robot**.
If youre using this repo as a staging image, the mapping is:
- Workspace path: `build/<ABS_PATH>`
- Robot path: `<ABS_PATH>`
Example: `build/usr/local/etc/jibo-jetstream-service.json` corresponds to `/usr/local/etc/jibo-jetstream-service.json` on-robot.
---
## Highest-level “what runs what”
### `/usr/local/etc/jibo-system-manager.json`
**Purpose:** Master orchestrator config. Defines the service startup order, executables, arguments (including which `-c /usr/local/etc/<service>.json` is used), environment variables, and some device-level settings.
**Why your tooling should care:** if you need to know **which process** consumes a config file, this is the authoritative mapping.
Notable sections:
- `SystemManager.service.services[]`
- `name`, `executable`, `modes.<mode>.arguments` (usually includes the config file path)
- `modes.<mode>.enabled` (turn a service on/off per mode)
- `SystemManager.skills.environment`
- `JIBO_HUB_SHIM_HOST` (host/port target for the hub shim)
- `JIBO_GQA_ENDPOINT` (HTTP endpoint used by the optional GQA shim)
- `credentials.path`: `/var/jibo/credentials.json` (runtime config; controls region selection in Jetstream)
- `wifi.*` (wpa_supplicant + DHCP client options)
- `time.*` (timezone/localtime paths and NTP sync)
**Apply/restart:** system-manager itself (how depends on your init/systemd on the robot). Most changes only take effect on service restart or reboot.
---
## Core robot service configs (`/usr/local/etc/*.json`)
These are the main knobs for the robots built-in services. Many follow this pattern:
- `WebCore.serverPort`: HTTP port for the service
- `<ServiceName>.registryPort`: service registry port (usually `8181`)
- `logging.*`: log levels / syslog routing
### `/usr/local/etc/jibo-service-registry.json`
**Purpose:** Service registry / management web endpoints.
- Ports: `8181` (`WebCore.serverPort`, `ServiceRegistry.registryPort`)
**Apply/restart:** restart `jibo-service-registry`.
### `/usr/local/etc/jibo-jetstream-service.json`
**Purpose:** Jetstream (hub client + streaming audio + wakeword/ASR pipeline glue).
Key knobs:
- `HubClient.override`: where Jetstream sends `/v1/listen` and friends.
- `hub_hostname`, `hub_port`
- Use this to point Jetstream to a **local or server-hosted hub shim**.
- `HubClient.listen_language`
- `HubClient.encoding_type` + `encoding-settings`: `OGG_OPUS` / `FLAC` settings
- `RecogHJ`, `HubAsr`: timing for SOS/EOS and max speech timeouts
**Apply/restart:** restart `jibo-jetstream-service`.
### `/usr/local/etc/jibo-asr-service.json`
**Purpose:** On-robot ASR service (WebSocket event stream + HTTP start/stop), logging, cloud/local STT selection.
Key knobs:
- `webCore.serverPort`: `8088`
- `AsrService.language`
- `AsrService.log_audio`, `log_text`, `log_path`, upload intervals/thresholds
- `AsrService.resident_task` / `resident_audio_channel`: default always-on hotphrase task (e.g. `audio_source_id":"alsa1"`)
- `AsrService.task_templates`: defines ASR pipelines
**Apply/restart:** restart `jibo-asr-service`.
Note: this file contains cloud credentials/keys in this build tree. Treat it as sensitive in your tooling (avoid echoing it into logs).
### `/usr/local/etc/jibo-tts-service.json`
**Purpose:** On-robot TTS voice + audio output.
Key knobs:
- `webCore.serverPort`: `8089`
- `TTSService.resourcePath`: voice resources
- `TTSService.alsaPlaybackDevice`: playback routing
- `voiceParams.*`: speed, volume, max chars, etc.
**Apply/restart:** restart `jibo-tts-service`.
### `/usr/local/etc/jibo-nlu-service.json`
**Purpose:** NLU service (local grammar/model parsing).
Key knobs:
- `webCore.serverPort`: `8787`
- `Service.nlu_data_dir`: NLU model data
- `Service.default_locale`
**Apply/restart:** restart `jibo-nlu-service`.
### `/usr/local/etc/jibo-audio-service.json`
**Purpose:** Audio routing/capture/playback device selection and audio processing thresholds.
Key knobs:
- `WebCore.serverPort`: `8383`
- `AudioService.alsaCaptureDevice`, `alsaPlaybackDevice`
- `AudioService.router*` latencies
- `AudioService.kinematic_model`: points to `/usr/local/etc/jibo-kinematic-model.json`
**Apply/restart:** restart `jibo-audio-service`.
### `/usr/local/etc/jibo-body-service.json`
**Purpose:** Low-level body control: serial devices, offsets, limits, battery thresholds, IMU calibration path.
Key knobs:
- `WebCore.serverPort`: `8282`
- `bodyBoard.*`: `/dev/ttyTHS0/1` devices, offsets, flipped flags, accel/vel limits
- `imu.driver.calibrationFile`: `/var/jibo/imu/imu-cal.json` (runtime file)
- `BodyService.kinematic_model`: points to `/usr/local/etc/jibo-kinematic-model.json`
**Apply/restart:** restart `jibo-body-service`.
### `/usr/local/etc/jibo-kinematic-model.json`
**Purpose:** Robot kinematic model (frame transforms, masses, inertias). Used by audio/body/LPS.
**Apply/restart:** restart consumers (at least `jibo-body-service`, `jibo-audio-service`, and `jibo-lps-service`).
### `/usr/local/etc/jibo-media-service.json`
**Purpose:** Media service camera configuration (CUDA/V4L2 device paths, capture params).
Key knobs:
- `WebCore.serverPort`: `7979`
- `MediaService.camera.*`: `/dev/video0`, `/dev/video1`, flips, AE/AWB tuning
**Apply/restart:** restart `jibo-media-service`.
### `/usr/local/etc/jibo-lps-service.json`
**Purpose:** LPS (Local Perception System) + internal media subsystem + visual awareness pipeline.
Key knobs:
- `WebCoreLPS.serverPort`: `8484`
- `CaptureSubsystem.camera_config_file`: `/usr/local/etc/lps/cameras.json`
- `EngineSubsystem.schemas.*`: `/usr/local/etc/lps/schemas/{normal,focused,minimal}.json`
- `EngineSubsystem.engine.state.entity_config_file`: `/usr/local/etc/lps/entityConfig.json`
- `EngineSubsystem.engine.state.geometry.*`: camera model params in `/var/jibo/lps/*.json` (runtime files)
**Apply/restart:** restart `jibo-lps-service`.
### `/usr/local/etc/lps/cameras.json`
**Purpose:** Camera device list + CUDA capture config + controls presets.
**Apply/restart:** restart `jibo-lps-service` (and anything using the same capture stack).
### `/usr/local/etc/lps/entityConfig.json`
**Purpose:** Entity tracking parameters (people/head tracking, confidence thresholds, trackers, etc.).
**Apply/restart:** restart `jibo-lps-service`.
### `/usr/local/etc/lps/schemas/normal.json`
### `/usr/local/etc/lps/schemas/focused.json`
### `/usr/local/etc/lps/schemas/minimal.json`
**Purpose:** LPS “schema” graphs: which detectors/actions run and at what cadence.
**Apply/restart:** restart `jibo-lps-service`.
### `/usr/local/etc/jibo-identity-service.json`
**Purpose:** Identity/face recognition engine + model paths.
Key knobs:
- `WebCore.serverPort`: `8489`
- `IdentityService.engine.identifier.*`: choose identifier type (deepid/eigenfaces/resnetfaceid) and model paths
- Storage path: `/var/jibo/identity/*` (runtime)
**Apply/restart:** restart `jibo-identity-service`.
### `/usr/local/etc/jibo-server-service.json`
**Purpose:** Cloud/server connection service + notifications.
- `WebCore.serverPort`: `8888`
- `NotificationSubsystem.serverURLSuffix`
**Apply/restart:** restart `jibo-server-service`.
### `/usr/local/etc/jibo-service-center-service.json`
**Purpose:** Service-center web UI/service.
- `WebCore.serverPort`: `9797`
**Apply/restart:** restart `jibo-service-center-service`.
### `/usr/local/etc/jibo-certification-service.json`
**Purpose:** Certification service.
- `WebCore.serverPort`: `9292`
**Apply/restart:** restart `jibo-certification-service`.
### `/usr/local/etc/jibo-system-monitoring-service.json`
**Purpose:** System health/storage monitoring + health log upload.
Key knobs:
- `WebCore.serverPort`: `4111`
- `SystemMonitoringService.storage.semantic`: path aliases used for reporting
- `health.upload.arguments`: uses `/var/jibo/credentials.json` (runtime)
**Apply/restart:** restart `jibo-system-monitoring-service`.
### `/usr/local/etc/jibo-test-capture-service.json`
**Purpose:** Camera capture tools service (debug/QA).
- `WebCore.serverPort`: `7979` (note: overlaps with `jibo-media-service.json` in this tree; only one should bind a given port at runtime)
**Apply/restart:** restart `jibo-test-capture-service`.
### `/usr/local/etc/jibo-test-capture.json`
**Purpose:** Test capture tool runtime behavior (recording toggles, lock counts, display/profiler options).
**Apply/restart:** whatever tool/runner loads it (not a standard service config; used by capture tooling).
### `/usr/local/etc/jibo-camera-calibrator.json`
**Purpose:** Camera calibration workflow + capture device selection. Writes calibration outputs into `/var/jibo/lps`.
**Apply/restart:** used by the calibrator tool; changes apply on next run.
### `/usr/local/etc/jibo-hub-shim.json`
**Purpose:** Robot-local hub shim config (for running the shim on the robot).
Key knobs:
- `listen.*`: bind/port/path for `/v1/listen`
- `asrService.baseUrl`: typically `http://127.0.0.1:8088` on-robot
**Apply/restart:** restart the hub-shim process (not a core Jibo service; depends on your deployment).
### `/usr/local/etc/jibo-sts.json`
**Purpose:** Placeholder config for secure-transfer (STS). In this tree it is currently empty.
**Apply/restart:** N/A (service may use defaults or other config sources).
---
## SSM / robot “mode” configuration (`/usr/local/etc/jibo-ssm/*.json`)
These files configure the Node-based SSM (service supervisor/skill launcher) and its ports per robot mode.
- `/usr/local/etc/jibo-ssm/jibo-ssm-normal.json`
- `/usr/local/etc/jibo-ssm/jibo-ssm-oobe.json`
- `/usr/local/etc/jibo-ssm/jibo-ssm-developer.json`
- `/usr/local/etc/jibo-ssm/jibo-ssm-int-developer.json`
Common knobs:
- `services.SkillsService.port` (HTTP port; typically `8779`)
- `services.DevShell.*` (developer ports: `8686/8989/9191`) (developer/int-developer)
- `services.WifiService.region` (e.g. `api`)
- `RegistryClient.host/port` (usually `127.0.0.1:8181`)
- `logging.namespaces` (fine-grained log routing)
**Apply/restart:** restart SSM / the Node process that loads these configs.
---
## Hub shim (server-hosted) config
This is for the PC/server side shim that emulates the hub `/v1/listen` endpoint.
Workspace source:
- `hub-shim/config.example.json` (template)
- `hub-shim/config.json` (local dev)
- `hub-shim/systemd/jibo-hub-shim.service` (service unit)
- `hub-shim/systemd/jibo-hub-shim.env.example` (env file template)
Server install locations (created by `hub-shim/install-server.sh`):
- `/opt/jibo-hub-shim/` (code)
- `/etc/jibo-hub-shim/config.json` (config)
- `/etc/jibo-hub-shim/jibo-hub-shim.env` (sets `JIBO_HUB_SHIM_CONFIG=/etc/jibo-hub-shim/config.json`)
- `/etc/systemd/system/jibo-hub-shim.service`
Key knobs in the shim config JSON:
- `listen.bindHost`, `listen.port`, `listen.path`
- `asrService.baseUrl`: robot ASR base URL (e.g. `http://<robot-ip>:8088` if shim runs off-robot)
- `asrService.audioSourceId`: commonly `alsa1` on-robot
- `asrService.timeoutMs`
- `gqaShim.enabled` + `gqaShim.ollama.*`: optional Ollama-backed “GQA shim” HTTP service
**Apply/restart:** `systemctl restart jibo-hub-shim` (server). For local dev: restart `node index.js ./config.json`.
---
## AI bridge (PC-side) configuration
### PC server (Python)
- `ai_bridge_server/server.py`
Config sources:
- CLI args: `--host`, `--port` (default run example uses `0.0.0.0:8020`)
- Env vars:
- `OLLAMA_URL` (default `http://127.0.0.1:11434/api/chat`)
- `OLLAMA_MODEL` (default `phi3.5`)
- `WHISPER_MODEL` (default `base`) (audio endpoint only)
**Apply/restart:** restart the Python process.
### Robot-side AI bridge client config
- `/opt/jibo/Jibo/Skills/@be/be/be/ai-bridge-config.json`
Key knobs:
- `enabled`
- `mode`: `TEXT` or `AUDIO`
- `serverBaseUrl`: your PCs bridge server URL (e.g. `http://<pc-ip>:8020`)
- ASR integration:
- `useAsrServiceStt`, `asrServiceHost`, `asrServicePort`, `asrAudioSourceId`, `asrTimeoutMs`, `asrAutoStart`
- Behavior:
- `wakeupChitchatPhrases[]`
- `followupEnabled`, `followupDelayMs`
- `aiForwardingAllowedSkills[]` gating
**Apply/restart:** restart the SkillsService / BE skill (or reboot).
Related dev-only override:
- `/opt/jibo/Jibo/Skills/@be/be/be/jibo-asr-service.local.json` (local-run ASR config variant)
---
## Skills menu / content configs (skill-layer “settings”)
These are not OS-level services, but they are **configuration surfaces** for skill UI/menu behavior.
- `/opt/jibo/Jibo/Skills/@be/menu-entries.d/*.json`
- Defines top-level menu entries/submenus (e.g. `childrenDir` points at a skill folder containing `menuEntry.json`).
- `/opt/jibo/Jibo/Skills/*/menuEntry.json`
- Declares skill menu metadata (title/hidden/etc.).
- `/opt/jibo/Jibo/Skills/@be/be/menu/menus/*.json`
- Defines button menus (labels/icons/utterances + hit area polygon).
**Apply/restart:** restart SkillsService / reload the menu skill.
---
## Init scripts and env-tunable configs (`/etc/init.d/*`)
These scripts are often the real place where **ports and file paths** are chosen, via environment variables.
### `/etc/init.d/S04jibo-asr-service`
- Launches: `/usr/local/bin/jibo-asr-service -c /usr/local/etc/jibo-asr-service.json`
- PID file: `/var/run/jibo-asr-service.pid`
- Log: `/tmp/jibo-asr-service.log`
### `/etc/init.d/S02jibo-skills-logd`
Skills UDP log daemon.
Env vars:
- `JIBO_LOGD_HOST` (default `127.0.0.1`)
- `JIBO_LOGD_PORT` (default `15140`)
- `JIBO_LOGD_FILE` (default `/tmp/jibo-skills.log`)
### `/etc/init.d/S03jibo-skills-logpanel`
Skills web log panel.
Env vars:
- `JIBO_LOGPANEL_BIND` (default `0.0.0.0`)
- `JIBO_LOGPANEL_PORT` (default `15150`)
- `JIBO_LOGD_FILE` (shared logfile path)
### `/etc/init.d/S21firewall`
Firewall rules (iptables). Not JSON, but it is a major “settings surface”.
- Always allows SSH port `22`
- Also explicitly allows SkillsService panel `8779` and log panel `15150`
- Opens additional ports based on the current mode via `/usr/bin/jibo-getmode`
**Apply:** run the init script (restart) or reboot.
---
## Runtime-created or runtime-edited config files (referenced by the above)
These are not present in this build tree, but the services above reference them. Your SSH tooling will often want to read/edit these too.
- `/var/jibo/credentials.json`
- Used for: Jetstream `region-settings` selection; system health upload credentials.
- `/var/jibo/imu/imu-cal.json`
- Used for: BodyService IMU calibration.
- `/var/jibo/lps/CameraModelParamsL.json`
- `/var/jibo/lps/CameraModelParamsR.json`
- `/var/jibo/lps/InterCameraTransform.json`
- Used for: LPS geometry.
- `/var/etc/timezone`, `/var/etc/localtime`
- Used for: system timezone.
---
## Quick port index (useful for “is my edit live?” checks)
From configs in this tree:
- Service registry: `8181`
- Body: `8282`
- Audio: `8383`
- Jetstream: `8090`
- ASR: `8088`
- TTS: `8089`
- NLU: `8787`
- Identity: `8489`
- LPS: `8484` (and internal media `8486`)
- Media: `7979`
- Server service: `8888`
- System manager: `8585`
- System monitoring: `4111`
- Service center: `9797`
- Certification: `9292`
- Skills service: `8779` (SSM)
- Skills logd: UDP `15140`
- Skills log panel: `15150`
- Hub shim (this project): `9000` (server-side shim)
- AI bridge server (PC): `8020`
---
## Suggested conventions for your editor tool
- Always read/parse JSON with comment tolerance disabled (these are strict JSON files).
- Treat these keys as “high risk” and avoid accidentally printing them:
- Anything containing `appkey`, `key`, `credential`, `password`, `token`.
- After edits:
- Restart only the owning service (see sections above).
- Prefer `system-manager` / SSM restart only when necessary.

3334
CONFIG_VALUES.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,474 @@
# JiboOS V3.1 Config locations inventory (for SSH tooling)
This workspace is a filesystem tree under `build/`. Most paths below are **absolute paths as they exist on the robot**.
If youre using this repo as a staging image, the mapping is:
- Workspace path: `build/<ABS_PATH>`
- Robot path: `<ABS_PATH>`
Example: `build/usr/local/etc/jibo-jetstream-service.json` corresponds to `/usr/local/etc/jibo-jetstream-service.json` on-robot.
---
## Highest-level “what runs what”
### `/usr/local/etc/jibo-system-manager.json`
**Purpose:** Master orchestrator config. Defines the service startup order, executables, arguments (including which `-c /usr/local/etc/<service>.json` is used), environment variables, and some device-level settings.
**Why your tooling should care:** if you need to know **which process** consumes a config file, this is the authoritative mapping.
Notable sections:
- `SystemManager.service.services[]`
- `name`, `executable`, `modes.<mode>.arguments` (usually includes the config file path)
- `modes.<mode>.enabled` (turn a service on/off per mode)
- `SystemManager.skills.environment`
- `JIBO_HUB_SHIM_HOST` (host/port target for the hub shim)
- `JIBO_GQA_ENDPOINT` (HTTP endpoint used by the optional GQA shim)
- `credentials.path`: `/var/jibo/credentials.json` (runtime config; controls region selection in Jetstream)
- `wifi.*` (wpa_supplicant + DHCP client options)
- `time.*` (timezone/localtime paths and NTP sync)
**Apply/restart:** system-manager itself (how depends on your init/systemd on the robot). Most changes only take effect on service restart or reboot.
---
## Core robot service configs (`/usr/local/etc/*.json`)
These are the main knobs for the robots built-in services. Many follow this pattern:
- `WebCore.serverPort`: HTTP port for the service
- `<ServiceName>.registryPort`: service registry port (usually `8181`)
- `logging.*`: log levels / syslog routing
### `/usr/local/etc/jibo-service-registry.json`
**Purpose:** Service registry / management web endpoints.
- Ports: `8181` (`WebCore.serverPort`, `ServiceRegistry.registryPort`)
**Apply/restart:** restart `jibo-service-registry`.
### `/usr/local/etc/jibo-jetstream-service.json`
**Purpose:** Jetstream (hub client + streaming audio + wakeword/ASR pipeline glue).
Key knobs:
- `HubClient.override`: where Jetstream sends `/v1/listen` and friends.
- `hub_hostname`, `hub_port`
- Use this to point Jetstream to a **local or server-hosted hub shim**.
- `HubClient.listen_language`
- `HubClient.encoding_type` + `encoding-settings`: `OGG_OPUS` / `FLAC` settings
- `RecogHJ`, `HubAsr`: timing for SOS/EOS and max speech timeouts
**Apply/restart:** restart `jibo-jetstream-service`.
### `/usr/local/etc/jibo-asr-service.json`
**Purpose:** On-robot ASR service (WebSocket event stream + HTTP start/stop), logging, cloud/local STT selection.
Key knobs:
- `webCore.serverPort`: `8088`
- `AsrService.language`
- `AsrService.log_audio`, `log_text`, `log_path`, upload intervals/thresholds
- `AsrService.resident_task` / `resident_audio_channel`: default always-on hotphrase task (e.g. `audio_source_id":"alsa1"`)
- `AsrService.task_templates`: defines ASR pipelines
**Apply/restart:** restart `jibo-asr-service`.
Note: this file contains cloud credentials/keys in this build tree. Treat it as sensitive in your tooling (avoid echoing it into logs).
### `/usr/local/etc/jibo-tts-service.json`
**Purpose:** On-robot TTS voice + audio output.
Key knobs:
- `webCore.serverPort`: `8089`
- `TTSService.resourcePath`: voice resources
- `TTSService.alsaPlaybackDevice`: playback routing
- `voiceParams.*`: speed, volume, max chars, etc.
**Apply/restart:** restart `jibo-tts-service`.
### `/usr/local/etc/jibo-nlu-service.json`
**Purpose:** NLU service (local grammar/model parsing).
Key knobs:
- `webCore.serverPort`: `8787`
- `Service.nlu_data_dir`: NLU model data
- `Service.default_locale`
**Apply/restart:** restart `jibo-nlu-service`.
### `/usr/local/etc/jibo-audio-service.json`
**Purpose:** Audio routing/capture/playback device selection and audio processing thresholds.
Key knobs:
- `WebCore.serverPort`: `8383`
- `AudioService.alsaCaptureDevice`, `alsaPlaybackDevice`
- `AudioService.router*` latencies
- `AudioService.kinematic_model`: points to `/usr/local/etc/jibo-kinematic-model.json`
**Apply/restart:** restart `jibo-audio-service`.
### `/usr/local/etc/jibo-body-service.json`
**Purpose:** Low-level body control: serial devices, offsets, limits, battery thresholds, IMU calibration path.
Key knobs:
- `WebCore.serverPort`: `8282`
- `bodyBoard.*`: `/dev/ttyTHS0/1` devices, offsets, flipped flags, accel/vel limits
- `imu.driver.calibrationFile`: `/var/jibo/imu/imu-cal.json` (runtime file)
- `BodyService.kinematic_model`: points to `/usr/local/etc/jibo-kinematic-model.json`
**Apply/restart:** restart `jibo-body-service`.
### `/usr/local/etc/jibo-kinematic-model.json`
**Purpose:** Robot kinematic model (frame transforms, masses, inertias). Used by audio/body/LPS.
**Apply/restart:** restart consumers (at least `jibo-body-service`, `jibo-audio-service`, and `jibo-lps-service`).
### `/usr/local/etc/jibo-media-service.json`
**Purpose:** Media service camera configuration (CUDA/V4L2 device paths, capture params).
Key knobs:
- `WebCore.serverPort`: `7979`
- `MediaService.camera.*`: `/dev/video0`, `/dev/video1`, flips, AE/AWB tuning
**Apply/restart:** restart `jibo-media-service`.
### `/usr/local/etc/jibo-lps-service.json`
**Purpose:** LPS (Local Perception System) + internal media subsystem + visual awareness pipeline.
Key knobs:
- `WebCoreLPS.serverPort`: `8484`
- `CaptureSubsystem.camera_config_file`: `/usr/local/etc/lps/cameras.json`
- `EngineSubsystem.schemas.*`: `/usr/local/etc/lps/schemas/{normal,focused,minimal}.json`
- `EngineSubsystem.engine.state.entity_config_file`: `/usr/local/etc/lps/entityConfig.json`
- `EngineSubsystem.engine.state.geometry.*`: camera model params in `/var/jibo/lps/*.json` (runtime files)
**Apply/restart:** restart `jibo-lps-service`.
### `/usr/local/etc/lps/cameras.json`
**Purpose:** Camera device list + CUDA capture config + controls presets.
**Apply/restart:** restart `jibo-lps-service` (and anything using the same capture stack).
### `/usr/local/etc/lps/entityConfig.json`
**Purpose:** Entity tracking parameters (people/head tracking, confidence thresholds, trackers, etc.).
**Apply/restart:** restart `jibo-lps-service`.
### `/usr/local/etc/lps/schemas/normal.json`
### `/usr/local/etc/lps/schemas/focused.json`
### `/usr/local/etc/lps/schemas/minimal.json`
**Purpose:** LPS “schema” graphs: which detectors/actions run and at what cadence.
**Apply/restart:** restart `jibo-lps-service`.
### `/usr/local/etc/jibo-identity-service.json`
**Purpose:** Identity/face recognition engine + model paths.
Key knobs:
- `WebCore.serverPort`: `8489`
- `IdentityService.engine.identifier.*`: choose identifier type (deepid/eigenfaces/resnetfaceid) and model paths
- Storage path: `/var/jibo/identity/*` (runtime)
**Apply/restart:** restart `jibo-identity-service`.
### `/usr/local/etc/jibo-server-service.json`
**Purpose:** Cloud/server connection service + notifications.
- `WebCore.serverPort`: `8888`
- `NotificationSubsystem.serverURLSuffix`
**Apply/restart:** restart `jibo-server-service`.
### `/usr/local/etc/jibo-service-center-service.json`
**Purpose:** Service-center web UI/service.
- `WebCore.serverPort`: `9797`
**Apply/restart:** restart `jibo-service-center-service`.
### `/usr/local/etc/jibo-certification-service.json`
**Purpose:** Certification service.
- `WebCore.serverPort`: `9292`
**Apply/restart:** restart `jibo-certification-service`.
### `/usr/local/etc/jibo-system-monitoring-service.json`
**Purpose:** System health/storage monitoring + health log upload.
Key knobs:
- `WebCore.serverPort`: `4111`
- `SystemMonitoringService.storage.semantic`: path aliases used for reporting
- `health.upload.arguments`: uses `/var/jibo/credentials.json` (runtime)
**Apply/restart:** restart `jibo-system-monitoring-service`.
### `/usr/local/etc/jibo-test-capture-service.json`
**Purpose:** Camera capture tools service (debug/QA).
- `WebCore.serverPort`: `7979` (note: overlaps with `jibo-media-service.json` in this tree; only one should bind a given port at runtime)
**Apply/restart:** restart `jibo-test-capture-service`.
### `/usr/local/etc/jibo-test-capture.json`
**Purpose:** Test capture tool runtime behavior (recording toggles, lock counts, display/profiler options).
**Apply/restart:** whatever tool/runner loads it (not a standard service config; used by capture tooling).
### `/usr/local/etc/jibo-camera-calibrator.json`
**Purpose:** Camera calibration workflow + capture device selection. Writes calibration outputs into `/var/jibo/lps`.
**Apply/restart:** used by the calibrator tool; changes apply on next run.
### `/usr/local/etc/jibo-hub-shim.json`
**Purpose:** Robot-local hub shim config (for running the shim on the robot).
Key knobs:
- `listen.*`: bind/port/path for `/v1/listen`
- `asrService.baseUrl`: typically `http://127.0.0.1:8088` on-robot
**Apply/restart:** restart the hub-shim process (not a core Jibo service; depends on your deployment).
### `/usr/local/etc/jibo-sts.json`
**Purpose:** Placeholder config for secure-transfer (STS). In this tree it is currently empty.
**Apply/restart:** N/A (service may use defaults or other config sources).
---
## SSM / robot “mode” configuration (`/usr/local/etc/jibo-ssm/*.json`)
These files configure the Node-based SSM (service supervisor/skill launcher) and its ports per robot mode.
- `/usr/local/etc/jibo-ssm/jibo-ssm-normal.json`
- `/usr/local/etc/jibo-ssm/jibo-ssm-oobe.json`
- `/usr/local/etc/jibo-ssm/jibo-ssm-developer.json`
- `/usr/local/etc/jibo-ssm/jibo-ssm-int-developer.json`
Common knobs:
- `services.SkillsService.port` (HTTP port; typically `8779`)
- `services.DevShell.*` (developer ports: `8686/8989/9191`) (developer/int-developer)
- `services.WifiService.region` (e.g. `api`)
- `RegistryClient.host/port` (usually `127.0.0.1:8181`)
- `logging.namespaces` (fine-grained log routing)
**Apply/restart:** restart SSM / the Node process that loads these configs.
---
## Hub shim (server-hosted) config
This is for the PC/server side shim that emulates the hub `/v1/listen` endpoint.
Workspace source:
- `hub-shim/config.example.json` (template)
- `hub-shim/config.json` (local dev)
- `hub-shim/systemd/jibo-hub-shim.service` (service unit)
- `hub-shim/systemd/jibo-hub-shim.env.example` (env file template)
Server install locations (created by `hub-shim/install-server.sh`):
- `/opt/jibo-hub-shim/` (code)
- `/etc/jibo-hub-shim/config.json` (config)
- `/etc/jibo-hub-shim/jibo-hub-shim.env` (sets `JIBO_HUB_SHIM_CONFIG=/etc/jibo-hub-shim/config.json`)
- `/etc/systemd/system/jibo-hub-shim.service`
Key knobs in the shim config JSON:
- `listen.bindHost`, `listen.port`, `listen.path`
- `asrService.baseUrl`: robot ASR base URL (e.g. `http://<robot-ip>:8088` if shim runs off-robot)
- `asrService.audioSourceId`: commonly `alsa1` on-robot
- `asrService.timeoutMs`
- `gqaShim.enabled` + `gqaShim.ollama.*`: optional Ollama-backed “GQA shim” HTTP service
**Apply/restart:** `systemctl restart jibo-hub-shim` (server). For local dev: restart `node index.js ./config.json`.
---
## AI bridge (PC-side) configuration
### PC server (Python)
- `ai_bridge_server/server.py`
Config sources:
- CLI args: `--host`, `--port` (default run example uses `0.0.0.0:8020`)
- Env vars:
- `OLLAMA_URL` (default `http://127.0.0.1:11434/api/chat`)
- `OLLAMA_MODEL` (default `phi3.5`)
- `WHISPER_MODEL` (default `base`) (audio endpoint only)
**Apply/restart:** restart the Python process.
### Robot-side AI bridge client config
- `/opt/jibo/Jibo/Skills/@be/be/be/ai-bridge-config.json`
Key knobs:
- `enabled`
- `mode`: `TEXT` or `AUDIO`
- `serverBaseUrl`: your PCs bridge server URL (e.g. `http://<pc-ip>:8020`)
- ASR integration:
- `useAsrServiceStt`, `asrServiceHost`, `asrServicePort`, `asrAudioSourceId`, `asrTimeoutMs`, `asrAutoStart`
- Behavior:
- `wakeupChitchatPhrases[]`
- `followupEnabled`, `followupDelayMs`
- `aiForwardingAllowedSkills[]` gating
**Apply/restart:** restart the SkillsService / BE skill (or reboot).
Related dev-only override:
- `/opt/jibo/Jibo/Skills/@be/be/be/jibo-asr-service.local.json` (local-run ASR config variant)
---
## Skills menu / content configs (skill-layer “settings”)
These are not OS-level services, but they are **configuration surfaces** for skill UI/menu behavior.
- `/opt/jibo/Jibo/Skills/@be/menu-entries.d/*.json`
- Defines top-level menu entries/submenus (e.g. `childrenDir` points at a skill folder containing `menuEntry.json`).
- `/opt/jibo/Jibo/Skills/*/menuEntry.json`
- Declares skill menu metadata (title/hidden/etc.).
- `/opt/jibo/Jibo/Skills/@be/be/menu/menus/*.json`
- Defines button menus (labels/icons/utterances + hit area polygon).
**Apply/restart:** restart SkillsService / reload the menu skill.
---
## Init scripts and env-tunable configs (`/etc/init.d/*`)
These scripts are often the real place where **ports and file paths** are chosen, via environment variables.
### `/etc/init.d/S04jibo-asr-service`
- Launches: `/usr/local/bin/jibo-asr-service -c /usr/local/etc/jibo-asr-service.json`
- PID file: `/var/run/jibo-asr-service.pid`
- Log: `/tmp/jibo-asr-service.log`
### `/etc/init.d/S02jibo-skills-logd`
Skills UDP log daemon.
Env vars:
- `JIBO_LOGD_HOST` (default `127.0.0.1`)
- `JIBO_LOGD_PORT` (default `15140`)
- `JIBO_LOGD_FILE` (default `/tmp/jibo-skills.log`)
### `/etc/init.d/S03jibo-skills-logpanel`
Skills web log panel.
Env vars:
- `JIBO_LOGPANEL_BIND` (default `0.0.0.0`)
- `JIBO_LOGPANEL_PORT` (default `15150`)
- `JIBO_LOGD_FILE` (shared logfile path)
### `/etc/init.d/S21firewall`
Firewall rules (iptables). Not JSON, but it is a major “settings surface”.
- Always allows SSH port `22`
- Also explicitly allows SkillsService panel `8779` and log panel `15150`
- Opens additional ports based on the current mode via `/usr/bin/jibo-getmode`
**Apply:** run the init script (restart) or reboot.
---
## Runtime-created or runtime-edited config files (referenced by the above)
These are not present in this build tree, but the services above reference them. Your SSH tooling will often want to read/edit these too.
- `/var/jibo/credentials.json`
- Used for: Jetstream `region-settings` selection; system health upload credentials.
- `/var/jibo/imu/imu-cal.json`
- Used for: BodyService IMU calibration.
- `/var/jibo/lps/CameraModelParamsL.json`
- `/var/jibo/lps/CameraModelParamsR.json`
- `/var/jibo/lps/InterCameraTransform.json`
- Used for: LPS geometry.
- `/var/etc/timezone`, `/var/etc/localtime`
- Used for: system timezone.
---
## Quick port index (useful for “is my edit live?” checks)
From configs in this tree:
- Service registry: `8181`
- Body: `8282`
- Audio: `8383`
- Jetstream: `8090`
- ASR: `8088`
- TTS: `8089`
- NLU: `8787`
- Identity: `8489`
- LPS: `8484` (and internal media `8486`)
- Media: `7979`
- Server service: `8888`
- System manager: `8585`
- System monitoring: `4111`
- Service center: `9797`
- Certification: `9292`
- Skills service: `8779` (SSM)
- Skills logd: UDP `15140`
- Skills log panel: `15150`
- Hub shim (this project): `9000` (server-side shim)
- AI bridge server (PC): `8020`
---
## Suggested conventions for your editor tool
- Always read/parse JSON with comment tolerance disabled (these are strict JSON files).
- Treat these keys as “high risk” and avoid accidentally printing them:
- Anything containing `appkey`, `key`, `credential`, `password`, `token`.
- After edits:
- Restart only the owning service (see sections above).
- Prefer `system-manager` / SSM restart only when necessary.

3334
JiboTools/CONFIG_VALUES.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -156,6 +156,32 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupToolSettings">
<property name="title">
<string>Tool Settings</string>
</property>
<layout class="QFormLayout" name="formToolSettings">
<item row="0" column="0">
<widget class="QLabel" name="labelEnableLogging">
<property name="text">
<string>Enable logging</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="enableLoggingCheck">
<property name="text">
<string/>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupHomeAssistant">
<property name="title">
@@ -193,12 +219,12 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupAiProvider">
<property name="title">
<string>AI Provider</string>
</property>
<layout class="QFormLayout" name="formAiProvider">
<item>
<widget class="QGroupBox" name="groupAiProvider">
<property name="title">
<string>AI Bridge</string>
</property>
<layout class="QFormLayout" name="formAiProvider">
<item row="0" column="0">
<widget class="QLabel" name="labelAiEnable">
<property name="text">
@@ -216,7 +242,7 @@
<item row="1" column="0">
<widget class="QLabel" name="labelAiProvider">
<property name="text">
<string>Provider</string>
<string>Mode</string>
</property>
</widget>
</item>
@@ -226,7 +252,7 @@
<item row="2" column="0">
<widget class="QLabel" name="labelAiEndpoint">
<property name="text">
<string>API endpoint</string>
<string>Server base URL</string>
</property>
</widget>
</item>
@@ -240,28 +266,245 @@
<item row="3" column="0">
<widget class="QLabel" name="labelAiKey">
<property name="text">
<string>API key</string>
<string>ASR host</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="aiKeyField">
<property name="echoMode">
<enum>QLineEdit::EchoMode::Password</enum>
<property name="placeholderText">
<string>127.0.0.1</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelAiBridgeRecordSeconds">
<property name="text">
<string>Record seconds</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="aiBridgeRecordSecondsSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>60</number>
</property>
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelAiBridgeUseAsr">
<property name="text">
<string>Use ASR service STT</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="aiBridgeUseAsrServiceSttCheck">
<property name="text">
<string/>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="labelAiBridgeAsrPort">
<property name="text">
<string>ASR port</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="aiBridgeAsrPortSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>8088</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="labelAiBridgeAsrAudioSource">
<property name="text">
<string>ASR audio source</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="aiBridgeAsrAudioSourceField">
<property name="placeholderText">
<string>alsa1</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="labelAiBridgeAsrTimeout">
<property name="text">
<string>ASR timeout (ms)</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="aiBridgeAsrTimeoutSpin">
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>120000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>15000</number>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="labelAiBridgeAsrAutoStart">
<property name="text">
<string>ASR auto start</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="aiBridgeAsrAutoStartCheck">
<property name="text">
<string/>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="labelAiBridgeFollowupEnabled">
<property name="text">
<string>Followup enabled</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="aiBridgeFollowupEnabledCheck">
<property name="text">
<string/>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="labelAiBridgeFollowupDelay">
<property name="text">
<string>Followup delay (ms)</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QSpinBox" name="aiBridgeFollowupDelaySpin">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>60000</number>
</property>
<property name="singleStep">
<number>50</number>
</property>
<property name="value">
<number>250</number>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="labelAiBridge">
<property name="text">
<string>Advanced</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QPushButton" name="editAiBridgeConfigButton">
<property name="text">
<string>Edit AI Bridge Config</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupConfigFiles">
<property name="title">
<string>Config Files</string>
</property>
<layout class="QVBoxLayout" name="configFilesLayout">
<property name="spacing">
<number>8</number>
</property>
<item>
<layout class="QHBoxLayout" name="configFilesTopRow">
<property name="spacing">
<number>8</number>
</property>
<item>
<widget class="QComboBox" name="configFileCombo"/>
</item>
<item>
<widget class="QPushButton" name="configReadButton">
<property name="text">
<string>Read</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="configWriteButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Write</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="configFileStatusLabel">
<property name="text">
<string>Select a config to view/edit</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelTokens">
<item>
<widget class="QPlainTextEdit" name="configEditor"/>
</item>
<item>
<widget class="QLabel" name="configLogLabel">
<property name="text">
<string>Tokens used</string>
<string>Activity log</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="tokensUsedLabel">
<property name="text">
<string>-1</string>
<item>
<widget class="QPlainTextEdit" name="configActivityLog">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f2f2f2"/>
<stop offset="1" stop-color="#d9d9d9"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="256" height="256" fill="none"/>
<circle cx="128" cy="128" r="110" fill="url(#g)" stroke="#9a9a9a" stroke-width="6"/>
<circle cx="128" cy="128" r="70" fill="#ffffff" stroke="#b3b3b3" stroke-width="6"/>
<rect x="88" y="168" width="80" height="26" rx="13" fill="#c7c7c7"/>
<text x="128" y="128" font-family="sans-serif" font-size="28" text-anchor="middle" fill="#444">Jibo</text>
</svg>

Before

Width:  |  Height:  |  Size: 696 B

View File

@@ -0,0 +1,131 @@
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional
_SENSITIVE_SUBSTRINGS = ("appkey", "key", "credential", "password", "token")
@dataclass(frozen=True)
class ConfigEntry:
remote_path: str
@property
def is_usr_local_etc(self) -> bool:
return self.remote_path.startswith("/usr/local/etc/")
class _Missing:
pass
MISSING = _Missing()
_HEADING_RE = re.compile(r"^###\s+(/\S+)")
def _find_inventory_file(filename: str) -> Optional[Path]:
"""Find inventory markdown files shipped with the repo.
The GUI package layout is:
JiboTools/JiboTools/gui/*.py
so we search:
- JiboTools/ (packaged copy)
- repo root (workspace copy)
"""
here = Path(__file__).resolve()
pkg_root = here.parents[2] # .../JiboTools
repo_root = pkg_root.parent # .../JiboAutoModv2
for p in (pkg_root / filename, repo_root / filename):
if p.exists():
return p
return None
def load_config_entries_from_values_md() -> list[ConfigEntry]:
values_md = _find_inventory_file("CONFIG_VALUES.md")
if values_md is None:
return []
entries: list[ConfigEntry] = []
seen: set[str] = set()
for line in values_md.read_text("utf-8", errors="replace").splitlines():
m = _HEADING_RE.match(line.strip())
if not m:
continue
path = m.group(1).strip()
if not path.startswith("/"):
continue
if path in seen:
continue
# Filter out non-robot/server dev configs the user doesn't want here.
if path.startswith("/hub-shim/"):
continue
if path.lower().endswith(".md"):
continue
# Keep it focused on JSON files (these are strict JSON configs).
if not path.lower().endswith(".json"):
continue
seen.add(path)
entries.append(ConfigEntry(remote_path=path))
return entries
def is_sensitive_path(path: str) -> bool:
p = path.lower()
return any(s in p for s in _SENSITIVE_SUBSTRINGS)
def short_json(value: Any, *, limit: int = 180) -> str:
try:
s = json.dumps(value, ensure_ascii=False)
except Exception:
s = repr(value)
if len(s) > limit:
return s[: limit - 3] + "..."
return s
def diff_json(old: Any, new: Any, prefix: str = "") -> list[tuple[str, Any, Any]]:
diffs: list[tuple[str, Any, Any]] = []
if isinstance(old, dict) and isinstance(new, dict):
keys = set(old.keys()) | set(new.keys())
for key in sorted(keys, key=lambda x: str(x)):
old_value = old.get(key, MISSING)
new_value = new.get(key, MISSING)
child_prefix = f"{prefix}.{key}" if prefix else str(key)
if old_value is MISSING or new_value is MISSING:
diffs.append((child_prefix, old_value, new_value))
else:
diffs.extend(diff_json(old_value, new_value, child_prefix))
return diffs
if isinstance(old, list) and isinstance(new, list):
max_len = max(len(old), len(new))
for i in range(max_len):
old_value = old[i] if i < len(old) else MISSING
new_value = new[i] if i < len(new) else MISSING
child_prefix = f"{prefix}[{i}]"
if old_value is MISSING or new_value is MISSING:
diffs.append((child_prefix, old_value, new_value))
else:
diffs.extend(diff_json(old_value, new_value, child_prefix))
return diffs
if old != new:
diffs.append((prefix or "<root>", old, new))
return diffs

View File

@@ -4,7 +4,7 @@ import json
import subprocess
import sys
from pathlib import Path
from typing import Optional
from typing import Any, Optional
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap
@@ -16,12 +16,15 @@ from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QLineEdit,
QPlainTextEdit,
QPushButton,
QSpinBox,
QTabWidget,
)
from .process_runner import resolve_python, resolve_python_invocation
from .ui_loader import load_ui, require_child
from .config_inventory import MISSING, diff_json, is_sensitive_path, load_config_entries_from_values_md, short_json
def _set_dot(label: QLabel, color: str) -> None:
@@ -34,6 +37,8 @@ def _set_dot(label: QLabel, color: str) -> None:
class MainWindowController:
_AI_BRIDGE_PATH = "/opt/jibo/Jibo/Skills/@be/be/be/ai-bridge-config.json"
def __init__(self) -> None:
project_root = Path(__file__).resolve().parents[1]
ui_path = project_root / "form.ui"
@@ -60,17 +65,44 @@ class MainWindowController:
self.ha_enable = require_child(self.window, "haEnableCheck", QCheckBox)
self.ha_server_ip = require_child(self.window, "haServerIpField", QLineEdit)
# AI Bridge (formerly "AI Provider")
self.ai_enable = require_child(self.window, "aiEnableCheck", QCheckBox)
self.ai_provider = require_child(self.window, "aiProviderCombo", QComboBox)
self.ai_endpoint = require_child(self.window, "aiEndpointField", QLineEdit)
self.ai_key = require_child(self.window, "aiKeyField", QLineEdit)
self.tokens_used = require_child(self.window, "tokensUsedLabel", QLabel)
self.ai_mode = require_child(self.window, "aiProviderCombo", QComboBox)
self.ai_server_base_url = require_child(self.window, "aiEndpointField", QLineEdit)
self.ai_asr_host = require_child(self.window, "aiKeyField", QLineEdit)
self.ai_record_seconds = require_child(self.window, "aiBridgeRecordSecondsSpin", QSpinBox)
self.ai_use_asr_service_stt = require_child(self.window, "aiBridgeUseAsrServiceSttCheck", QCheckBox)
self.ai_asr_port = require_child(self.window, "aiBridgeAsrPortSpin", QSpinBox)
self.ai_asr_audio_source = require_child(self.window, "aiBridgeAsrAudioSourceField", QLineEdit)
self.ai_asr_timeout_ms = require_child(self.window, "aiBridgeAsrTimeoutSpin", QSpinBox)
self.ai_asr_auto_start = require_child(self.window, "aiBridgeAsrAutoStartCheck", QCheckBox)
self.ai_followup_enabled = require_child(self.window, "aiBridgeFollowupEnabledCheck", QCheckBox)
self.ai_followup_delay_ms = require_child(self.window, "aiBridgeFollowupDelaySpin", QSpinBox)
self.edit_ai_bridge_button = require_child(self.window, "editAiBridgeConfigButton", QPushButton)
self._ai_bridge_obj: Optional[dict[str, Any]] = None
# Tool settings
self.enable_logging_check = require_child(self.window, "enableLoggingCheck", QCheckBox)
# Config editor (main panel "Config" section)
self.config_file_combo = require_child(self.window, "configFileCombo", QComboBox)
self.config_read_button = require_child(self.window, "configReadButton", QPushButton)
self.config_write_button = require_child(self.window, "configWriteButton", QPushButton)
self.config_status_label = require_child(self.window, "configFileStatusLabel", QLabel)
self.config_editor = require_child(self.window, "configEditor", QPlainTextEdit)
self.config_activity_log = require_child(self.window, "configActivityLog", QPlainTextEdit)
self._config_last_read_text: Optional[str] = None
self._config_paths: list[str] = []
# Jibo card controls
self.robot_settings_button = require_child(self.window, "RobotSettings", QPushButton)
self.robot_action_combo = require_child(self.window, "comboBox", QComboBox)
self.jibo_image = require_child(self.window, "jiboImage", QLabel)
self._robot_settings_window: Optional[object] = None
# Update page
self.install_button = require_child(self.window, "installButton", QPushButton)
self.check_updates_button = require_child(self.window, "checkUpdatesButton", QPushButton)
@@ -83,6 +115,7 @@ class MainWindowController:
self._wire_signals()
self._sync_enabled()
self._sync_all()
self._populate_config_file_combo()
@property
def host(self) -> str:
@@ -113,16 +146,22 @@ class MainWindowController:
"}"
)
# Provider choices
self.ai_provider.clear()
self.ai_provider.addItems(["Self-hosted", "OpenAI", "Other"])
# AI Bridge mode choices
self.ai_mode.clear()
self.ai_mode.addItems(["TEXT", "AUDIO"])
# Robot controls start disabled until connected.
self.robot_settings_button.setEnabled(False)
self.robot_action_combo.setEnabled(False)
# Config editor defaults
self.config_editor.setPlaceholderText("Select a config file, then Read")
self.config_activity_log.setReadOnly(True)
self.config_activity_log.setPlaceholderText("Logging is disabled")
self.config_read_button.setEnabled(False)
self.config_write_button.setEnabled(False)
# Defaults
self.tokens_used.setText("-1")
self.connect_button.setText("Connect")
self.jibo_title.setText("Connect Your Jibo")
@@ -136,21 +175,83 @@ class MainWindowController:
self.ha_enable.toggled.connect(self._sync_enabled)
self.ai_enable.toggled.connect(self._sync_enabled)
self.robot_settings_button.clicked.connect(self._open_robot_settings)
self.edit_ai_bridge_button.clicked.connect(self._jump_to_ai_bridge_config)
# Keep AI Bridge in-memory config in sync with UI edits.
self.ai_enable.toggled.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_mode.currentIndexChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_server_base_url.textChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_asr_host.textChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_record_seconds.valueChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_use_asr_service_stt.toggled.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_asr_port.valueChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_asr_audio_source.textChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_asr_timeout_ms.valueChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_asr_auto_start.toggled.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_followup_enabled.toggled.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_followup_delay_ms.valueChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.config_file_combo.currentIndexChanged.connect(self._on_config_combo_changed)
self.config_read_button.clicked.connect(self._read_selected_config)
self.config_write_button.clicked.connect(self._write_selected_config)
self.config_editor.textChanged.connect(self._on_config_editor_changed)
self.enable_logging_check.toggled.connect(self._sync_logging_placeholders)
self.install_button.clicked.connect(self._launch_installer)
self.check_updates_button.clicked.connect(self._launch_updater)
def _open_robot_settings(self) -> None:
if not self.session_connected or self._ssh_client is None:
self.status_text.setText("Connect to a Jibo first")
return
try:
from .robot_settings_window import RobotSettingsWindow # local import to keep startup fast
except Exception as e:
self.status_text.setText(f"Failed to load Robot Settings UI: {e}")
return
if self._robot_settings_window is None:
self._robot_settings_window = RobotSettingsWindow(
ssh_client=self._ssh_client,
logging_enabled_check=self.enable_logging_check,
)
# Refresh the SSH client reference in case we reconnected.
try:
self._robot_settings_window.set_ssh_client(self._ssh_client) # type: ignore[attr-defined]
except Exception:
pass
try:
self._robot_settings_window.show() # type: ignore[attr-defined]
except Exception:
pass
def _sync_enabled(self) -> None:
self.preview_connected_check.setEnabled(self.override_check.isChecked())
self.ha_server_ip.setEnabled(self.ha_enable.isChecked())
ai_enabled = self.ai_enable.isChecked()
self.ai_provider.setEnabled(ai_enabled)
self.ai_endpoint.setEnabled(ai_enabled)
self.ai_key.setEnabled(ai_enabled)
self.ai_mode.setEnabled(ai_enabled)
self.ai_server_base_url.setEnabled(ai_enabled)
self.ai_asr_host.setEnabled(ai_enabled)
self.ai_record_seconds.setEnabled(ai_enabled)
self.ai_use_asr_service_stt.setEnabled(ai_enabled)
self.ai_asr_port.setEnabled(ai_enabled)
self.ai_asr_audio_source.setEnabled(ai_enabled)
self.ai_asr_timeout_ms.setEnabled(ai_enabled)
self.ai_asr_auto_start.setEnabled(ai_enabled)
self.ai_followup_enabled.setEnabled(ai_enabled)
self.ai_followup_delay_ms.setEnabled(ai_enabled)
# Connection button enabled unless a connect attempt is in progress.
self.connect_button.setEnabled(not self._connecting)
connected = self.session_connected
self.config_read_button.setEnabled(connected and self.config_file_combo.count() > 0)
# write button is controlled by editor dirty state
def _sync_all(self) -> None:
host = self.host
connected = self.session_connected
@@ -166,6 +267,8 @@ class MainWindowController:
self.robot_action_combo.setEnabled(connected)
self.connect_button.setText("Disconnect" if connected else "Connect")
self._sync_enabled()
dot_color = "#2ecc71" if connected else ("#e67e22" if host else "#bdc3c7")
_set_dot(self.conn_dot, dot_color)
_set_dot(self.status_dot, dot_color)
@@ -215,8 +318,304 @@ class MainWindowController:
finally:
self._ssh_client = None
self._identity = None
if self._robot_settings_window is not None:
try:
self._robot_settings_window.set_ssh_client(None) # type: ignore[attr-defined]
except Exception:
pass
self.config_status_label.setText("Disconnected")
self._config_last_read_text = None
self.config_write_button.setEnabled(False)
self._sync_all()
def _sync_logging_placeholders(self) -> None:
if self.enable_logging_check.isChecked():
self.config_activity_log.setPlaceholderText("")
else:
self.config_activity_log.setPlaceholderText("Logging is disabled")
def _log(self, message: str) -> None:
if not self.enable_logging_check.isChecked():
return
self.config_activity_log.appendPlainText(message)
def _populate_config_file_combo(self) -> None:
# Populate from inventory, excluding /usr/local/etc (those belong under Robot Settings)
entries = load_config_entries_from_values_md()
paths = [e.remote_path for e in entries if not e.is_usr_local_etc]
paths = sorted(paths)
self._config_paths = paths
self.config_file_combo.blockSignals(True)
try:
self.config_file_combo.clear()
self.config_file_combo.addItems(paths)
finally:
self.config_file_combo.blockSignals(False)
if paths:
self.config_status_label.setText("Select a config to view/edit")
else:
self.config_status_label.setText("No configs found in inventory")
def _jump_to_ai_bridge_config(self) -> None:
target = self._AI_BRIDGE_PATH
idx = self.config_file_combo.findText(target)
if idx < 0:
self.config_file_combo.addItem(target)
idx = self.config_file_combo.findText(target)
if idx >= 0:
self.config_file_combo.setCurrentIndex(idx)
self._read_selected_config()
# Seed editor with the current AI Bridge UI state so the user can
# immediately press Write.
try:
merged = self._merged_ai_bridge_obj_from_ui()
desired_text = json.dumps(merged, indent=2, ensure_ascii=False) + "\n"
self.config_editor.setPlainText(desired_text)
except Exception:
pass
return
self.config_status_label.setText("AI Bridge config not selectable")
def _on_config_combo_changed(self) -> None:
self._config_last_read_text = None
self.config_write_button.setEnabled(False)
p = self._selected_config_path()
self.config_status_label.setText(p or "Select a config to view/edit")
def _selected_config_path(self) -> Optional[str]:
p = self.config_file_combo.currentText().strip()
if p.startswith("/"):
return p
return None
def _sftp_read_text(self, remote_path: str) -> str:
if self._ssh_client is None:
raise RuntimeError("Not connected")
sftp = self._ssh_client.open_sftp()
try:
with sftp.open(remote_path, "r") as f:
raw = f.read()
finally:
sftp.close()
if isinstance(raw, bytes):
return raw.decode("utf-8", errors="replace")
return str(raw)
def _sftp_write_text(self, remote_path: str, text: str) -> None:
if self._ssh_client is None:
raise RuntimeError("Not connected")
sftp = self._ssh_client.open_sftp()
try:
with sftp.open(remote_path, "w") as f:
f.write(text.encode("utf-8"))
finally:
sftp.close()
def _ssh_exec(self, command: str, *, timeout: int = 30) -> tuple[int, str, str]:
if self._ssh_client is None:
raise RuntimeError("Not connected")
stdin, stdout, stderr = self._ssh_client.exec_command(command, timeout=timeout)
_ = stdin
out = stdout.read()
err = stderr.read()
out_s = out.decode("utf-8", errors="replace") if isinstance(out, bytes) else str(out)
err_s = err.decode("utf-8", errors="replace") if isinstance(err, bytes) else str(err)
code = stdout.channel.recv_exit_status()
return int(code), out_s, err_s
def _read_selected_config(self) -> None:
p = self._selected_config_path()
if not p:
return
if not self.session_connected:
self.config_status_label.setText("Connect to a Jibo first")
return
try:
text = self._sftp_read_text(p)
self._config_last_read_text = text
self.config_editor.setPlainText(text)
self.config_write_button.setEnabled(False)
self.config_status_label.setText(f"Loaded {p}")
self._log(f"READ {p} ({len(text)} bytes)")
if p == self._AI_BRIDGE_PATH:
try:
obj = json.loads(text)
if isinstance(obj, dict):
self._ai_bridge_obj = obj
self._apply_ai_bridge_obj_to_ui(obj)
except Exception:
pass
except Exception as e:
self.config_status_label.setText(f"Read failed: {e}")
self._log(f"READ FAILED {p}: {e}")
def _load_ai_bridge_from_robot(self) -> None:
if self._ssh_client is None:
return
try:
raw = self._sftp_read_text(self._AI_BRIDGE_PATH)
except Exception:
return
try:
obj = json.loads(raw)
except Exception:
return
if not isinstance(obj, dict):
return
self._ai_bridge_obj = obj
self._apply_ai_bridge_obj_to_ui(obj)
self._log(f"READ {self._AI_BRIDGE_PATH} (auto)")
def _apply_ai_bridge_obj_to_ui(self, obj: dict[str, Any]) -> None:
widgets = [
self.ai_enable,
self.ai_mode,
self.ai_server_base_url,
self.ai_asr_host,
self.ai_record_seconds,
self.ai_use_asr_service_stt,
self.ai_asr_port,
self.ai_asr_audio_source,
self.ai_asr_timeout_ms,
self.ai_asr_auto_start,
self.ai_followup_enabled,
self.ai_followup_delay_ms,
]
for w in widgets:
w.blockSignals(True)
try:
self.ai_enable.setChecked(bool(obj.get("enabled", True)))
mode = str(obj.get("mode", "TEXT")).upper()
idx = self.ai_mode.findText(mode)
if idx >= 0:
self.ai_mode.setCurrentIndex(idx)
self.ai_server_base_url.setText(str(obj.get("serverBaseUrl", "")))
self.ai_record_seconds.setValue(int(obj.get("recordSeconds", 5)))
self.ai_use_asr_service_stt.setChecked(bool(obj.get("useAsrServiceStt", True)))
self.ai_asr_host.setText(str(obj.get("asrServiceHost", "127.0.0.1")))
self.ai_asr_port.setValue(int(obj.get("asrServicePort", 8088)))
self.ai_asr_audio_source.setText(str(obj.get("asrAudioSourceId", "alsa1")))
self.ai_asr_timeout_ms.setValue(int(obj.get("asrTimeoutMs", 15000)))
self.ai_asr_auto_start.setChecked(bool(obj.get("asrAutoStart", True)))
self.ai_followup_enabled.setChecked(bool(obj.get("followupEnabled", True)))
self.ai_followup_delay_ms.setValue(int(obj.get("followupDelayMs", 250)))
finally:
for w in widgets:
w.blockSignals(False)
self._sync_enabled()
def _ai_bridge_fields_from_ui(self) -> dict[str, Any]:
mode = self.ai_mode.currentText().strip() or "TEXT"
return {
"enabled": bool(self.ai_enable.isChecked()),
"mode": mode,
"serverBaseUrl": self.ai_server_base_url.text().strip(),
"recordSeconds": int(self.ai_record_seconds.value()),
"useAsrServiceStt": bool(self.ai_use_asr_service_stt.isChecked()),
"asrServiceHost": self.ai_asr_host.text().strip() or "127.0.0.1",
"asrServicePort": int(self.ai_asr_port.value()),
"asrAudioSourceId": self.ai_asr_audio_source.text().strip() or "alsa1",
"asrTimeoutMs": int(self.ai_asr_timeout_ms.value()),
"asrAutoStart": bool(self.ai_asr_auto_start.isChecked()),
"followupEnabled": bool(self.ai_followup_enabled.isChecked()),
"followupDelayMs": int(self.ai_followup_delay_ms.value()),
}
def _merged_ai_bridge_obj_from_ui(self) -> dict[str, Any]:
base: dict[str, Any] = {}
if isinstance(self._ai_bridge_obj, dict):
base.update(self._ai_bridge_obj)
base.update(self._ai_bridge_fields_from_ui())
return base
def _sync_ai_bridge_obj_from_ui(self, *_args: Any) -> None:
# Keep unknown keys (if any) from the on-robot JSON.
try:
self._ai_bridge_obj = self._merged_ai_bridge_obj_from_ui()
except Exception:
pass
def _on_config_editor_changed(self) -> None:
if self._config_last_read_text is None:
self.config_write_button.setEnabled(False)
return
self.config_write_button.setEnabled(self.config_editor.toPlainText() != self._config_last_read_text)
def _write_selected_config(self) -> None:
p = self._selected_config_path()
if not p:
return
if not self.session_connected:
self.config_status_label.setText("Connect to a Jibo first")
return
new_text_raw = self.config_editor.toPlainText()
try:
new_obj = json.loads(new_text_raw)
except Exception as e:
self.config_status_label.setText(f"Invalid JSON: {e}")
return
new_text = json.dumps(new_obj, indent=2, ensure_ascii=False) + "\n"
try:
old_text = self._sftp_read_text(p)
except Exception:
old_text = ""
try:
old_obj: Any = json.loads(old_text) if old_text else MISSING
except Exception:
old_obj = MISSING
# Safety: if a /usr/local path ever ends up here, handle remount.
if p.startswith("/usr/local/"):
cmd = "mount -o remount,rw /usr/local"
self._log(f"EXEC {cmd}")
code, _out, err = self._ssh_exec(cmd, timeout=30)
if code != 0:
self.config_status_label.setText("Remount /usr/local failed")
self._log(f"EXEC FAILED ({code}) {cmd} :: {err.strip()}")
return
if old_obj is not MISSING:
diffs = diff_json(old_obj, new_obj)
if diffs:
self._log(f"WRITE {p} (changes: {len(diffs)})")
for path, ov, nv in diffs[:200]:
if is_sensitive_path(path):
self._log(f" {path}: *** -> ***")
continue
o = "<missing>" if ov is MISSING else short_json(ov)
n = "<missing>" if nv is MISSING else short_json(nv)
self._log(f" {path}: {o} -> {n}")
if len(diffs) > 200:
self._log(f" ... ({len(diffs) - 200} more)")
else:
self._log(f"WRITE {p} (no JSON diffs detected)")
else:
self._log(f"WRITE {p} (no previous JSON to diff)")
try:
self._sftp_write_text(p, new_text)
self._log(f"WROTE {p} ({len(new_text)} bytes)")
self._config_last_read_text = new_text
self.config_editor.setPlainText(new_text)
self.config_write_button.setEnabled(False)
self.config_status_label.setText(f"Wrote {p}")
except Exception as e:
self.config_status_label.setText(f"Write failed: {e}")
self._log(f"WRITE FAILED {p}: {e}")
def _toggle_connection(self) -> None:
if self.session_connected:
self._disconnect()
@@ -271,6 +670,12 @@ class MainWindowController:
self._ssh_client = client
self._identity = identity if isinstance(identity, dict) else None
self.status_text.setText(f"Connected via SSH to {host}")
# Auto-populate AI Bridge section when connected.
try:
self._load_ai_bridge_from_robot()
except Exception:
pass
except Exception as e:
try:
client.close()

View File

@@ -0,0 +1,313 @@
from __future__ import annotations
import json
import time
from typing import Any, Optional
from PySide6.QtCore import Qt, Slot
from PySide6.QtWidgets import (
QAbstractItemView,
QHBoxLayout,
QLabel,
QMainWindow,
QMessageBox,
QPushButton,
QPlainTextEdit,
QSplitter,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
QCheckBox,
)
from .config_inventory import ConfigEntry, MISSING, diff_json, is_sensitive_path, load_config_entries_from_values_md, short_json
class RobotSettingsWindow:
def __init__(self, *, ssh_client: object, logging_enabled_check: QCheckBox) -> None:
self._ssh_client: Optional[object] = ssh_client
self._logging_enabled_check = logging_enabled_check
self.window = QMainWindow()
self.window.setWindowTitle("Robot Settings")
root = QWidget()
self.window.setCentralWidget(root)
outer = QVBoxLayout(root)
outer.setContentsMargins(12, 12, 12, 12)
outer.setSpacing(10)
self.status = QLabel("Select a config to view/edit")
self.status.setTextInteractionFlags(Qt.TextSelectableByMouse)
outer.addWidget(self.status)
splitter = QSplitter(Qt.Horizontal)
outer.addWidget(splitter, 1)
# Left: tree
self.tree = QTreeWidget()
self.tree.setHeaderHidden(True)
self.tree.setSelectionMode(QAbstractItemView.SingleSelection)
splitter.addWidget(self.tree)
# Right: editor + buttons + log
right = QWidget()
right_layout = QVBoxLayout(right)
right_layout.setContentsMargins(0, 0, 0, 0)
right_layout.setSpacing(8)
btn_row = QWidget()
btn_layout = QHBoxLayout(btn_row)
btn_layout.setContentsMargins(0, 0, 0, 0)
btn_layout.setSpacing(8)
self.read_button = QPushButton("Read")
self.write_button = QPushButton("Write")
self.write_button.setEnabled(False)
btn_layout.addWidget(self.read_button)
btn_layout.addWidget(self.write_button)
btn_layout.addStretch(1)
right_layout.addWidget(btn_row)
self.editor = QPlainTextEdit()
self.editor.setPlaceholderText("Select a config file from the list to load it")
right_layout.addWidget(self.editor, 1)
self.log = QPlainTextEdit()
self.log.setReadOnly(True)
self.log.setMaximumBlockCount(2000)
self.log.setPlaceholderText("Logging is disabled")
right_layout.addWidget(QLabel("Activity log"))
right_layout.addWidget(self.log, 1)
splitter.addWidget(right)
splitter.setStretchFactor(0, 0)
splitter.setStretchFactor(1, 1)
splitter.setSizes([260, 520])
self._entries: list[ConfigEntry] = []
self._current_remote_path: Optional[str] = None
self._last_read_text: Optional[str] = None
self._populate_tree()
self._wire_signals()
self._sync_logging_ui()
def show(self) -> None:
self.window.show()
self.window.raise_()
self.window.activateWindow()
def set_ssh_client(self, client: Optional[object]) -> None:
self._ssh_client = client
def _wire_signals(self) -> None:
self.tree.itemSelectionChanged.connect(self._on_tree_selection)
self.read_button.clicked.connect(self._read_current)
self.write_button.clicked.connect(self._write_current)
self.editor.textChanged.connect(self._on_editor_changed)
self._logging_enabled_check.toggled.connect(self._sync_logging_ui)
def _sync_logging_ui(self) -> None:
if self._logging_enabled_check.isChecked():
self.log.setPlaceholderText("")
else:
self.log.setPlaceholderText("Logging is disabled")
def _log(self, message: str) -> None:
if not self._logging_enabled_check.isChecked():
return
ts = time.strftime("%H:%M:%S")
self.log.appendPlainText(f"[{ts}] {message}")
def _populate_tree(self) -> None:
self.tree.clear()
self._entries = load_config_entries_from_values_md()
root_settings = QTreeWidgetItem(["Settings (/usr/local/etc)"])
self.tree.addTopLevelItem(root_settings)
root_settings.setExpanded(True)
def add_entry(parent: QTreeWidgetItem, entry: ConfigEntry) -> None:
item = QTreeWidgetItem([entry.remote_path])
item.setData(0, Qt.UserRole, entry.remote_path)
parent.addChild(item)
count = 0
for e in sorted(self._entries, key=lambda x: x.remote_path):
if e.is_usr_local_etc:
add_entry(root_settings, e)
count += 1
if count == 0:
root_settings.addChild(QTreeWidgetItem(["(No /usr/local/etc configs found in inventory)"]))
def _selected_remote_path(self) -> Optional[str]:
items = self.tree.selectedItems()
if not items:
return None
item = items[0]
p = item.data(0, Qt.UserRole)
if isinstance(p, str) and p.startswith("/"):
return p
return None
@Slot()
def _on_tree_selection(self) -> None:
p = self._selected_remote_path()
self._current_remote_path = p
self.write_button.setEnabled(False)
self._last_read_text = None
if p:
self.status.setText(p)
self._read_current()
else:
self.status.setText("Select a config to view/edit")
@Slot()
def _on_editor_changed(self) -> None:
# Enable write only if we have a loaded file and text changed.
if not self._current_remote_path or self._last_read_text is None:
self.write_button.setEnabled(False)
return
self.write_button.setEnabled(self.editor.toPlainText() != self._last_read_text)
def _require_ssh(self) -> object:
if self._ssh_client is None:
raise RuntimeError("Not connected")
return self._ssh_client
def _ssh_exec(self, command: str, *, timeout: int = 30) -> tuple[int, str, str]:
client = self._require_ssh()
stdin, stdout, stderr = client.exec_command(command, timeout=timeout) # type: ignore[attr-defined]
_ = stdin
out = stdout.read()
err = stderr.read()
if isinstance(out, bytes):
out_s = out.decode("utf-8", errors="replace")
else:
out_s = str(out)
if isinstance(err, bytes):
err_s = err.decode("utf-8", errors="replace")
else:
err_s = str(err)
code = stdout.channel.recv_exit_status() # type: ignore[attr-defined]
return int(code), out_s, err_s
def _sftp_read_text(self, remote_path: str) -> str:
client = self._require_ssh()
sftp = client.open_sftp() # type: ignore[attr-defined]
try:
with sftp.open(remote_path, "r") as f:
raw = f.read()
finally:
sftp.close()
if isinstance(raw, bytes):
return raw.decode("utf-8", errors="replace")
return str(raw)
def _sftp_write_text(self, remote_path: str, text: str) -> None:
client = self._require_ssh()
sftp = client.open_sftp() # type: ignore[attr-defined]
try:
with sftp.open(remote_path, "w") as f:
data = text.encode("utf-8")
f.write(data)
finally:
sftp.close()
@Slot()
def _read_current(self) -> None:
remote_path = self._current_remote_path
if not remote_path:
return
try:
text = self._sftp_read_text(remote_path)
self._log(f"READ {remote_path} ({len(text)} bytes)")
self.editor.setPlainText(text)
self._last_read_text = text
self.write_button.setEnabled(False)
except Exception as e:
self.status.setText(f"Read failed: {e}")
self._log(f"READ FAILED {remote_path}: {e}")
QMessageBox.critical(self.window, "Read failed", str(e))
@Slot()
def _write_current(self) -> None:
remote_path = self._current_remote_path
if not remote_path:
return
new_text_raw = self.editor.toPlainText()
# Validate JSON if possible; this tool is focused on strict JSON configs.
try:
new_obj = json.loads(new_text_raw)
except Exception as e:
QMessageBox.warning(self.window, "Invalid JSON", f"JSON parse failed: {e}")
return
# Canonicalize to keep robot-side JSON strict/clean.
new_text = json.dumps(new_obj, indent=2, ensure_ascii=False) + "\n"
try:
old_text = self._sftp_read_text(remote_path)
except Exception:
old_text = ""
old_obj: Any
try:
old_obj = json.loads(old_text) if old_text else MISSING
except Exception:
old_obj = MISSING
# Mounted dir special case: /usr/local/* is often read-only until remount.
if remote_path.startswith("/usr/local/"):
cmd = "mount -o remount,rw /usr/local"
self._log(f"EXEC {cmd}")
code, out, err = self._ssh_exec(cmd, timeout=30)
if code != 0:
self._log(f"EXEC FAILED ({code}) {cmd} :: {err.strip()}")
QMessageBox.critical(
self.window,
"Remount failed",
f"Failed to remount /usr/local read-write (exit {code}).\n\n{err.strip()}",
)
return
if out.strip():
self._log(out.strip())
# Compute diffs (best-effort).
if old_obj is not MISSING:
diffs = diff_json(old_obj, new_obj)
if diffs:
self._log(f"WRITE {remote_path} (changes: {len(diffs)})")
for p, ov, nv in diffs[:200]:
if is_sensitive_path(p):
self._log(f" {p}: *** -> ***")
continue
o = "<missing>" if ov is MISSING else short_json(ov)
n = "<missing>" if nv is MISSING else short_json(nv)
self._log(f" {p}: {o} -> {n}")
if len(diffs) > 200:
self._log(f" ... ({len(diffs) - 200} more)")
else:
self._log(f"WRITE {remote_path} (no JSON diffs detected)")
else:
self._log(f"WRITE {remote_path} (no previous JSON to diff)")
try:
self._sftp_write_text(remote_path, new_text)
self._log(f"WROTE {remote_path} ({len(new_text)} bytes)")
# Refresh read baseline.
self.editor.setPlainText(new_text)
self._last_read_text = new_text
self.write_button.setEnabled(False)
self.status.setText(f"Wrote {remote_path}")
except Exception as e:
self.status.setText(f"Write failed: {e}")
self._log(f"WRITE FAILED {remote_path}: {e}")
QMessageBox.critical(self.window, "Write failed", str(e))

View File

@@ -98,6 +98,61 @@
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="descriptionLabel">
<property name="text">
<string>Tool description</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="dumpLayout">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QCheckBox" name="useExistingDumpCheck">
<property name="text">
<string>I already have a full eMMC dump (.bin)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="dumpPathField">
<property name="placeholderText">
<string>Dump path (passed as --dump-path)</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browseDumpButton">
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="currentStepLabel">
<property name="text">
<string>Idle</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="logEdit">
<property name="readOnly">

View File

@@ -1,11 +1,22 @@
from __future__ import annotations
import shlex
import re
from pathlib import Path
from PySide6.QtCore import QObject, Slot
from PySide6.QtGui import QCloseEvent, QTextCursor
from PySide6.QtWidgets import QMainWindow, QApplication, QLabel, QLineEdit, QPushButton, QPlainTextEdit
from PySide6.QtWidgets import (
QMainWindow,
QApplication,
QLabel,
QLineEdit,
QPushButton,
QPlainTextEdit,
QCheckBox,
QFileDialog,
QProgressBar,
)
from .process_runner import ProcessRunner, resolve_python_invocation
from .terminal_helper import TerminalHelper
@@ -18,6 +29,10 @@ class ToolRunnerWindow(QObject):
self._script = script
self._is_updater = "jibo_updater.py" in script
self._is_installer = "jibo_automod.py" in script
self._output_buffer = ""
self._last_step_total: int | None = None
ui_path = Path(__file__).resolve().parent / "tool_runner.ui"
self.window = load_ui(ui_path)
@@ -34,6 +49,12 @@ class ToolRunnerWindow(QObject):
self._open_terminal = require_child(self.window, "openTerminalButton", QPushButton)
self._host_field = require_child(self.window, "hostField", QLineEdit)
self._extra_args = require_child(self.window, "extraArgsField", QLineEdit)
self._description = require_child(self.window, "descriptionLabel", QLabel)
self._use_existing_dump = require_child(self.window, "useExistingDumpCheck", QCheckBox)
self._dump_path = require_child(self.window, "dumpPathField", QLineEdit)
self._browse_dump = require_child(self.window, "browseDumpButton", QPushButton)
self._progress = require_child(self.window, "progressBar", QProgressBar)
self._current_step = require_child(self.window, "currentStepLabel", QLabel)
self._log = require_child(self.window, "logEdit", QPlainTextEdit)
self._status = require_child(self.window, "statusLabel", QLabel)
self._clear = require_child(self.window, "clearLogButton", QPushButton)
@@ -42,6 +63,40 @@ class ToolRunnerWindow(QObject):
self._host_field.setVisible(self._is_updater)
# Installer-specific UX
self._use_existing_dump.setVisible(self._is_installer)
self._dump_path.setVisible(self._is_installer)
self._browse_dump.setVisible(self._is_installer)
self._progress.setVisible(self._is_installer)
self._current_step.setVisible(self._is_installer)
if self._is_installer:
self._description.setText(
"This installer will: build Shofel, dump eMMC (or use an existing dump), "
"analyze partitions, modify /var/jibo/mode.json to enable developer mode, "
"write the modified /var partition back, and optionally verify.\n"
"Warning: Do not disconnect the robot during reads/writes."
)
elif self._is_updater:
self._description.setText(
"Updater will: download/extract a JiboOs release and upload the build/ overlay to the robot over SSH."
)
else:
self._description.setText("Run the selected tool with optional arguments.")
self._dump_path.setEnabled(False)
self._browse_dump.setEnabled(False)
self._use_existing_dump.toggled.connect(self._sync_dump_widgets)
self._browse_dump.clicked.connect(self._pick_dump_path)
self._sync_dump_widgets()
self._progress.setRange(0, 6)
self._progress.setValue(0)
self._progress.setTextVisible(True)
self._progress.setFormat("Step %v/%m")
self._current_step.setText("Idle")
self._start_stop.clicked.connect(self._toggle)
self._open_terminal.clicked.connect(self._open_in_terminal)
self._clear.clicked.connect(lambda: self._log.setPlainText(""))
@@ -68,8 +123,16 @@ class ToolRunnerWindow(QObject):
args += ["--ip", host]
extra = self._extra_args.text().strip()
if extra:
args += shlex.split(extra)
extra_args: list[str] = shlex.split(extra) if extra else []
# Installer convenience: if the user has an existing dump, pass it via --dump-path
if self._is_installer and self._use_existing_dump.isChecked():
dump_path = self._dump_path.text().strip()
if dump_path and "--dump-path" not in extra_args:
extra_args += ["--dump-path", dump_path]
if extra_args:
args += extra_args
return args
@@ -78,6 +141,23 @@ class ToolRunnerWindow(QObject):
if self.runner.running:
self.runner.stop()
else:
if self._is_installer and self._use_existing_dump.isChecked() and not self._dump_path.text().strip():
self._status.setText("Pick a dump file (or uncheck 'existing dump')")
return
if self._is_installer and self._use_existing_dump.isChecked():
p = Path(self._dump_path.text().strip())
if not p.exists():
self._status.setText("Dump file not found")
return
# Reset progress state for a new run.
self._output_buffer = ""
self._last_step_total = None
if self._is_installer:
self._progress.setRange(0, 0)
self._progress.setValue(0)
self._current_step.setText("Starting…")
program, prefix = resolve_python_invocation()
self.runner.start(program, [*prefix, *self._build_args()])
@@ -93,14 +173,30 @@ class ToolRunnerWindow(QObject):
self._log.insertPlainText(chunk)
self._log.moveCursor(QTextCursor.End)
if self._is_installer:
self._ingest_for_progress(chunk)
def _sync_buttons(self) -> None:
running = self.runner.running
self._start_stop.setText("Stop" if running else "Start")
self._open_terminal.setEnabled(not running)
if not running and self._is_installer:
# Leave progress/status in a meaningful final state.
if self.runner.exitCode == 0 and self._last_step_total:
self._progress.setRange(0, self._last_step_total)
self._progress.setValue(self._last_step_total)
if self._current_step.text().strip() in ("", "Starting…"):
self._current_step.setText("Finished")
elif self.runner.exitCode > 0:
if self._current_step.text().strip() in ("", "Starting…"):
self._current_step.setText("Exited with errors")
def _sync_status(self) -> None:
if self.runner.running:
self._status.setText("Running...")
# Indeterminate until we see a structured step marker.
if self._is_installer and self._last_step_total is None:
self._progress.setRange(0, 0)
return
code = self.runner.exitCode
if code >= 0:
@@ -115,6 +211,85 @@ class ToolRunnerWindow(QObject):
pass
event.accept()
@Slot(bool)
def _sync_dump_widgets(self, checked: bool | None = None) -> None:
enabled = bool(checked) if checked is not None else self._use_existing_dump.isChecked()
self._dump_path.setEnabled(enabled)
self._browse_dump.setEnabled(enabled)
@Slot()
def _pick_dump_path(self) -> None:
start_dir = str(Path.home())
path, _ = QFileDialog.getOpenFileName(
self.window,
"Select full eMMC dump (.bin)",
start_dir,
"Binary images (*.bin *.img *.raw);;All files (*)",
)
if path:
self._dump_path.setText(path)
def _ingest_for_progress(self, chunk: str) -> None:
self._output_buffer += chunk
lines = self._output_buffer.splitlines(keepends=True)
# Keep any partial line for the next chunk.
if lines and not (lines[-1].endswith("\n") or lines[-1].endswith("\r")):
self._output_buffer = lines[-1]
lines = lines[:-1]
else:
self._output_buffer = ""
for line in lines:
clean = _strip_ansi(line).strip()
if not clean:
continue
# Also surface meaningful non-step status lines (RCM detection, warnings, etc.)
if clean.startswith(("", "", "", "")) or "RCM" in clean:
msg = _clean_status_line(clean)
if msg and not msg.startswith("["):
self._current_step.setText(msg)
m = re.search(r"\[(\d+)\s*/\s*(\d+)\]\s*(.+)$", clean)
if not m:
continue
step = int(m.group(1))
total = int(m.group(2))
msg = m.group(3).strip()
# Some flows use [0/6] for dependency checks.
if total > 0:
self._last_step_total = total
self._progress.setRange(0, total)
self._progress.setValue(max(0, min(step, total)))
self._progress.setFormat("Step %v/%m")
if msg:
self._current_step.setText(msg)
def _on_close(self, event: QCloseEvent) -> None:
try:
self.runner.stop()
except Exception:
pass
event.accept()
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
def _strip_ansi(s: str) -> str:
return _ANSI_RE.sub("", s)
def _clean_status_line(s: str) -> str:
# Drop leading glyphs used by the CLI (info/warn/success/error)
s = re.sub(r"^[✓⚠✗ℹ]\s+", "", s).strip()
# Collapse extra whitespace
s = re.sub(r"\s+", " ", s).strip()
return s
def run_tool_window(*, title: str, script: str) -> int:
app = QApplication.instance() or QApplication([])

View File

@@ -15,9 +15,10 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QLineEdit,
QMainWindow, QPlainTextEdit, QPushButton, QSizePolicy,
QSpacerItem, QStatusBar, QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QApplication, QCheckBox, QHBoxLayout, QLabel,
QLineEdit, QMainWindow, QPlainTextEdit, QProgressBar,
QPushButton, QSizePolicy, QSpacerItem, QStatusBar,
QVBoxLayout, QWidget)
class Ui_ToolRunnerWindow(object):
def setupUi(self, ToolRunnerWindow):
@@ -75,6 +76,45 @@ class Ui_ToolRunnerWindow(object):
self.rootLayout.addLayout(self.argsLayout)
self.descriptionLabel = QLabel(self.centralwidget)
self.descriptionLabel.setObjectName(u"descriptionLabel")
self.descriptionLabel.setWordWrap(True)
self.rootLayout.addWidget(self.descriptionLabel)
self.dumpLayout = QHBoxLayout()
self.dumpLayout.setSpacing(10)
self.dumpLayout.setObjectName(u"dumpLayout")
self.useExistingDumpCheck = QCheckBox(self.centralwidget)
self.useExistingDumpCheck.setObjectName(u"useExistingDumpCheck")
self.dumpLayout.addWidget(self.useExistingDumpCheck)
self.dumpPathField = QLineEdit(self.centralwidget)
self.dumpPathField.setObjectName(u"dumpPathField")
self.dumpLayout.addWidget(self.dumpPathField)
self.browseDumpButton = QPushButton(self.centralwidget)
self.browseDumpButton.setObjectName(u"browseDumpButton")
self.dumpLayout.addWidget(self.browseDumpButton)
self.rootLayout.addLayout(self.dumpLayout)
self.progressBar = QProgressBar(self.centralwidget)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setValue(0)
self.rootLayout.addWidget(self.progressBar)
self.currentStepLabel = QLabel(self.centralwidget)
self.currentStepLabel.setObjectName(u"currentStepLabel")
self.currentStepLabel.setWordWrap(True)
self.rootLayout.addWidget(self.currentStepLabel)
self.logEdit = QPlainTextEdit(self.centralwidget)
self.logEdit.setObjectName(u"logEdit")
self.logEdit.setReadOnly(True)
@@ -118,6 +158,11 @@ class Ui_ToolRunnerWindow(object):
self.openTerminalButton.setText(QCoreApplication.translate("ToolRunnerWindow", u"Open in terminal", None))
self.hostField.setPlaceholderText(QCoreApplication.translate("ToolRunnerWindow", u"Jibo IP (required for updater)", None))
self.extraArgsField.setPlaceholderText(QCoreApplication.translate("ToolRunnerWindow", u"Extra arguments (optional)", None))
self.descriptionLabel.setText(QCoreApplication.translate("ToolRunnerWindow", u"Tool description", None))
self.useExistingDumpCheck.setText(QCoreApplication.translate("ToolRunnerWindow", u"I already have a full eMMC dump (.bin)", None))
self.dumpPathField.setPlaceholderText(QCoreApplication.translate("ToolRunnerWindow", u"Dump path (passed as --dump-path)", None))
self.browseDumpButton.setText(QCoreApplication.translate("ToolRunnerWindow", u"Browse\u2026", None))
self.currentStepLabel.setText(QCoreApplication.translate("ToolRunnerWindow", u"Idle", None))
self.statusLabel.setText(QCoreApplication.translate("ToolRunnerWindow", u"Idle", None))
self.clearLogButton.setText(QCoreApplication.translate("ToolRunnerWindow", u"Clear log", None))
# retranslateUi

View File

@@ -17,9 +17,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFormLayout,
QFrame, QGroupBox, QHBoxLayout, QLabel,
QLineEdit, QMainWindow, QPushButton, QScrollArea,
QSizePolicy, QSpacerItem, QStatusBar, QTabWidget,
QVBoxLayout, QWidget)
QLineEdit, QMainWindow, QPlainTextEdit, QPushButton,
QScrollArea, QSizePolicy, QSpacerItem, QSpinBox,
QStatusBar, QTabWidget, QVBoxLayout, QWidget)
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
@@ -98,6 +98,24 @@ class Ui_MainWindow(object):
self.configScrollLayout.addWidget(self.groupPreview)
self.groupToolSettings = QGroupBox(self.configScrollContents)
self.groupToolSettings.setObjectName(u"groupToolSettings")
self.formToolSettings = QFormLayout(self.groupToolSettings)
self.formToolSettings.setObjectName(u"formToolSettings")
self.labelEnableLogging = QLabel(self.groupToolSettings)
self.labelEnableLogging.setObjectName(u"labelEnableLogging")
self.formToolSettings.setWidget(0, QFormLayout.ItemRole.LabelRole, self.labelEnableLogging)
self.enableLoggingCheck = QCheckBox(self.groupToolSettings)
self.enableLoggingCheck.setObjectName(u"enableLoggingCheck")
self.enableLoggingCheck.setChecked(False)
self.formToolSettings.setWidget(0, QFormLayout.ItemRole.FieldRole, self.enableLoggingCheck)
self.configScrollLayout.addWidget(self.groupToolSettings)
self.groupHomeAssistant = QGroupBox(self.configScrollContents)
self.groupHomeAssistant.setObjectName(u"groupHomeAssistant")
self.formHomeAssistant = QFormLayout(self.groupHomeAssistant)
@@ -166,23 +184,171 @@ class Ui_MainWindow(object):
self.aiKeyField = QLineEdit(self.groupAiProvider)
self.aiKeyField.setObjectName(u"aiKeyField")
self.aiKeyField.setEchoMode(QLineEdit.EchoMode.Password)
self.formAiProvider.setWidget(3, QFormLayout.ItemRole.FieldRole, self.aiKeyField)
self.labelTokens = QLabel(self.groupAiProvider)
self.labelTokens.setObjectName(u"labelTokens")
self.labelAiBridgeRecordSeconds = QLabel(self.groupAiProvider)
self.labelAiBridgeRecordSeconds.setObjectName(u"labelAiBridgeRecordSeconds")
self.formAiProvider.setWidget(4, QFormLayout.ItemRole.LabelRole, self.labelTokens)
self.formAiProvider.setWidget(4, QFormLayout.ItemRole.LabelRole, self.labelAiBridgeRecordSeconds)
self.tokensUsedLabel = QLabel(self.groupAiProvider)
self.tokensUsedLabel.setObjectName(u"tokensUsedLabel")
self.aiBridgeRecordSecondsSpin = QSpinBox(self.groupAiProvider)
self.aiBridgeRecordSecondsSpin.setObjectName(u"aiBridgeRecordSecondsSpin")
self.aiBridgeRecordSecondsSpin.setMinimum(1)
self.aiBridgeRecordSecondsSpin.setMaximum(60)
self.aiBridgeRecordSecondsSpin.setValue(5)
self.formAiProvider.setWidget(4, QFormLayout.ItemRole.FieldRole, self.tokensUsedLabel)
self.formAiProvider.setWidget(4, QFormLayout.ItemRole.FieldRole, self.aiBridgeRecordSecondsSpin)
self.labelAiBridgeUseAsr = QLabel(self.groupAiProvider)
self.labelAiBridgeUseAsr.setObjectName(u"labelAiBridgeUseAsr")
self.formAiProvider.setWidget(5, QFormLayout.ItemRole.LabelRole, self.labelAiBridgeUseAsr)
self.aiBridgeUseAsrServiceSttCheck = QCheckBox(self.groupAiProvider)
self.aiBridgeUseAsrServiceSttCheck.setObjectName(u"aiBridgeUseAsrServiceSttCheck")
self.aiBridgeUseAsrServiceSttCheck.setChecked(True)
self.formAiProvider.setWidget(5, QFormLayout.ItemRole.FieldRole, self.aiBridgeUseAsrServiceSttCheck)
self.labelAiBridgeAsrPort = QLabel(self.groupAiProvider)
self.labelAiBridgeAsrPort.setObjectName(u"labelAiBridgeAsrPort")
self.formAiProvider.setWidget(6, QFormLayout.ItemRole.LabelRole, self.labelAiBridgeAsrPort)
self.aiBridgeAsrPortSpin = QSpinBox(self.groupAiProvider)
self.aiBridgeAsrPortSpin.setObjectName(u"aiBridgeAsrPortSpin")
self.aiBridgeAsrPortSpin.setMinimum(1)
self.aiBridgeAsrPortSpin.setMaximum(65535)
self.aiBridgeAsrPortSpin.setValue(8088)
self.formAiProvider.setWidget(6, QFormLayout.ItemRole.FieldRole, self.aiBridgeAsrPortSpin)
self.labelAiBridgeAsrAudioSource = QLabel(self.groupAiProvider)
self.labelAiBridgeAsrAudioSource.setObjectName(u"labelAiBridgeAsrAudioSource")
self.formAiProvider.setWidget(7, QFormLayout.ItemRole.LabelRole, self.labelAiBridgeAsrAudioSource)
self.aiBridgeAsrAudioSourceField = QLineEdit(self.groupAiProvider)
self.aiBridgeAsrAudioSourceField.setObjectName(u"aiBridgeAsrAudioSourceField")
self.formAiProvider.setWidget(7, QFormLayout.ItemRole.FieldRole, self.aiBridgeAsrAudioSourceField)
self.labelAiBridgeAsrTimeout = QLabel(self.groupAiProvider)
self.labelAiBridgeAsrTimeout.setObjectName(u"labelAiBridgeAsrTimeout")
self.formAiProvider.setWidget(8, QFormLayout.ItemRole.LabelRole, self.labelAiBridgeAsrTimeout)
self.aiBridgeAsrTimeoutSpin = QSpinBox(self.groupAiProvider)
self.aiBridgeAsrTimeoutSpin.setObjectName(u"aiBridgeAsrTimeoutSpin")
self.aiBridgeAsrTimeoutSpin.setMinimum(100)
self.aiBridgeAsrTimeoutSpin.setMaximum(120000)
self.aiBridgeAsrTimeoutSpin.setSingleStep(100)
self.aiBridgeAsrTimeoutSpin.setValue(15000)
self.formAiProvider.setWidget(8, QFormLayout.ItemRole.FieldRole, self.aiBridgeAsrTimeoutSpin)
self.labelAiBridgeAsrAutoStart = QLabel(self.groupAiProvider)
self.labelAiBridgeAsrAutoStart.setObjectName(u"labelAiBridgeAsrAutoStart")
self.formAiProvider.setWidget(9, QFormLayout.ItemRole.LabelRole, self.labelAiBridgeAsrAutoStart)
self.aiBridgeAsrAutoStartCheck = QCheckBox(self.groupAiProvider)
self.aiBridgeAsrAutoStartCheck.setObjectName(u"aiBridgeAsrAutoStartCheck")
self.aiBridgeAsrAutoStartCheck.setChecked(True)
self.formAiProvider.setWidget(9, QFormLayout.ItemRole.FieldRole, self.aiBridgeAsrAutoStartCheck)
self.labelAiBridgeFollowupEnabled = QLabel(self.groupAiProvider)
self.labelAiBridgeFollowupEnabled.setObjectName(u"labelAiBridgeFollowupEnabled")
self.formAiProvider.setWidget(10, QFormLayout.ItemRole.LabelRole, self.labelAiBridgeFollowupEnabled)
self.aiBridgeFollowupEnabledCheck = QCheckBox(self.groupAiProvider)
self.aiBridgeFollowupEnabledCheck.setObjectName(u"aiBridgeFollowupEnabledCheck")
self.aiBridgeFollowupEnabledCheck.setChecked(True)
self.formAiProvider.setWidget(10, QFormLayout.ItemRole.FieldRole, self.aiBridgeFollowupEnabledCheck)
self.labelAiBridgeFollowupDelay = QLabel(self.groupAiProvider)
self.labelAiBridgeFollowupDelay.setObjectName(u"labelAiBridgeFollowupDelay")
self.formAiProvider.setWidget(11, QFormLayout.ItemRole.LabelRole, self.labelAiBridgeFollowupDelay)
self.aiBridgeFollowupDelaySpin = QSpinBox(self.groupAiProvider)
self.aiBridgeFollowupDelaySpin.setObjectName(u"aiBridgeFollowupDelaySpin")
self.aiBridgeFollowupDelaySpin.setMinimum(0)
self.aiBridgeFollowupDelaySpin.setMaximum(60000)
self.aiBridgeFollowupDelaySpin.setSingleStep(50)
self.aiBridgeFollowupDelaySpin.setValue(250)
self.formAiProvider.setWidget(11, QFormLayout.ItemRole.FieldRole, self.aiBridgeFollowupDelaySpin)
self.labelAiBridge = QLabel(self.groupAiProvider)
self.labelAiBridge.setObjectName(u"labelAiBridge")
self.formAiProvider.setWidget(12, QFormLayout.ItemRole.LabelRole, self.labelAiBridge)
self.editAiBridgeConfigButton = QPushButton(self.groupAiProvider)
self.editAiBridgeConfigButton.setObjectName(u"editAiBridgeConfigButton")
self.formAiProvider.setWidget(12, QFormLayout.ItemRole.FieldRole, self.editAiBridgeConfigButton)
self.configScrollLayout.addWidget(self.groupAiProvider)
self.groupConfigFiles = QGroupBox(self.configScrollContents)
self.groupConfigFiles.setObjectName(u"groupConfigFiles")
self.configFilesLayout = QVBoxLayout(self.groupConfigFiles)
self.configFilesLayout.setSpacing(8)
self.configFilesLayout.setObjectName(u"configFilesLayout")
self.configFilesTopRow = QHBoxLayout()
self.configFilesTopRow.setSpacing(8)
self.configFilesTopRow.setObjectName(u"configFilesTopRow")
self.configFileCombo = QComboBox(self.groupConfigFiles)
self.configFileCombo.setObjectName(u"configFileCombo")
self.configFilesTopRow.addWidget(self.configFileCombo)
self.configReadButton = QPushButton(self.groupConfigFiles)
self.configReadButton.setObjectName(u"configReadButton")
self.configFilesTopRow.addWidget(self.configReadButton)
self.configWriteButton = QPushButton(self.groupConfigFiles)
self.configWriteButton.setObjectName(u"configWriteButton")
self.configWriteButton.setEnabled(False)
self.configFilesTopRow.addWidget(self.configWriteButton)
self.configFilesLayout.addLayout(self.configFilesTopRow)
self.configFileStatusLabel = QLabel(self.groupConfigFiles)
self.configFileStatusLabel.setObjectName(u"configFileStatusLabel")
self.configFileStatusLabel.setWordWrap(True)
self.configFilesLayout.addWidget(self.configFileStatusLabel)
self.configEditor = QPlainTextEdit(self.groupConfigFiles)
self.configEditor.setObjectName(u"configEditor")
self.configFilesLayout.addWidget(self.configEditor)
self.configLogLabel = QLabel(self.groupConfigFiles)
self.configLogLabel.setObjectName(u"configLogLabel")
self.configFilesLayout.addWidget(self.configLogLabel)
self.configActivityLog = QPlainTextEdit(self.groupConfigFiles)
self.configActivityLog.setObjectName(u"configActivityLog")
self.configActivityLog.setReadOnly(True)
self.configFilesLayout.addWidget(self.configActivityLog)
self.configScrollLayout.addWidget(self.groupConfigFiles)
self.configBottomSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.configScrollLayout.addItem(self.configBottomSpacer)
@@ -401,20 +567,41 @@ class Ui_MainWindow(object):
self.overrideCheck.setText("")
self.labelPreviewConnected.setText(QCoreApplication.translate("MainWindow", u"Connected", None))
self.previewConnectedCheck.setText("")
self.groupToolSettings.setTitle(QCoreApplication.translate("MainWindow", u"Tool Settings", None))
self.labelEnableLogging.setText(QCoreApplication.translate("MainWindow", u"Enable logging", None))
self.enableLoggingCheck.setText("")
self.groupHomeAssistant.setTitle(QCoreApplication.translate("MainWindow", u"Home Assistant", None))
self.labelHaEnable.setText(QCoreApplication.translate("MainWindow", u"Enabled", None))
self.haEnableCheck.setText("")
self.labelHaServerIp.setText(QCoreApplication.translate("MainWindow", u"Server IP", None))
self.haServerIpField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Home Assistant host", None))
self.groupAiProvider.setTitle(QCoreApplication.translate("MainWindow", u"AI Provider", None))
self.groupAiProvider.setTitle(QCoreApplication.translate("MainWindow", u"AI Bridge", None))
self.labelAiEnable.setText(QCoreApplication.translate("MainWindow", u"Enabled", None))
self.aiEnableCheck.setText("")
self.labelAiProvider.setText(QCoreApplication.translate("MainWindow", u"Provider", None))
self.labelAiEndpoint.setText(QCoreApplication.translate("MainWindow", u"API endpoint", None))
self.labelAiProvider.setText(QCoreApplication.translate("MainWindow", u"Mode", None))
self.labelAiEndpoint.setText(QCoreApplication.translate("MainWindow", u"Server base URL", None))
self.aiEndpointField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"http://...", None))
self.labelAiKey.setText(QCoreApplication.translate("MainWindow", u"API key", None))
self.labelTokens.setText(QCoreApplication.translate("MainWindow", u"Tokens used", None))
self.tokensUsedLabel.setText(QCoreApplication.translate("MainWindow", u"-1", None))
self.labelAiKey.setText(QCoreApplication.translate("MainWindow", u"ASR host", None))
self.aiKeyField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"127.0.0.1", None))
self.labelAiBridgeRecordSeconds.setText(QCoreApplication.translate("MainWindow", u"Record seconds", None))
self.labelAiBridgeUseAsr.setText(QCoreApplication.translate("MainWindow", u"Use ASR service STT", None))
self.aiBridgeUseAsrServiceSttCheck.setText("")
self.labelAiBridgeAsrPort.setText(QCoreApplication.translate("MainWindow", u"ASR port", None))
self.labelAiBridgeAsrAudioSource.setText(QCoreApplication.translate("MainWindow", u"ASR audio source", None))
self.aiBridgeAsrAudioSourceField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"alsa1", None))
self.labelAiBridgeAsrTimeout.setText(QCoreApplication.translate("MainWindow", u"ASR timeout (ms)", None))
self.labelAiBridgeAsrAutoStart.setText(QCoreApplication.translate("MainWindow", u"ASR auto start", None))
self.aiBridgeAsrAutoStartCheck.setText("")
self.labelAiBridgeFollowupEnabled.setText(QCoreApplication.translate("MainWindow", u"Followup enabled", None))
self.aiBridgeFollowupEnabledCheck.setText("")
self.labelAiBridgeFollowupDelay.setText(QCoreApplication.translate("MainWindow", u"Followup delay (ms)", None))
self.labelAiBridge.setText(QCoreApplication.translate("MainWindow", u"Advanced", None))
self.editAiBridgeConfigButton.setText(QCoreApplication.translate("MainWindow", u"Edit AI Bridge Config", None))
self.groupConfigFiles.setTitle(QCoreApplication.translate("MainWindow", u"Config Files", None))
self.configReadButton.setText(QCoreApplication.translate("MainWindow", u"Read", None))
self.configWriteButton.setText(QCoreApplication.translate("MainWindow", u"Write", None))
self.configFileStatusLabel.setText(QCoreApplication.translate("MainWindow", u"Select a config to view/edit", None))
self.configLogLabel.setText(QCoreApplication.translate("MainWindow", u"Activity log", None))
self.TryToConnect.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
self.JiboIpField.setInputMask("")
self.JiboIpField.setText("")

128
README.md
View File

@@ -285,6 +285,130 @@ JiboAutoMod/
- devsparx for the T124 port
- The Jibo preservation community
## License
CLI Arguments
This page documents the CLI flags for the two main tools:
This tool is provided as-is for educational and preservation purposes. See individual component licenses in the Shofel directory.
../jibo_automod.py (installer/mod tool)
../jibo_updater.py (OS updater)
If youre using launchers (.sh/.bat), they forward all arguments through.
jibo_automod.py
Modes (mutually exclusive)
(no mode flag)
Full mod workflow (full eMMC dump, extract/modify/write /var).
--dump-only
Only dump eMMC; dont modify anything.
--write-partition FILE
Write an already-prepared partition image to eMMC.
You must tell it where to write using --start-sector.
--mode-json-only
Fast path:
read GPT (small)
read /var only (~500MB)
modify /var/jibo/mode.json
write back either a patch (default) or full /var
Common options
--dump-path FILE
Use an existing dump file instead of reading from the device.
--output FILE / -o FILE
Output file for dumps.
--start-sector HEX
Start sector for --write-partition.
Parsed with base autodetect (0x... works).
Default is 0x7E9022 (but the full/fast workflows usually compute the start sector from GPT).
--force-dump
Re-dump even if a dump exists.
--rebuild-shofel
Force rebuilding ShofEL (make clean then make).
--skip-detection
Skip USB “is Jibo connected” checks.
--no-verify
Skip verification read-back.
Fast mode options
--full-var-write
Only meaningful with --mode-json-only.
If set: write the full /var partition back (slower).
If not set: compute sector diffs and only write changed ranges (faster).
Examples
Full mod:
python3 jibo_automod.py
Dump only:
python3 jibo_automod.py --dump-only -o my_dump.bin
Use existing dump:
python3 jibo_automod.py --dump-path jibo_work/jibo_full_dump.bin
Fast mode:
python3 jibo_automod.py --mode-json-only
Write a prepared partition:
python3 jibo_automod.py --write-partition var_partition.bin --start-sector 0x7E9022
jibo_updater.py
The updater assumes you already have SSH access to the robot.
Required
--ip <host> (alias: --host)
IP or hostname of your Jibo.
Connection
--user <name> (default root)
--password <pass> (default jibo)
--ssh-timeout <seconds> (default 15)
Release selection
--releases-api <url>
API endpoint for Gitea releases.
--stable
Ignore prereleases.
--tag <tag>
Install a specific tag instead of “latest”.
Archive layout
--build-path <relative/path>
If the updater cant find the build/ folder automatically, specify where it is inside the extracted archive.
Safety / UX
--state-file <path>
Where it remembers the last applied version per host.
--force
Re-download and re-install even if the local state says youre already on that version.
--yes
Dont prompt for confirmation.
--dry-run
Download/extract + connect, but dont upload files and dont touch mode.json.
Returning to normal mode
--return-normal
After update, set /var/jibo/mode.json back to normal (no prompt).
--no-return-normal
Never prompt and never change mode back.
Examples
Update to latest:
python3 jibo_updater.py --ip 192.168.1.50
Stable only:
python3 jibo_updater.py --ip 192.168.1.50 --stable
Specific tag:
python3 jibo_updater.py --ip 192.168.1.50 --tag v3.3.0
Dry run:
python3 jibo_updater.py --ip 192.168.1.50 --dry-run

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
{
"192.168.1.15": "v3.3.0"
}