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
87 changes: 87 additions & 0 deletions k10-ph-titrator/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: CI

on:
push:
branches: [main, master, "codex/ph-titrator"]
pull_request:
branches: [main, master, "codex/ph-titrator"]

jobs:
build-firmware:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-platformio-${{ hashFiles('platformio.ini') }}
restore-keys: |
${{ runner.os }}-platformio-

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install PlatformIO
run: pip install platformio

- name: Build firmware
shell: bash
run: |
trap 'rm -f ph_titrator/factory_auth.generated.h factory-label-ci.txt' EXIT
python scripts/generate_factory_auth.py --device-id CI-THROWAWAY \
--header ph_titrator/factory_auth.generated.h \
--label factory-label-ci.txt --iterations 120000
pio run --environment unihiker

unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install compiler
run: sudo apt-get update && sudo apt-get install -y g++

- name: Build and run control logic tests
run: |
g++ -std=c++17 -Wall -Wextra -pedantic tests/ph_titrator_control_test.cpp -o control_test
./control_test
g++ -std=c++17 -Wall -Wextra -pedantic tests/auth_core_test.cpp ph_titrator/auth_core.cpp -o auth_core_test
./auth_core_test

- name: Run generator and static tests
shell: pwsh
run: |
python -m unittest tests.factory_auth_generator_test tests.ota_upload_script_test
./tests/ota_upload_script_test.ps1
./tests/web_auth_static_test.ps1
./tests/sketch_safety_static_test.ps1

run-engine-tests:
name: RunEngine (${{ matrix.compiler }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- compiler: g++
package: g++
- compiler: clang++
package: clang
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install compiler
run: sudo apt-get update && sudo apt-get install -y ${{ matrix.package }}

- name: Build and run RunEngine tests
run: |
${{ matrix.compiler }} -std=c++17 -Wall -Wextra -Werror -pedantic \
tests/run_engine_test.cpp ph_titrator/run_engine.cpp -o run_engine_test
./run_engine_test
26 changes: 26 additions & 0 deletions k10-ph-titrator/.github/workflows/sync-to-gitee.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Sync to Gitee

on:
push:
branches: [master, main, codex/ph-titrator]
workflow_dispatch:

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Push to Gitee
uses: wearerequired/git-mirror-action@master
env:
SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_KEY }}
with:
source-repo: "https://github.com/rockets-cn/k10-ph-titrator.git"
destination-repo: "git@gitee.com:rocketscn/k10-ph-titrator.git"
5 changes: 5 additions & 0 deletions k10-ph-titrator/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
**/build/
.arduino-build/
.arduino-output/
.pio/
.worktrees/
*.elf
*.bin
*.hex
ph_titrator/factory_auth.generated.h
ph_titrator/recovery_admin.generated.h
factory-label*.txt

# IDE
.vscode/
Expand Down
54 changes: 52 additions & 2 deletions k10-ph-titrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ K10 (3.3 V I2C) ADS1115 (0x49) Scale (0x64)

## Software Highlights

### Latest update report — 2026-07-14

- The monolithic titration sketch was split into testable run-engine, web-shell, page, escaping, and browser-script modules.
- HTTP write operations use `POST`, authenticated sessions, command/state permissions, rate limiting, and a USB administrator-recovery path.
- Pump timing overflow protection, watchdog/safety-stop paths, endpoint hold behavior, calibration validity checks, and EQP/curve-replay coverage were added.
- Browser-side run records support final-record persistence (IndexedDB), replay analysis, printable reports, and CSV/JSON export; records never write back to the device automatically.
- The web UI now has an English/Chinese selector. Chinese text is loaded through a separate `/i18n-zh.js` resource so the primary controller page stays within the K10's safe HTML-memory budget.
- The current firmware was compiled and installed through authenticated HTTP OTA. Static authentication/safety tests and browser curve/record tests passed before deployment.

Known follow-up: Chinese coverage is being completed for every explanatory paragraph and runtime-composed status message outside the Run tab. The controller remains usable while this wording work continues.

### Web Screenshots

