For agentic workers: REQUIRED SUB-SKILL: Use
executing-plansto implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. 当前任务内联执行,不执行 Git 提交。
Goal: 将秤体改为应用进程级唯一连接,消除重复初始化、双串口读取线程、监听竞态和新页面无当前重量的问题。
Architecture: CptApplication 启动唯一 WeigherController,Controller 通过可替换的 WeigherDriver 串行访问厂商 SDK,并维护状态机、最新重量及恢复策略。Activity、Fragment、设置调试页和 Demo 页只订阅 Controller 或发送命令,不再拥有串口。
Tech Stack: Java 8、Android HandlerThread、JUnit 4、WbAiThingsSDK、MMKV。
Files:
Create: app/src/main/java/com/zhct/traybinding/hardware/weigher/WeigherReading.java
Create: app/src/main/java/com/zhct/traybinding/hardware/weigher/WeigherState.java
Create: app/src/main/java/com/zhct/traybinding/hardware/weigher/WeigherRecoveryPolicy.java
Test: app/src/test/java/com/zhct/traybinding/hardware/weigher/WeigherRecoveryPolicyTest.java
Step 1: 写恢复策略失败测试
覆盖以下断言:初始化 6 秒超时;ACTIVE 5 秒静默触发软探测;探测后 3 秒触发升级恢复;5 分钟内不允许第二次自动重启;停止状态永不恢复。
核心测试形式:
@Test
public void activeConnectionBecomesSilentAfterConfiguredTimeout() {
WeigherRecoveryPolicy policy = new WeigherRecoveryPolicy(5_000L, 3_000L, 300_000L);
policy.onWeightReceived(1_000L);
assertFalse(policy.shouldProbe(5_999L));
assertTrue(policy.shouldProbe(6_000L));
}
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) sh ./gradlew :app:testDebugUnitTest --tests 'com.zhct.traybinding.hardware.weigher.WeigherRecoveryPolicyTest'
预期:因目标类尚不存在而失败。
WeigherReading 使用构造器写入不可变字段,WeigherState 定义 IDLE/INITIALIZING/ACTIVE/DEGRADED/RETRY_WAIT/FAILED/SHUTDOWN。所有时间阈值通过构造参数传入,策略不引用 Android 类。
预期:所有恢复策略测试通过。
Files:
Create: app/src/main/java/com/zhct/traybinding/hardware/weigher/WeigherDriver.java
Create: app/src/main/java/com/zhct/traybinding/hardware/weigher/AithingsWeigherDriver.java
Test: app/src/test/java/com/zhct/traybinding/hardware/weigher/FakeWeigherDriver.java
Step 1: 定义最小驱动接口
interface WeigherDriver {
interface Listener {
void onInit(boolean success);
void onWeight(int address, int state, int weightGrams);
void onIdentifyRead(int rate);
void onError(int errorCode);
}
void setListener(Listener listener);
void init();
void requestWeight();
void startContinuousRead();
void stopContinuousRead();
void setFastFilter(int value);
void setIdentify(int value);
void readIdentify();
void zero();
void tare();
void release();
}
将 SDK 回调转换为 Driver 回调。类注释必须说明 Weigher 是静态单例、初始化线程必须有 Looper、同一进程不得创建第二个拥有者。
Fake 记录每个方法调用次数和调用顺序,并提供主动触发初始化、重量和错误回调的方法。
Files:
Create: app/src/test/java/com/zhct/traybinding/hardware/weigher/WeigherControllerTest.java
Create: app/src/main/java/com/zhct/traybinding/hardware/weigher/WeigherController.java
Step 1: 写 Controller 失败测试
必须覆盖:
@Test public void repeatedStartInitializesDriverOnlyOnce() { }
@Test public void listenerIsInstalledBeforeInit() { }
@Test public void initSuccessStartsContinuousReadAndAppliesConfigOnce() { }
@Test public void subscriberReceivesLatestReadingImmediately() { }
@Test public void repeatedRecoveryRequestsAreCoalesced() { }
@Test public void hardwareFaultDoesNotEnterTightRetryLoop() { }
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) sh ./gradlew :app:testDebugUnitTest --tests 'com.zhct.traybinding.hardware.weigher.WeigherControllerTest'
Controller 提供 initialize(Context) 创建生产单例,内部创建 Driver 和 HandlerThread("weigher-control");重复初始化返回同一实例。包级测试构造器注入 Driver、控制调度器、主线程分发器、时钟、配置提供者和进程重启器,测试使用同步实现。
依赖接口作为 Controller 的包级嵌套接口定义,避免增加无职责的公共类:
interface TaskScheduler {
void execute(Runnable task);
void executeDelayed(Runnable task, long delayMillis);
void cancel(Runnable task);
}
interface EventDispatcher {
void dispatch(Runnable task);
}
interface Clock {
long elapsedRealtime();
long currentTimeMillis();
}
interface ConfigProvider {
int fastFilter();
int identifyRate();
long lastAutomaticRestartAt();
void recordAutomaticRestartAt(long wallClockMillis);
}
interface ProcessRestarter {
void restart(String reason);
}
运行期静默判断使用 elapsedRealtime(),跨进程持久化的 5 分钟重启限流使用 currentTimeMillis(),避免设备重启后 elapsed realtime 清零导致时间域不一致。
公共 API:
public void start();
public static WeigherController initialize(Context context);
public static WeigherController getInstance();
public void addListener(Listener listener, boolean replayLatest);
public void removeListener(Listener listener);
public WeigherState getState();
public WeigherReading getLatestReading();
public boolean hasValidReading();
public void requestWeight();
public void retryFromUser();
public void zero();
public void tare();
public void readIdentify();
public void setIdentify(int value);
回调 UI 前复制监听器快照,不持有 Controller 锁。重量序号由 Controller 单调递增;latestReading 使用 volatile 或同步访问保证可见性。
预期:目标测试全部通过,Fake Driver 的调用序列为 setListener → init → requestWeight → startContinuousRead → setFastFilter → setIdentify。
Files:
Modify: app/src/main/java/com/zhct/traybinding/hardware/weigher/WeigherController.java
Modify: app/src/test/java/com/zhct/traybinding/hardware/weigher/WeigherControllerTest.java
Step 1: 增加静默与恢复测试
验证 5 秒静默只发送一次软探测;探测后收到重量恢复 ACTIVE;探测后 3 秒仍无数据只请求一次受控重启;自动重启被 5 分钟限流。
使用具名常量:
private static final long INITIALIZATION_TIMEOUT_MILLIS = 6_000L;
private static final long SILENCE_TIMEOUT_MILLIS = 5_000L;
private static final long PROBE_GRACE_MILLIS = 3_000L;
private static final long AUTO_RESTART_WINDOW_MILLIS = 300_000L;
private static final int MAX_INITIALIZATION_ATTEMPTS = 2;
运行中静默不无限执行 SDK 重建;软探测失败后调用注入的 ProcessRestarter。重启次数写入 MMKV,超过频率进入 FAILED。
连接错误可恢复;零位与传感器错误进入硬件故障;鉴别率错误恢复默认值一次;未知错误保留原始错误码。
预期:无并发恢复、无无限重试、状态转换符合设计。
Files:
Modify: app/src/main/java/com/zhct/traybinding/application/CptApplication.java
Step 1: 在基础设施完成后启动 Controller
在 MMKV 和 ResourcesUtils 初始化之后,当 Constants.APP_MODEL_TAG == 2 时依次调用 WeigherController.initialize(this) 和 WeigherController.getInstance().start()。
确认 start()只向 weigher-control 投递任务,Application.onCreate()不执行串口 RootCmd 或阻塞等待。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) sh ./gradlew :app:assembleDebug
Files:
Modify: app/src/main/java/com/zhct/traybinding/main/WeightMainActivity.java
Step 1: 改为 Controller 监听器
onStart()只注册监听并读取最新快照;onStop()取消监听。onDestroy()不关闭进程级秤体。
删除旧 SensorScale 字段、openWeightSensorScale()、两套初始化实现、重新初始化弹窗和空 SensorScale.closeScale()逻辑。
每包继续更新序号与稳定性会话;重量变化时只调用一次 Fragment 回调。新增 registerWeightValueChangeCallback(listener, replayLatest) 和 unregisterWeightValueChangeCallback(listener),旧 Fragment 不能清除新 Fragment。
进入打餐中前检查 Controller 快照;无有效重量时清空当前扫码状态并提示用户,不记录 Integer.MAX_VALUE。
扫码器由 Activity 自身生命周期启动,不再依赖秤体 onInit(true)。
Files:
Modify: app/src/main/java/com/zhct/traybinding/fragment/PickBeforeFragment.java
Modify: app/src/main/java/com/zhct/traybinding/fragment/PickOngoingFragment.java
Modify: app/src/main/java/com/zhct/traybinding/fragment/PickOngoingWithBalanceFragment.java
Modify: app/src/main/java/com/zhct/traybinding/fragment/chongqinghuanwei/ChongqingHuanweiPickBeforeFragment.java
Modify: app/src/main/java/com/zhct/traybinding/fragment/chongqinghuanwei/ChongqingHuanweiPickOngoingFragment.java
Step 1: 打餐前页面在 onStart 注册并回放
onStop()使用监听器实例取消注册。
在 onResume()中先获取起始重量和序号,再注册并请求回放;onStop()取消注册。
重量保持不变时切换页面,打餐中页面应立即显示 0g,不得显示整盘重量或空值。
Files:
Modify: app/src/main/java/com/zhct/traybinding/widget/DebugWeigherPop.java
Modify: app/src/main/java/com/zhct/traybinding/main/DemoActivity.java
Step 1: DebugWeigherPop 使用 Controller
连接按钮改为状态检测/用户重试;读取重量、置零、去皮和鉴别率命令转给 Controller;弹窗关闭只取消监听。
不再创建 SensorScale、遍历设备或直接关闭串口。自动读取开关只控制调试 UI 是否刷新。
移除直接 SensorScale 初始化和关闭逻辑;若 Demo 不再使用,则从正式 Manifest 移除入口。
rg -n 'new SensorScale|Weigher\.(init|unInit|startContinuousRead|stopContinuousRead)' app/src/main/java
预期:生产调用只存在于 AithingsWeigherDriver。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) sh ./gradlew :app:testDebugUnitTest --tests 'com.zhct.traybinding.hardware.weigher.*'
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) sh ./gradlew :app:testDebugUnitTest
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) sh ./gradlew :app:assembleDebug
git diff --check
git status --short
确认没有过程文档进入源码仓库,没有无说明的魔法时间值和串口路径。
SerialPortRead 数量始终为 1。0g 差值。Files:
Create: work_android/WeightSingle116/change_records/2026-09-04-process-wide-weigher-controller.md
Create: same-basename HTML
Modify: work_android/WeightSingle116/change_records/CHANGELOG.md
记录根因、状态机、代码范围、SDK 限制、测试命令和真机证据。
Markdown 与 HTML 内容一致。
不提交或推送 Git,除非用户明确要求。