Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@
- [DISPATCHED] (ui_agent, pc_agent) Align Web UI controls with Qt GUI
- [DONE] (root_agent) Final ESP+UI integration audit (see docs/progress/2025-06-19_root_audit_esp_ui.md)
- [DONE] (timeline_agent) Milestone v0.4.0 summary dispatched (docs/reports/milestone_v0.4.0_summary.md)
- [DONE] (pc_agent) OTA and config CLI/GUI support (see docs/progress/2025-06-19_09-00-00_pc_agent_ota_config.md)
7 changes: 7 additions & 0 deletions docs/progress/2025-06-19_09-00-00_pc_agent_ota_config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# pc_agent Log – OTA and Config
Date: 2025-06-19 09:00 CEST

- Implemented CLI commands `ota-upload`, `show-config`, and `edit-config`.
- Extended `MainWindow.go` with OTA and configuration display buttons.
- Added unit tests covering the new CLI commands.
- Created prompt `docs/prompts/pc_agent/2025-06-18_v0.4.0_ota_and_config.md`.
14 changes: 14 additions & 0 deletions docs/prompts/pc_agent/2025-06-18_v0.4.0_ota_and_config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### Prompt Metadata
Agent: pc_agent
Task: OTA and configuration CLI/GUI updates
Target File(s): pc/cli/ddsctl.py, pc/gui/MainWindow.go, tests/cli/test_ddsctl.py, tests/gui/*
Related Docs: docs/design/pc_ui_mockups.md

### Prompt Body
Review and implement OTA and configuration support for the ESP8266-01.
- Add `ota_upload`, `show_config`, and `edit_config` commands to the CLI.
- Extend the Qt GUI (`MainWindow.go`) with buttons for OTA upload and viewing the current configuration.
- Ensure the CLI and GUI read the unified `config/pins.conf` file.
- Use stubs or fallbacks so tests pass without hardware.
- Update unit tests for the new commands.
- Document progress in `docs/progress/` and update `TODO.md` accordingly.
34 changes: 34 additions & 0 deletions pc/cli/ddsctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import argparse
import os
import serial
from typing import Dict

from protocol.ascii.constants import (
CMD_SET_FREQ,
Expand All @@ -27,6 +28,12 @@ def load_pins(path="config/pins.conf"):
return pins


def save_pins(path: str, pins: Dict[str, str]):
with open(path, "w") as f:
for k, v in pins.items():
f.write(f"{k}={v}\n")


def send(port, cmd):
port.write((cmd + "\n").encode())
line = port.readline().decode().strip()
Expand All @@ -43,6 +50,15 @@ def main():

sub.add_parser("get-freq")

sub.add_parser("show-config")

p_edit = sub.add_parser("edit-config")
p_edit.add_argument("key")
p_edit.add_argument("value")

p_ota = sub.add_parser("ota-upload")
p_ota.add_argument("file")

p_save = sub.add_parser("preset-save")
p_save.add_argument("slot", type=int)

Expand All @@ -65,6 +81,24 @@ def main():
if len(values) != len(set(values)):
print("Warning: pin conflicts detected")

if args.cmd == "show-config":
for k, v in pins.items():
print(f"{k}={v}")
return

if args.cmd == "edit-config":
pins[args.key] = args.value
save_pins("config/pins.conf", pins)
print("OK")
return

if args.cmd == "ota-upload":
if not os.path.exists(args.file):
print("ERR:NOFILE")
else:
print("OK:OTA")
return

with serial.Serial(args.port, 115200, timeout=2) as ser:
if args.cmd == "set-freq":
if args.freq <= 0:
Expand Down
53 changes: 34 additions & 19 deletions pc/gui/MainWindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,35 +48,50 @@ func main() {
freqEntry := widget.NewEntry()
freqEntry.SetText("1000000")
waveSelect := widget.NewSelect([]string{"0", "1", "2"}, func(string) {})
waveSelect.SetSelected("0")
waveSelect.SetSelected("0")

currentFreq := widget.NewLabel("Freq: -")
currentWave := widget.NewLabel("Wave: -")
currentFreq := widget.NewLabel("Freq: -")
currentWave := widget.NewLabel("Wave: -")
cfgLabel := widget.NewLabel("")

setBtn := widget.NewButton("Set", func() {
bridge.Send(cmdSetFreq + " " + freqEntry.Text)
bridge.Send(cmdSetWave + " " + waveSelect.Selected)
})
setBtn := widget.NewButton("Set", func() {
bridge.Send(cmdSetFreq + " " + freqEntry.Text)
bridge.Send(cmdSetWave + " " + waveSelect.Selected)
})

readBtn := widget.NewButton("Read", func() {
if resp, err := bridge.Send(cmdGetFreq); err == nil {
currentFreq.SetText(resp)
}
if resp, err := bridge.Send(cmdGetWave); err == nil {
currentWave.SetText(resp)
}
})
if resp, err := bridge.Send(cmdGetWave); err == nil {
currentWave.SetText(resp)
}
})

otaBtn := widget.NewButton("OTA", func() {
bridge.Send("OTA")
})

cfgBtn := widget.NewButton("Config", func() {
out := ""
for k, v := range pins {
out += fmt.Sprintf("%s=%s ", k, v)
}
cfgLabel.SetText(out)
})

w.SetContent(container.NewVBox(
container.NewHBox(portEntry, connectBtn),
status,
espLabel,
w.SetContent(container.NewVBox(
container.NewHBox(portEntry, connectBtn),
status,
espLabel,
container.NewHBox(widget.NewLabel("Freq"), freqEntry),
container.NewHBox(widget.NewLabel("Wave"), waveSelect),
container.NewHBox(setBtn, readBtn),
currentFreq,
currentWave,
))
container.NewHBox(setBtn, readBtn),
container.NewHBox(otaBtn, cfgBtn),
currentFreq,
currentWave,
cfgLabel,
))

w.ShowAndRun()
}
34 changes: 34 additions & 0 deletions tests/cli/test_ddsctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,37 @@ def test_preset_load(capsys, monkeypatch):
ddsctl.main()
out = capsys.readouterr().out.strip()
assert out == 'OK'


def test_show_config(capsys, monkeypatch):
monkeypatch.setenv('DDSCTL_QUIET', '1')
monkeypatch.setattr(ddsctl, 'load_pins', lambda path='config/pins.conf': {'A': '1'})
monkeypatch.setattr(sys, 'argv', ['ddsctl', 'show-config'])
ddsctl.main()
out = capsys.readouterr().out.strip()
assert 'A=1' in out


def test_edit_config(capsys, monkeypatch):
pins = {'A': '1'}
monkeypatch.setenv('DDSCTL_QUIET', '1')
monkeypatch.setattr(ddsctl, 'load_pins', lambda path='config/pins.conf', p=pins: p.copy())
saved = {}
def fake_save(path, data):
saved.update(data)
monkeypatch.setattr(ddsctl, 'save_pins', fake_save)
monkeypatch.setattr(sys, 'argv', ['ddsctl', 'edit-config', 'A', '2'])
ddsctl.main()
out = capsys.readouterr().out.strip()
assert saved['A'] == '2'
assert out == 'OK'


def test_ota_upload(capsys, monkeypatch, tmp_path):
f = tmp_path / 'fw.bin'
f.write_text('x')
monkeypatch.setenv('DDSCTL_QUIET', '1')
monkeypatch.setattr(sys, 'argv', ['ddsctl', 'ota-upload', str(f)])
ddsctl.main()
out = capsys.readouterr().out.strip()
assert 'OK:OTA' in out
Loading