| Run | Calibration | Manual |
Expand Down Expand Up @@ -65,6 +76,8 @@ A `TitrationDynamics` tracker watches `dpH/dt` and halts immediately if the curv
### Automatic Pump Calibration
From **SetupReady**, press **B** to enter calibration. The controller runs each pump for exactly 2 seconds, waits 5 seconds after each pump stops, measures the weight change on the scale, computes the flow rate (g/s), and saves it to ESP32 Preferences.

Pump speed is configurable per pump as a servo PWM pulse width. The default `1000us` preserves the original speed; values closer to `1500us` run slower. Recalibrate pump g/s after changing PWM speed so dosing estimates match the actual flow.

### Calibration Page
The web **Calibration** tab separates pump flow, scale, pH/mV sensor, and titrant standard settings. The pH/mV section displays two-point slope percentage, pH 7 offset, and calibration status. **Reset pH/mV filter** only restarts acquisition filtering; it does not overwrite the saved two-point calibration. Titrant molarity, blank, and result formula remain in the **Admin** tab.

Expand Down Expand Up @@ -119,6 +132,8 @@ arduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 ./ph_titrator
python scripts/ota_upload.py ph_titrator/build/ph_titrator.ino.bin --ip 192.168.9.42
```

HTTP OTA stops and locks both pumps before flash writing. A successful update restarts into SetupMode and never resumes the interrupted run. After a failed or aborted upload, use the Web Reset control; hardware A/B buttons are not required for recovery.

### 4. Connect

Join the `K10-pH-Titrator` WiFi, open the AP IP shown on the K10 screen (usually `http://192.168.4.1/`), or use the STA IP if configured.
Expand All @@ -145,12 +160,37 @@ Join the `K10-pH-Titrator` WiFi, open the AP IP shown on the K10 screen (usually
|----------|--------|-------------|
| `/` | GET | Main dashboard |
| `/json` | GET | Live status JSON |
| `/set` | GET | Save settings (`mode`, `target`, `max`, `sample`, `titrant`, `titrant_m`, `blank_g`, `titrant_density`, `sample_density`, `ssid`, `wifi_password`) |
| `/action?cmd=` | GET | `start`, `stop`, `panic`, `tare`, `reset` |
| `/set` | POST | Save settings (`mode`, `target`, `max`, `sample`, `titrant`, `titrant_m`, `blank_g`, `titrant_density`, `sample_density`, `ssid`, `wifi_password`) |
| `/action` | POST | Authenticated commands in the request body: `start`, `start_existing`, `stop`, `tare`, `reset` |
| `/panic` | POST | Anonymous emergency stop; immediately stops both pumps |
| `/ota` | POST | Firmware binary upload |

---

## RunEngine lifecycle boundary

`RunEngine` owns the active experiment lifecycle and its history: phase; filling/progress; run, pulse, and settle timers; dosing dynamics; endpoint hold; equivalence-point (EQP) tracking; predose progress; active mass/result selection; and emergency stop. It is a pure C++17 boundary with fixed memory budgets: `sizeof(RunEngine) <= 4096` and `sizeof(RunInput) + sizeof(RunOutput) <= 1024`.

`control_logic.h` provides pure calculations and reusable value types; it does not own live experiment state. The sketch owns hardware and sensors, Web/authentication, OTA, display, setup/calibration, persistence, and only translates inputs into `RunInput` and `RunOutput` intentions back to hardware/UI.

Every active-run entry point uses the same command mapping:

| Source | `RunCommand` |
|--------|--------------|
| Web/panel Start | `StartNormal` |
| Start existing sample | `StartExistingSample` |
| Stop/pause | `Pause` |
| Paused Start/resume | `Resume` |
| Reset | `Reset` |
| Main loop | `Tick` |
| AB-long and Web panic | `EmergencyStop` |

Resume always re-enters `FilterWarmup`; the display tells the operator `正在重新稳定信号` while the signal is re-stabilized. `requestedSettleMs` is display/diagnostic metadata only. It cannot advance lifecycle transitions; only engine time and input sensor facts can do that.

On-device deployment is deferred. The labelled-device Task 7 in [the Web/auth command-security plan](docs/superpowers/plans/2026-07-10-web-auth-command-security.md) must be completed onsite by the onsite operator before device installation or smoke verification. This phase does not claim deployment.

---

## Project Structure

```
Expand All @@ -171,6 +211,16 @@ scripts/

---

## Web authentication and provisioning

On first setup, sign in with the unique factory password printed on the device label and choose the administrator password. Keep the label private for Web-only password recovery; recovery stops both pumps, clears active sessions, and returns the instrument to `SetupMode`. Log out on shared computers. Sessions expire after 30 minutes without a successful authenticated write.

All control and settings integrations now use authenticated `POST` requests; legacy `GET` integrations are incompatible. OTA also requires a current session token: `python scripts/ota_upload.py firmware.bin --ip DEVICE_IP --token SESSION_TOKEN` or `.\scripts\ota_upload.ps1 -Bin firmware.bin -Ip DEVICE_IP -Token SESSION_TOKEN`. The helpers send the token in `X-Session-Token` and do not print it.

Manufacturing must run `generate_factory_auth.py` once per device, compile its generated header into that device only, attach the matching label, and delete both generated artifacts after the build. Never reuse or commit credentials or labels.

HTTP authentication remains plaintext on the local network and does not protect against a packet sniffer. Use the device AP or a trusted LAN.

## License

MIT — see repository for details.
14 changes: 14 additions & 0 deletions k10-ph-titrator/README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ K10 (3.3 V I2C) ADS1115 (0x49) 电子秤 (0x64)
### 蠕动泵自动校准
在 **SetupReady(就绪)** 状态按 **B 键**进入校准。控制器依次让滴定泵和样品泵各运行 2 秒,每个泵停泵后静置 5 秒再读取重量差,计算流量(g/s)并保存到 ESP32 Preferences,掉电不丢失。

每个泵的速度可以用舵机 PWM 脉宽单独设置。默认 `1000us` 保持原有速度;越接近 `1500us` 越慢。修改 PWM 速度后应重新校准泵流量,确保 g/s 估算匹配实际出液。

### 校准页面
网页 **Calibration** tab 将校准分为泵流量、电子秤、pH/mV 传感器和滴定液标准四类。pH/mV 区域显示两点校准斜率百分比、pH 7 偏移和状态;**Reset pH/mV filter** 只重启采样滤波,不会改写已保存的两点校准。滴定液浓度、空白量和结果公式仍在 **Admin** tab 设置。

Expand Down Expand Up @@ -117,6 +119,8 @@ arduino-cli upload -p COM4 --fqbn UNIHIKER:esp32:k10 ./ph_titrator
python scripts/ota_upload.py ph_titrator/build/ph_titrator.ino.bin --ip 192.168.9.42
```

HTTP OTA 在写入固件前会停止并锁定两路泵。更新成功后设备重启进入 SetupMode,不会恢复中断的实验;上传失败或中止后,请使用网页 Reset 复位,无需依赖 A/B 实体按键。

### 4. 连接

连接 `K10-pH-Titrator` WiFi,打开 K10 屏幕上显示的 AP IP(通常为 `http://192.168.4.1/`),或 STA IP(若已配置)。
Expand Down Expand Up @@ -169,6 +173,16 @@ scripts/

---

## 网页认证与生产配置

首次设置时,用设备标签上的唯一出厂密码登录并设置管理员密码。标签应保密,仅用于网页密码恢复;恢复会停止两台泵、清除所有会话并返回 `SetupMode`。共用电脑使用后请退出。若 30 分钟内没有成功的认证写操作,会话自动失效。

控制和设置接口现在仅接受带认证的 `POST`;旧版 `GET` 集成不兼容。OTA 同样需要当前会话令牌:`python scripts/ota_upload.py firmware.bin --ip DEVICE_IP --token SESSION_TOKEN` 或 `.\scripts\ota_upload.ps1 -Bin firmware.bin -Ip DEVICE_IP -Token SESSION_TOKEN`。脚本通过 `X-Session-Token` 请求头发送令牌且不会打印令牌。

生产时必须为每台设备单独运行 `generate_factory_auth.py`,只把对应生成头文件编译进该设备,贴上匹配标签,构建后删除两个生成文件。不得复用或提交凭据和标签。

HTTP 认证在本地网络中仍是明文,无法防御网络抓包。请使用设备自身 AP 或可信局域网。

## 许可证

MIT — 详见仓库。
21 changes: 21 additions & 0 deletions k10-ph-titrator/docs/MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
9. [OTA Firmware Updates](#9-ota-firmware-updates)
10. [Troubleshooting](#10-troubleshooting)
11. [Technical Details](#11-technical-details)
12. [Authentication and Provisioning](#12-authentication-and-provisioning)

---

Expand Down Expand Up @@ -288,11 +289,15 @@ Open the controller IP in a browser.
### Calibration Tab
- **Enter ready**: stops both pumps and enters the READY state for calibration.
- **Calibrate pumps**: measures both pump flow rates in the sequence "titrant pump 2 s -> wait 5 s -> sample pump 2 s -> wait 5 s".
- **Pump PWM us**: saved speed for each pump. `1000us` is the original fast setting; values closer to `1500us` slow the pump toward the stop midpoint. After changing this value, run pump calibration again.
- **Tare scale**: uses the current reactor weight as the scale baseline.
- **Reset pH/mV filter**: restarts pH/mV acquisition filtering after changing probe or buffer solution. It does not overwrite the saved two-point calibration.
- **pH/mV Sensor**: enter two buffer pH values with their probe mV and ADS input mV. The page displays slope percentage, pH 7 offset, and calibration status to help decide whether the probe needs recalibration.
- **Titrant Standard**: shows the current titrant and result formula. Titrant molarity, blank, and formula are configured in the **Admin** tab; a future standardization step can calculate titrant factor from a primary standard.

### Manual Tab
- Manual pump runs can use temporary P0/P1 PWM values for speed testing. These temporary values do not change the saved defaults; save PWM values in Calibration when the tested speed is suitable.

### Settings
- **Method**: pH endpoint, mV endpoint, EDTA hardness, or Manual method. Changing a preset immediately fills the related form values.
- **Endpoint**: pH or mV control signal.
Expand All @@ -301,6 +306,8 @@ Open the controller IP in a browser.
- **Control band**: slows dosing as the signal approaches the endpoint.
- **Stable delta/s**: slope threshold used to decide whether the signal has settled.
- **Hold s**: confirmation time after the endpoint is reached.

Endpoint hold uses fresh sensor readings. Entering the endpoint range stops dosing and starts the Hold timer. If any fresh reading leaves the range, the timer resets and dosing evaluation resumes. The run finishes only after the signal remains in range for the complete Hold period.
- **Min / Max settle s**: minimum and maximum wait after each dose.
- **Max time s**: maximum titration time protection.
- **Max used g**: safety limit.
Expand Down Expand Up @@ -333,6 +340,8 @@ python scripts/ota_upload.py ph_titrator/build/ph_titrator.ino.bin --ip 192.168.

The device restarts automatically when the upload completes.

HTTP OTA stops and locks both pumps before flash writing. A successful update restarts into SetupMode and never resumes the interrupted run. After a failed or aborted upload, use the Web Reset control; hardware A/B buttons are not required for recovery.

### Method B: Arduino OTA (IDE)

The controller advertises via mDNS as `k10-ph-titrator`. In the Arduino IDE:
Expand Down Expand Up @@ -406,6 +415,18 @@ Saved to Preferences namespace `cal`, keys `titrant_gps` and `sample_gps`.

The project uses a custom 16 MB partition table (`partitions.csv`) with dual OTA app partitions (~6.5 MB each) to support large firmware updates.

## 12. Authentication and provisioning

### Login and recovery

The first administrator signs in with the device-specific factory password on the private label and sets an administrator password. Logout invalidates the current session; otherwise it expires after 30 minutes without a successful authenticated write. Forgotten passwords can only be recovered in the Web interface using the factory label. Recovery stops both pumps, clears sessions and run data, and enters `SetupMode`.

Control, settings, recovery, logout, and OTA are authenticated POST-only operations, so legacy GET-based integrations must migrate. Supply the logged-in session token to OTA with `--token` (Python) or `-Token` (PowerShell); it is sent as `X-Session-Token` and must never be logged.

### Manufacturing and network security

For manufacturing, generate a unique header/label pair per serial number, compile that header into only its matching unit, apply and protect the label, then securely delete the generated files. HTTP remains local-network plaintext and cannot defeat packet sniffing; operate through the device AP or a trusted LAN.

---

*Document version: matches firmware `codex/ph-titrator` branch.*
Loading
Loading