GUI installer v2
This commit is contained in:
474
CONFIG_LOCATIONS.md
Normal file
474
CONFIG_LOCATIONS.md
Normal 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 you’re 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 robot’s 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 PC’s 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
3334
CONFIG_VALUES.md
Normal file
File diff suppressed because it is too large
Load Diff
474
JiboTools/CONFIG_LOCATIONS.md
Normal file
474
JiboTools/CONFIG_LOCATIONS.md
Normal 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 you’re 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 robot’s 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 PC’s 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
3334
JiboTools/CONFIG_VALUES.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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">
|
||||
@@ -196,7 +222,7 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupAiProvider">
|
||||
<property name="title">
|
||||
<string>AI Provider</string>
|
||||
<string>AI Bridge</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formAiProvider">
|
||||
<item row="0" column="0">
|
||||
@@ -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="labelTokens">
|
||||
<widget class="QLabel" name="labelAiBridgeRecordSeconds">
|
||||
<property name="text">
|
||||
<string>Tokens used</string>
|
||||
<string>Record seconds</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="tokensUsedLabel">
|
||||
<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>-1</string>
|
||||
<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>
|
||||
<widget class="QPlainTextEdit" name="configEditor"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="configLogLabel">
|
||||
<property name="text">
|
||||
<string>Activity log</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="configActivityLog">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
BIN
JiboTools/JiboTools/gui/Assets/Jibo/A
Normal file
BIN
JiboTools/JiboTools/gui/Assets/Jibo/A
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 3.0 MiB |
BIN
JiboTools/JiboTools/gui/Assets/Jibo/MissingJibo.PNG
Normal file
BIN
JiboTools/JiboTools/gui/Assets/Jibo/MissingJibo.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
@@ -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 |
131
JiboTools/JiboTools/gui/config_inventory.py
Normal file
131
JiboTools/JiboTools/gui/config_inventory.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
313
JiboTools/JiboTools/gui/robot_settings_window.py
Normal file
313
JiboTools/JiboTools/gui/robot_settings_window.py
Normal 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))
|
||||
@@ -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">
|
||||
|
||||
@@ -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([])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
128
README.md
@@ -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 you’re 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; don’t 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 can’t 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 you’re already on that version.
|
||||
--yes
|
||||
|
||||
Don’t prompt for confirmation.
|
||||
--dry-run
|
||||
|
||||
Download/extract + connect, but don’t upload files and don’t 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
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"192.168.1.15": "v3.3.0"
|
||||
}
|
||||
Reference in New Issue
Block a user