refactor(plc): 重构PLC通信管理与监控轮询机制

- 实现PLC长连接策略,支持连接复用和自动重连
- 添加监控轮询的退避重试机制,支持快速重试和最大延迟配置
- 实现写入指令期间暂停监控轮询,防止线程池排队积压
- 添加详细的诊断日志自动记录和错误分类功能
- 优化UI状态渲染,区分正常监控和通讯异常状态
- 添加后台断连释放连接资源,防止连接泄露
- 实现监控策略类解耦,支持灵活的异常判定规则
- 添加单元测试覆盖监控策略和断连功能
main
yangk 2 weeks ago
parent 56fdf39d8e
commit 6fbc8971c7

@ -3,8 +3,10 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".PdaPlcApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -25,4 +27,4 @@
</activity>
</application>
</manifest>
</manifest>

@ -0,0 +1,72 @@
package com.example.pdaplccontrol;
import java.util.Map;
final class DiagnosticJson {
private DiagnosticJson() {
}
static String toJson(Map<String, Object> values) {
StringBuilder json = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : values.entrySet()) {
if (!first) {
json.append(',');
}
first = false;
appendQuoted(json, entry.getKey());
json.append(':');
appendValue(json, entry.getValue());
}
return json.append('}').toString();
}
private static void appendValue(StringBuilder json, Object value) {
if (value == null) {
json.append("null");
} else if (value instanceof Number || value instanceof Boolean) {
json.append(value);
} else {
appendQuoted(json, String.valueOf(value));
}
}
private static void appendQuoted(StringBuilder json, String value) {
json.append('"');
for (int i = 0; i < value.length(); i++) {
char current = value.charAt(i);
switch (current) {
case '"':
json.append("\\\"");
break;
case '\\':
json.append("\\\\");
break;
case '\b':
json.append("\\b");
break;
case '\f':
json.append("\\f");
break;
case '\n':
json.append("\\n");
break;
case '\r':
json.append("\\r");
break;
case '\t':
json.append("\\t");
break;
default:
if (current < 0x20) {
json.append(String.format("\\u%04x", (int) current));
} else {
json.append(current);
}
break;
}
}
json.append('"');
}
}

@ -0,0 +1,24 @@
package com.example.pdaplccontrol;
final class DiagnosticLogFilePolicy {
static final long MAX_FILE_BYTES = 20L * 1024L * 1024L;
static final int MAX_FILE_COUNT = 10;
static final String FILE_PREFIX = "PdaPlc-diagnostics-";
private DiagnosticLogFilePolicy() {
}
static String fileName(String sessionTimestamp, int partNumber) {
String partSuffix = partNumber <= 1 ? "" : "-part-" + partNumber;
return FILE_PREFIX + sessionTimestamp + partSuffix + ".jsonl";
}
static boolean shouldRotate(long currentBytes, int nextLineBytes) {
return currentBytes > 0 && currentBytes + nextLineBytes > MAX_FILE_BYTES;
}
static boolean shouldDeleteBeforeCreating(int newestFirstIndex) {
return newestFirstIndex >= MAX_FILE_COUNT - 1;
}
}

@ -0,0 +1,98 @@
package com.example.pdaplccontrol;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
final class DiagnosticLogger {
private static final String TAG = "PdaPlcDiagnostics";
private static final ExecutorService LOG_EXECUTOR = Executors.newSingleThreadExecutor();
private static final AtomicLong OPERATION_SEQUENCE = new AtomicLong();
private static volatile boolean initialized;
private static Context applicationContext;
private static String sessionTimestamp;
private static MediaStoreDiagnosticLogWriter writer;
private DiagnosticLogger() {
}
static synchronized void initialize(Context context) {
if (initialized) {
return;
}
initialized = true;
applicationContext = context.getApplicationContext();
sessionTimestamp = formatLocalTime("yyyyMMdd-HHmmss", new Date());
LOG_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
ensureWriter();
}
});
}
static long nextOperationId() {
return OPERATION_SEQUENCE.incrementAndGet();
}
static void log(String eventName, Object... keyValues) {
try {
final Map<String, Object> event = new LinkedHashMap<>();
event.put("timestamp", formatLocalTime("yyyy-MM-dd'T'HH:mm:ss.SSSZ", new Date()));
event.put("elapsed_ms", SystemClock.elapsedRealtime());
event.put("event", eventName);
for (int i = 0; i + 1 < keyValues.length; i += 2) {
event.put(String.valueOf(keyValues[i]), keyValues[i + 1]);
}
LOG_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
MediaStoreDiagnosticLogWriter currentWriter = ensureWriter();
if (currentWriter == null) {
return;
}
try {
currentWriter.write(event);
} catch (Exception e) {
currentWriter.recoverAfterFailure();
Log.e(TAG, "写入自动诊断日志失败", e);
}
}
});
} catch (Exception e) {
Log.e(TAG, "提交自动诊断日志失败", e);
}
}
private static MediaStoreDiagnosticLogWriter ensureWriter() {
if (writer != null || applicationContext == null || sessionTimestamp == null) {
return writer;
}
try {
writer = new MediaStoreDiagnosticLogWriter(applicationContext, sessionTimestamp);
writer.openInitialFile();
} catch (Exception e) {
writer = null;
Log.e(TAG, "创建自动诊断日志失败", e);
}
return writer;
}
private static String formatLocalTime(String pattern, Date date) {
SimpleDateFormat formatter = new SimpleDateFormat(pattern, Locale.US);
formatter.setTimeZone(TimeZone.getDefault());
return formatter.format(date);
}
}

@ -4,6 +4,7 @@ import android.content.res.ColorStateList;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
@ -15,6 +16,10 @@ import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
/**
* PDA
* 线 PLC 线
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private final Button[] lineButtons = new Button[9];
@ -28,8 +33,27 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private EditText editWeightKg;
private TextView textMonitorStatus;
private LinearLayout rootLayout;
// 监控是否应该处于活跃轮询状态(生命周期相关)
private boolean monitorRunning;
// 标识是否已发起了读取请求但尚未返回,防止在低波速或卡顿网络下造成轮询任务堆积
private boolean monitorRequestInFlight;
// 核心避坑设计:标识当前是否正在向 PLC 写入指令(发线体号或重量)。
// 在写入期间,必须暂停后台监控轮询。这是因为底层 Moka7 S7 驱动是单线程串行交互,
// 读写操作串行排队。如果在下发指令时监控依然频繁发起读请求,会导致指令响应变慢甚至连接阻塞。
private boolean commandInProgress;
// 标识本次启动后是否至少成功读取过一次 PLC 数据,用于辅助断线过期时间判定
private boolean hasSuccessfulMonitorRead;
// 连续监控读取失败的次数,用于配合策略执行退避重试和异常渲染
private int consecutiveMonitorFailures;
// 记录上一次成功读取到 PLC 数据时的系统开机时间(毫秒)
private long lastMonitorSuccessElapsedMillis;
private ColorStateList defaultLineButtonBackgroundTint;
private ColorStateList defaultLineButtonTextColors;
@ -37,6 +61,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DiagnosticLogger.log("activity_created");
rootLayout = findViewById(R.id.rootLayout);
editWeightKg = findViewById(R.id.editWeightKg);
@ -48,12 +73,15 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
@Override
protected void onStart() {
super.onStart();
// 页面可见时启动 PLC 数据周期监控
startMonitor();
}
@Override
protected void onStop() {
// 页面不可见时立即停止轮询,并释放 PLC 的长连接,防止退至后台后死连接无谓占用 PLC 的受限连接池
stopMonitor();
PlcManager.getInstance().disconnectAsync();
super.onStop();
}
@ -66,7 +94,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
&& event.getAction() == KeyEvent.ACTION_UP
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER);
if (isDone) {
rootLayout.requestFocus();
rootLayout.requestFocus(); // 输入完成后转移焦点,方便更新 PLC 传回的最新值
sendWeight();
return true;
}
@ -99,6 +127,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
if (v instanceof Button) {
short lineNumber = (short) v.getTag();
// 发送指令前暂停监控以防线程池排队积压
pauseMonitorForCommand();
setControlsEnabled(false);
Toast.makeText(this, "正在向 PLC 发送线体号 " + lineNumber + "...", Toast.LENGTH_SHORT).show();
@ -115,6 +145,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
return;
}
// 发送指令前暂停监控以防线程池排队积压
pauseMonitorForCommand();
setControlsEnabled(false);
Toast.makeText(this, "正在向 PLC 发送重量 "
+ WeightInputParser.kgToDisplayText(weightKg) + "kg...", Toast.LENGTH_SHORT).show();
@ -122,18 +154,25 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
PlcManager.getInstance().sendWeightKgAsync(weightKg, createCallback());
}
/**
*
*/
private PlcManager.PlcCallback createCallback() {
return new PlcManager.PlcCallback() {
@Override
public void onSuccess(String message) {
Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
setControlsEnabled(true);
// 写入指令执行完毕,延迟恢复周期监控
resumeMonitorAfterCommand();
}
@Override
public void onError(String error) {
Toast.makeText(MainActivity.this, error, Toast.LENGTH_LONG).show();
setControlsEnabled(true);
// 即使下发失败,也需恢复监控以更新当前真实数据
resumeMonitorAfterCommand();
}
};
}
@ -149,17 +188,37 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void startMonitor() {
monitorRunning = true;
commandInProgress = false;
monitorRequestInFlight = false;
consecutiveMonitorFailures = 0;
hasSuccessfulMonitorRead = false;
lastMonitorSuccessElapsedMillis = 0L;
monitorHandler.removeCallbacks(monitorRunnable);
renderMonitorPending();
renderMonitorNormal();
DiagnosticLogger.log(
"monitor_started",
"ui_status", currentMonitorStatus(),
"monitor_interval_ms", PlcConfig.MONITOR_INTERVAL_MILLIS
);
monitorHandler.post(monitorRunnable);
}
private void stopMonitor() {
monitorRunning = false;
commandInProgress = false;
monitorHandler.removeCallbacks(monitorRunnable);
DiagnosticLogger.log(
"monitor_stopped",
"consecutive_failures", consecutiveMonitorFailures,
"ui_status", currentMonitorStatus()
);
}
/**
*
*/
private void readCurrentValues() {
// 若已停止监控、或上一次读取网络请求仍在执行中,则跳过本次,防止堆积
if (!monitorRunning || monitorRequestInFlight) {
return;
}
@ -172,50 +231,95 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
if (monitorRunning) {
renderCurrentValues(values);
}
scheduleNextMonitorRead();
// 按照策略类中推荐的延迟执行下一次读取
int nextDelayMillis = PlcMonitorPolicy.nextDelayMillis(consecutiveMonitorFailures);
DiagnosticLogger.log(
"monitor_read_success",
"monitor_running", monitorRunning,
"consecutive_failures", consecutiveMonitorFailures,
"millis_since_last_success", 0L,
"ui_status", currentMonitorStatus(),
"next_delay_ms", nextDelayMillis
);
scheduleNextMonitorRead(nextDelayMillis);
}
@Override
public void onError(String error) {
monitorRequestInFlight = false;
if (monitorRunning) {
renderMonitorStale();
renderMonitorFailure();
}
scheduleNextMonitorRead();
// 读取失败时依据策略如连续失败1/2次时加速重试计算退避重试延迟
int nextDelayMillis = PlcMonitorPolicy.nextDelayMillis(consecutiveMonitorFailures);
DiagnosticLogger.log(
"monitor_read_failure",
"monitor_running", monitorRunning,
"consecutive_failures", consecutiveMonitorFailures,
"millis_since_last_success", millisSinceLastSuccess(),
"ui_status", currentMonitorStatus(),
"next_delay_ms", nextDelayMillis,
"error_text", error
);
scheduleNextMonitorRead(nextDelayMillis);
}
});
}
private void scheduleNextMonitorRead() {
if (monitorRunning) {
monitorHandler.postDelayed(monitorRunnable, PlcConfig.MONITOR_INTERVAL_MILLIS);
private void scheduleNextMonitorRead(int delayMillis) {
// 如果用户正在执行下发动作,则不再安排下一次轮询,彻底让路给写入指令
if (monitorRunning && !commandInProgress) {
monitorHandler.removeCallbacks(monitorRunnable);
monitorHandler.postDelayed(monitorRunnable, delayMillis);
}
}
/**
* PLC
*/
private void renderCurrentValues(PlcCurrentValues values) {
consecutiveMonitorFailures = 0;
hasSuccessfulMonitorRead = true;
lastMonitorSuccessElapsedMillis = SystemClock.elapsedRealtime(); // 记录成功刷新的时间戳
renderMonitorNormal();
// 避坑考量:如果当前输入框正处于编辑聚焦状态,不应强制刷新数据,以防止覆盖用户的输入
if (!editWeightKg.hasFocus()) {
editWeightKg.setText(WeightInputParser.kgToDisplayText(values.getWeightKg()));
}
renderCurrentLineNumber(values.getLineNumber());
}
private void renderMonitorPending() {
textMonitorStatus.setText("正在读取PLC...");
textMonitorStatus.setTextColor(getColor(R.color.monitor_status_pending));
}
private void renderMonitorNormal() {
textMonitorStatus.setText("监控正常");
textMonitorStatus.setText("监控中");
textMonitorStatus.setTextColor(getColor(R.color.monitor_status_normal));
}
private void renderMonitorStale() {
textMonitorStatus.setText("已失联/数据过期");
textMonitorStatus.setTextColor(getColor(R.color.monitor_status_stale));
clearCurrentLineNumberHighlight();
/**
* 退
*/
private void renderMonitorFailure() {
consecutiveMonitorFailures++;
long millisSinceLastSuccess = hasSuccessfulMonitorRead
? SystemClock.elapsedRealtime() - lastMonitorSuccessElapsedMillis
: 0L;
// 委托策略类判定此时是否属于严重“通讯异常”
if (PlcMonitorPolicy.isCommunicationAbnormal(
consecutiveMonitorFailures,
hasSuccessfulMonitorRead,
millisSinceLastSuccess
)) {
textMonitorStatus.setText("PLC通讯异常");
textMonitorStatus.setTextColor(getColor(R.color.monitor_status_stale));
} else {
// 如果连续失败次数较少且未满超时时间,前台依然静默维持“监控中”的正常状态,规避闪烁报错
renderMonitorNormal();
}
}
/**
* PLC 线
*/
private void renderCurrentLineNumber(short currentLineNumber) {
for (Button btn : lineButtons) {
if (btn == null) {
@ -233,12 +337,41 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
}
private void clearCurrentLineNumberHighlight() {
for (Button btn : lineButtons) {
if (btn != null) {
btn.setBackgroundTintList(defaultLineButtonBackgroundTint);
btn.setTextColor(defaultLineButtonTextColors);
}
/**
*
*/
private void pauseMonitorForCommand() {
commandInProgress = true;
monitorHandler.removeCallbacks(monitorRunnable);
DiagnosticLogger.log(
"monitor_paused_for_command",
"consecutive_failures", consecutiveMonitorFailures,
"ui_status", currentMonitorStatus()
);
}
/**
*
*/
private void resumeMonitorAfterCommand() {
commandInProgress = false;
if (monitorRunning) {
DiagnosticLogger.log(
"monitor_resumed_after_command",
"next_delay_ms", PlcConfig.MONITOR_INTERVAL_MILLIS,
"ui_status", currentMonitorStatus()
);
scheduleNextMonitorRead(PlcConfig.MONITOR_INTERVAL_MILLIS);
}
}
private long millisSinceLastSuccess() {
return hasSuccessfulMonitorRead
? SystemClock.elapsedRealtime() - lastMonitorSuccessElapsedMillis
: 0L;
}
private String currentMonitorStatus() {
return textMonitorStatus == null ? "UNKNOWN" : textMonitorStatus.getText().toString();
}
}

@ -0,0 +1,175 @@
package com.example.pdaplccontrol;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
final class MediaStoreDiagnosticLogWriter {
private static final String RELATIVE_PATH = Environment.DIRECTORY_DOWNLOADS + "/PdaPlc/";
private final Context context;
private final ContentResolver contentResolver;
private final String sessionTimestamp;
private OutputStream outputStream;
private long currentBytes;
private int partNumber;
MediaStoreDiagnosticLogWriter(Context context, String sessionTimestamp) {
this.context = context.getApplicationContext();
this.contentResolver = context.getContentResolver();
this.sessionTimestamp = sessionTimestamp;
}
void openInitialFile() throws IOException {
openNextFile();
}
void write(Map<String, Object> event) throws IOException {
appendNetworkState(event);
byte[] line = (DiagnosticJson.toJson(event) + "\n").getBytes(StandardCharsets.UTF_8);
if (DiagnosticLogFilePolicy.shouldRotate(currentBytes, line.length)) {
openNextFile();
}
ensureOpen();
outputStream.write(line);
outputStream.flush();
currentBytes += line.length;
}
void recoverAfterFailure() {
closeCurrentFile();
}
private void ensureOpen() throws IOException {
if (outputStream == null) {
openNextFile();
}
}
private void openNextFile() throws IOException {
closeCurrentFile();
cleanupOldFilesBeforeCreating();
partNumber++;
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME,
DiagnosticLogFilePolicy.fileName(sessionTimestamp, partNumber));
values.put(MediaStore.MediaColumns.MIME_TYPE, "application/x-ndjson");
values.put(MediaStore.MediaColumns.RELATIVE_PATH, RELATIVE_PATH);
Uri uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri == null) {
throw new IOException("无法在 Download/PdaPlc 创建诊断日志");
}
outputStream = contentResolver.openOutputStream(uri, "w");
if (outputStream == null) {
contentResolver.delete(uri, null, null);
throw new IOException("无法打开诊断日志输出流");
}
currentBytes = 0L;
}
private void cleanupOldFilesBeforeCreating() {
String[] projection = {MediaStore.MediaColumns._ID};
String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=? AND "
+ MediaStore.MediaColumns.DISPLAY_NAME + " LIKE ?";
String[] selectionArgs = {RELATIVE_PATH, DiagnosticLogFilePolicy.FILE_PREFIX + "%"};
String sortOrder = MediaStore.MediaColumns.DATE_ADDED + " DESC, "
+ MediaStore.MediaColumns.DISPLAY_NAME + " DESC";
try (Cursor cursor = contentResolver.query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)) {
if (cursor == null) {
return;
}
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID);
int index = 0;
while (cursor.moveToNext()) {
if (DiagnosticLogFilePolicy.shouldDeleteBeforeCreating(index)) {
long id = cursor.getLong(idColumn);
Uri uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id);
contentResolver.delete(uri, null, null);
}
index++;
}
} catch (Exception ignored) {
// 清理失败不能影响本次诊断日志创建,更不能影响 PLC 通讯。
}
}
private void appendNetworkState(Map<String, Object> event) {
try {
ConnectivityManager manager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network activeNetwork = manager == null ? null : manager.getActiveNetwork();
NetworkCapabilities capabilities =
activeNetwork == null ? null : manager.getNetworkCapabilities(activeNetwork);
event.put("active_network", activeNetwork != null && capabilities != null);
event.put("network_type", networkType(capabilities));
} catch (Exception e) {
event.put("active_network", null);
event.put("network_type", "UNKNOWN");
event.put("network_state_error", e.getClass().getSimpleName());
}
}
private static String networkType(NetworkCapabilities capabilities) {
if (capabilities == null) {
return "NO_ACTIVE_NETWORK";
}
StringBuilder type = new StringBuilder();
appendTransport(type, capabilities, NetworkCapabilities.TRANSPORT_WIFI, "WIFI");
appendTransport(type, capabilities, NetworkCapabilities.TRANSPORT_CELLULAR, "CELLULAR");
appendTransport(type, capabilities, NetworkCapabilities.TRANSPORT_ETHERNET, "ETHERNET");
appendTransport(type, capabilities, NetworkCapabilities.TRANSPORT_VPN, "VPN");
return type.length() == 0 ? "OTHER" : type.toString();
}
private static void appendTransport(
StringBuilder type,
NetworkCapabilities capabilities,
int transport,
String name
) {
if (!capabilities.hasTransport(transport)) {
return;
}
if (type.length() > 0) {
type.append('+');
}
type.append(name);
}
private void closeCurrentFile() {
if (outputStream == null) {
return;
}
try {
outputStream.close();
} catch (IOException ignored) {
// 关闭失败仅影响诊断日志,不能向业务线程传播。
} finally {
outputStream = null;
currentBytes = 0L;
}
}
}

@ -2,33 +2,63 @@ package com.example.pdaplccontrol;
import Moka7.S7;
import Moka7.S7Client;
import android.os.SystemClock;
/**
* Moka7 S7 PLC
*
*/
class Moka7PlcValueSender implements PlcValueSender {
// S7 客户端连接实例。长连接策略下,此实例生命周期会持续存在,直到发生异常或主动断连。
private S7Client client;
@Override
public void sendLineNumber(short lineNumber) throws PlcCommunicationException {
writeWord(PlcConfig.LINE_NUMBER_OFFSET, lineNumber);
writeWord(PlcConfig.LINE_NUMBER_OFFSET, lineNumber, "WRITE_LINE_NUMBER");
}
@Override
public void sendWeightKg(short weightKg) throws PlcCommunicationException {
writeWord(PlcConfig.WEIGHT_KG_OFFSET, weightKg);
writeWord(PlcConfig.WEIGHT_KG_OFFSET, weightKg, "WRITE_WEIGHT");
}
@Override
public PlcCurrentValues readCurrentValues() throws PlcCommunicationException {
byte[] data = readBytes(PlcConfig.MONITOR_START_OFFSET, PlcConfig.MONITOR_BYTE_LENGTH);
byte[] data = readBytes(
PlcConfig.MONITOR_START_OFFSET,
PlcConfig.MONITOR_BYTE_LENGTH,
"READ_CURRENT_VALUES"
);
return new PlcCurrentValues(
decodeWord(data, PlcConfig.LINE_NUMBER_OFFSET - PlcConfig.MONITOR_START_OFFSET),
decodeWord(data, PlcConfig.WEIGHT_KG_OFFSET - PlcConfig.MONITOR_START_OFFSET)
);
}
private void writeWord(int offset, short value) throws PlcCommunicationException {
S7Client client = new S7Client();
try {
connect(client);
/**
* PLC
* TCP ensureConnected
*/
@Override
public synchronized void disconnect() {
disconnectInternal(DiagnosticLogger.nextOperationId(), "REQUESTED");
}
/**
* Word DB
*/
private void writeWord(int offset, short value, String operationType)
throws PlcCommunicationException {
long operationId = DiagnosticLogger.nextOperationId();
long operationStarted = SystemClock.elapsedRealtime();
boolean connectedBefore = isConnected();
boolean reusedConnection = false;
logOperationStart(operationId, operationType, connectedBefore);
try {
reusedConnection = ensureConnected(operationId, operationType);
long ioStarted = SystemClock.elapsedRealtime();
int writeRes = client.WriteArea(
S7.S7AreaDB,
PlcConfig.DB_NUMBER,
@ -36,53 +66,295 @@ class Moka7PlcValueSender implements PlcValueSender {
PlcConfig.WORD_BYTE_LENGTH,
encodeWord(value)
);
logIoResult(operationId, operationType, "WRITE", ioStarted, writeRes);
if (writeRes != 0) {
throw new PlcCommunicationException("写入数据失败,错误码:" + S7Client.ErrorText(writeRes));
// 写入失败说明当前长连接可能已失效,需立即执行断连清理,避免下一次调用继续读取损坏的连接
disconnectInternal(operationId, "WRITE_FAILED");
throw communicationException("写入数据失败", "WRITE", writeRes);
}
logOperationSuccess(
operationId,
operationType,
operationStarted,
connectedBefore,
reusedConnection
);
} catch (PlcCommunicationException e) {
logOperationFailure(
operationId,
operationType,
operationStarted,
connectedBefore,
reusedConnection,
e
);
throw e;
} catch (Exception e) {
throw new PlcCommunicationException("通讯发生异常: " + e.getMessage(), e);
} finally {
client.Disconnect();
// 捕获可能产生的 Socket、网络断开等非受控异常执行连接清理确保重连自愈
disconnectInternal(operationId, "JAVA_EXCEPTION");
PlcCommunicationException communicationException = new PlcCommunicationException(
"通讯发生异常: " + e.getMessage(),
"WRITE",
null,
e
);
logOperationFailure(
operationId,
operationType,
operationStarted,
connectedBefore,
reusedConnection,
communicationException
);
throw communicationException;
}
}
private byte[] readBytes(int offset, int amount) throws PlcCommunicationException {
S7Client client = new S7Client();
/**
*
*/
private byte[] readBytes(int offset, int amount, String operationType)
throws PlcCommunicationException {
long operationId = DiagnosticLogger.nextOperationId();
long operationStarted = SystemClock.elapsedRealtime();
boolean connectedBefore = isConnected();
boolean reusedConnection = false;
logOperationStart(operationId, operationType, connectedBefore);
try {
connect(client);
reusedConnection = ensureConnected(operationId, operationType);
byte[] data = new byte[amount];
long ioStarted = SystemClock.elapsedRealtime();
int readRes = client.ReadArea(S7.S7AreaDB, PlcConfig.DB_NUMBER, offset, amount, data);
logIoResult(operationId, operationType, "READ", ioStarted, readRes);
if (readRes != 0) {
throw new PlcCommunicationException("读取数据失败,错误码:" + S7Client.ErrorText(readRes));
// 读取失败立即断连,以便下一次读取能重连,避免残余脏连接阻塞后续轮询
disconnectInternal(operationId, "READ_FAILED");
throw communicationException("读取数据失败", "READ", readRes);
}
logOperationSuccess(
operationId,
operationType,
operationStarted,
connectedBefore,
reusedConnection
);
return data;
} catch (PlcCommunicationException e) {
logOperationFailure(
operationId,
operationType,
operationStarted,
connectedBefore,
reusedConnection,
e
);
throw e;
} catch (Exception e) {
throw new PlcCommunicationException("通讯发生异常: " + e.getMessage(), e);
} finally {
client.Disconnect();
disconnectInternal(operationId, "JAVA_EXCEPTION");
PlcCommunicationException communicationException = new PlcCommunicationException(
"通讯发生异常: " + e.getMessage(),
"READ",
null,
e
);
logOperationFailure(
operationId,
operationType,
operationStarted,
connectedBefore,
reusedConnection,
communicationException
);
throw communicationException;
}
}
private void connect(S7Client client) throws PlcCommunicationException {
/**
* PLC
*
* 1. TCP S7
* 2. disconnect S7Client
*/
private boolean ensureConnected(long operationId, String operationType)
throws PlcCommunicationException {
if (isConnected()) {
DiagnosticLogger.log(
"plc_connection_reused",
"operation_id", operationId,
"operation_type", operationType,
"connected", true
);
return true;
}
// 先清理可能残留的半连接或无效 client
disconnectInternal(operationId, "BEFORE_CONNECT_CLEANUP");
client = new S7Client();
client.SetConnectionType(S7.OP);
long connectStarted = SystemClock.elapsedRealtime();
DiagnosticLogger.log(
"plc_connect_start",
"operation_id", operationId,
"operation_type", operationType,
"receive_timeout_ms", client.RecvTimeout
);
int connectRes = client.ConnectTo(PlcConfig.PLC_IP, PlcConfig.RACK, PlcConfig.SLOT);
if (connectRes != 0) {
throw new PlcCommunicationException("PLC连接失败错误码" + connectRes);
DiagnosticLogger.log(
"plc_connect_failure",
"operation_id", operationId,
"operation_type", operationType,
"duration_ms", SystemClock.elapsedRealtime() - connectStarted,
"error_code", connectRes,
"error_hex", PlcErrorClassifier.errorHex(connectRes),
"error_category", PlcErrorClassifier.category(connectRes),
"error_text", S7Client.ErrorText(connectRes)
);
disconnectInternal(operationId, "CONNECT_FAILED");
throw new PlcCommunicationException(
"PLC连接失败错误码" + connectRes,
"CONNECT",
connectRes
);
}
DiagnosticLogger.log(
"plc_connect_success",
"operation_id", operationId,
"operation_type", operationType,
"duration_ms", SystemClock.elapsedRealtime() - connectStarted,
"connected", true,
"negotiated_pdu_length", client.PDULength()
);
return false;
}
private synchronized void disconnectInternal(long operationId, String reason) {
if (client == null) {
return;
}
boolean connectedBefore = client.Connected;
long started = SystemClock.elapsedRealtime();
client.Disconnect();
client = null;
DiagnosticLogger.log(
"plc_disconnect",
"operation_id", operationId,
"reason", reason,
"duration_ms", SystemClock.elapsedRealtime() - started,
"connected_before", connectedBefore,
"connected_after", false
);
}
private boolean isConnected() {
return client != null && client.Connected;
}
private static PlcCommunicationException communicationException(
String message,
String phase,
int errorCode
) {
return new PlcCommunicationException(
message + ",错误码:" + S7Client.ErrorText(errorCode),
phase,
errorCode
);
}
private void logOperationStart(long operationId, String operationType, boolean connectedBefore) {
DiagnosticLogger.log(
"plc_operation_start",
"operation_id", operationId,
"operation_type", operationType,
"connected_before", connectedBefore
);
}
private void logOperationSuccess(
long operationId,
String operationType,
long operationStarted,
boolean connectedBefore,
boolean reusedConnection
) {
DiagnosticLogger.log(
"plc_operation_success",
"operation_id", operationId,
"operation_type", operationType,
"duration_ms", SystemClock.elapsedRealtime() - operationStarted,
"connected_before", connectedBefore,
"connected_after", isConnected(),
"reused_connection", reusedConnection
);
}
private void logOperationFailure(
long operationId,
String operationType,
long operationStarted,
boolean connectedBefore,
boolean reusedConnection,
PlcCommunicationException exception
) {
Integer errorCode = exception.getErrorCode();
Throwable cause = exception.getCause();
DiagnosticLogger.log(
"plc_operation_failure",
"operation_id", operationId,
"operation_type", operationType,
"duration_ms", SystemClock.elapsedRealtime() - operationStarted,
"connected_before", connectedBefore,
"connected_after", isConnected(),
"reused_connection", reusedConnection,
"failure_phase", exception.getPhase(),
"error_code", errorCode,
"error_hex", errorCode == null ? null : PlcErrorClassifier.errorHex(errorCode),
"error_category", errorCode == null
? "JAVA_EXCEPTION"
: PlcErrorClassifier.category(errorCode),
"error_text", exception.getMessage(),
"exception_type", cause == null ? null : cause.getClass().getName(),
"exception_message", cause == null ? null : cause.getMessage()
);
}
private void logIoResult(
long operationId,
String operationType,
String phase,
long ioStarted,
int errorCode
) {
boolean success = errorCode == 0;
DiagnosticLogger.log(
"plc_io_result",
"operation_id", operationId,
"operation_type", operationType,
"phase", phase,
"duration_ms", SystemClock.elapsedRealtime() - ioStarted,
"success", success,
"error_code", success ? null : errorCode,
"error_hex", success ? null : PlcErrorClassifier.errorHex(errorCode),
"error_category", success ? null : PlcErrorClassifier.category(errorCode),
"error_text", success ? null : S7Client.ErrorText(errorCode)
);
}
/**
* short 2 S7 Word
*/
static byte[] encodeWord(short value) {
byte[] data = new byte[PlcConfig.WORD_BYTE_LENGTH];
// S7 的 Word/Int 都是 2 字节大端格式;当前值范围都在正数区间内。
S7.SetShortAt(data, 0, value);
return data;
}
/**
* S7 2 short
*/
static short decodeWord(byte[] data, int offset) {
return (short) S7.GetShortAt(data, offset);
}

@ -0,0 +1,42 @@
package com.example.pdaplccontrol;
import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import Moka7.S7;
public class PdaPlcApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
DiagnosticLogger.initialize(this);
DiagnosticLogger.log(
"app_start",
"app_version", applicationVersion(),
"android_version", Build.VERSION.RELEASE,
"device_model", Build.MANUFACTURER + " " + Build.MODEL,
"plc_ip", PlcConfig.PLC_IP,
"rack", PlcConfig.RACK,
"slot", PlcConfig.SLOT,
"db_number", PlcConfig.DB_NUMBER,
"connection_type", "S7.OP",
"connection_type_code", S7.OP,
"moka7_receive_timeout_ms", 2000,
"monitor_interval_ms", PlcConfig.MONITOR_INTERVAL_MILLIS,
"monitor_failure_threshold", PlcConfig.MONITOR_FAILURE_THRESHOLD,
"monitor_stale_threshold_ms", PlcConfig.MONITOR_STALE_THRESHOLD_MILLIS
);
}
private String applicationVersion() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
return packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
return "UNKNOWN";
}
}
}

@ -2,11 +2,37 @@ package com.example.pdaplccontrol;
public class PlcCommunicationException extends Exception {
private final String phase;
private final Integer errorCode;
public PlcCommunicationException(String message) {
super(message);
this(message, "UNKNOWN", null, null);
}
public PlcCommunicationException(String message, Throwable cause) {
this(message, "UNKNOWN", null, cause);
}
public PlcCommunicationException(String message, String phase, Integer errorCode) {
this(message, phase, errorCode, null);
}
public PlcCommunicationException(
String message,
String phase,
Integer errorCode,
Throwable cause
) {
super(message, cause);
this.phase = phase;
this.errorCode = errorCode;
}
public String getPhase() {
return phase;
}
public Integer getErrorCode() {
return errorCode;
}
}

@ -12,7 +12,25 @@ final class PlcConfig {
static final int WORD_BYTE_LENGTH = 2;
static final int MONITOR_START_OFFSET = LINE_NUMBER_OFFSET;
static final int MONITOR_BYTE_LENGTH = WEIGHT_KG_OFFSET + WORD_BYTE_LENGTH - MONITOR_START_OFFSET;
static final int MONITOR_INTERVAL_MILLIS = 2000;
// 监控常规轮询间隔5000毫秒。设为 5 秒是为了降低 PDA 与 PLC 的网络交互负荷。
static final int MONITOR_INTERVAL_MILLIS = 5000;
// 监控发生第一次读取失败时的退避重试延迟2000毫秒。初次失败快速重试以规避偶发抖动。
static final int MONITOR_FIRST_FAILURE_RETRY_MILLIS = 2000;
// 监控发生第二次读取失败时的退避重试延迟3000毫秒
static final int MONITOR_SECOND_FAILURE_RETRY_MILLIS = 3000;
// 监控发生多次连续失败时的最大退避延迟5000毫秒。防止网络彻底断开时高频请求占用设备连接资源。
static final int MONITOR_MAX_FAILURE_RETRY_MILLIS = 5000;
// 判定通讯异常的连续失败次数阈值。达到 3 次失败才报错,避免网络偶发丢包导致界面闪红。
static final int MONITOR_FAILURE_THRESHOLD = 3;
// 判定通讯数据失效的兜底时间阈值15秒。用于防止由于其他原因导致的监控界面数据僵死、长期未刷新的情况。
static final long MONITOR_STALE_THRESHOLD_MILLIS = 15000L;
static final int MIN_LINE_NUMBER = 10;
static final int MAX_LINE_NUMBER = 18;
static final int MIN_WEIGHT_KG = 0;

@ -0,0 +1,42 @@
package com.example.pdaplccontrol;
import java.util.Locale;
import Moka7.S7Client;
final class PlcErrorClassifier {
private PlcErrorClassifier() {
}
static String category(int errorCode) {
switch (errorCode) {
case S7Client.errTCPConnectionFailed:
return "TCP_CONNECTION_FAILED";
case S7Client.errTCPDataSend:
return "TCP_SEND_FAILED";
case S7Client.errTCPDataRecv:
return "TCP_RECEIVE_FAILED";
case S7Client.errTCPDataRecvTout:
return "TCP_RECEIVE_TIMEOUT";
case S7Client.errTCPConnectionReset:
return "TCP_CONNECTION_RESET";
case S7Client.errISOInvalidPDU:
case S7Client.errISOConnectionFailed:
case S7Client.errISONegotiatingPDU:
return "ISO_HANDSHAKE_FAILED";
case S7Client.errS7InvalidPDU:
return "S7_INVALID_PDU";
case S7Client.errS7DataRead:
return "S7_READ_FAILED";
case S7Client.errS7DataWrite:
return "S7_WRITE_FAILED";
default:
return "UNKNOWN_MOKA7_ERROR";
}
}
static String errorHex(int errorCode) {
return String.format(Locale.US, "0x%04X", errorCode);
}
}

@ -6,11 +6,23 @@ import android.os.Looper;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* PLC
*
* 1.
* 2. 使线SingleThreadExecutor
* 线线
* 线访 Moka7 S7 线
*/
public class PlcManager {
private static PlcManager instance;
private final PlcValueSender plcValueSender;
// 串行指令执行器(单线程池),所有的 PLC 通讯网络 IO 操作都在此执行
private final Executor commandExecutor;
// 主线程回调执行器,用于将结果推送回 UI 线程
private final Executor callbackExecutor;
private PlcManager() {
@ -40,6 +52,9 @@ public class PlcManager {
void onError(String error);
}
/**
* PLC 线
*/
public void sendLineNumberAsync(final short lineNumber, final PlcCallback callback) {
if (!isValidLineNumber(lineNumber)) {
notifyError(callback, "线体号无效,只允许 10-18");
@ -59,6 +74,9 @@ public class PlcManager {
});
}
/**
* PLC
*/
public void sendWeightKgAsync(final short weightKg, final PlcCallback callback) {
if (!isValidWeightKg(weightKg)) {
notifyError(callback, "重量范围为 0-30kg");
@ -79,6 +97,9 @@ public class PlcManager {
});
}
/**
* PLC
*/
public void readCurrentValuesAsync(final PlcCurrentValuesCallback callback) {
commandExecutor.execute(new Runnable() {
@Override
@ -92,6 +113,20 @@ public class PlcManager {
});
}
/**
* PLC
* IO线 Executor 线
*
*/
public void disconnectAsync() {
commandExecutor.execute(new Runnable() {
@Override
public void run() {
plcValueSender.disconnect();
}
});
}
static boolean isValidLineNumber(short lineNumber) {
return lineNumber >= PlcConfig.MIN_LINE_NUMBER && lineNumber <= PlcConfig.MAX_LINE_NUMBER;
}
@ -148,6 +183,9 @@ public class PlcManager {
});
}
/**
* Android 线
*/
private static class MainThreadExecutor implements Executor {
private final Handler mainHandler = new Handler(Looper.getMainLooper());

@ -0,0 +1,49 @@
package com.example.pdaplccontrol;
/**
* PLC
* UI 便
*/
final class PlcMonitorPolicy {
private PlcMonitorPolicy() {
}
/**
*
*
* 1. 使5 PLC
* 2. 2/3
* 3. 5 PDA
*/
static int nextDelayMillis(int consecutiveFailures) {
if (consecutiveFailures <= 0) {
return PlcConfig.MONITOR_INTERVAL_MILLIS;
}
if (consecutiveFailures == 1) {
return PlcConfig.MONITOR_FIRST_FAILURE_RETRY_MILLIS;
}
if (consecutiveFailures == 2) {
return PlcConfig.MONITOR_SECOND_FAILURE_RETRY_MILLIS;
}
return PlcConfig.MONITOR_MAX_FAILURE_RETRY_MILLIS;
}
/**
* PLC
*
* 1.
* 2. 33 15
*/
static boolean isCommunicationAbnormal(
int consecutiveFailures,
boolean hasSuccessfulRead,
long millisSinceLastSuccess
) {
if (consecutiveFailures >= PlcConfig.MONITOR_FAILURE_THRESHOLD) {
return true;
}
// 即使连续失败次数未达阈值,若距离上一次成功读取的时间过长,亦判定为数据过期/异常,以防僵死状态悬挂
return hasSuccessfulRead && millisSinceLastSuccess >= PlcConfig.MONITOR_STALE_THRESHOLD_MILLIS;
}
}

@ -16,4 +16,11 @@ public interface PlcValueSender {
* PLC
*/
PlcCurrentValues readCurrentValues() throws PlcCommunicationException;
/**
* PLC
* PDA 退
* TCP PLC PLC 8
*/
void disconnect();
}

@ -44,8 +44,8 @@
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:gravity="center"
android:text="正在读取PLC..."
android:textColor="@color/monitor_status_pending"
android:text="监控中"
android:textColor="@color/monitor_status_normal"
android:textSize="16sp" />
<TextView

@ -6,5 +6,4 @@
<color name="line_monitor_active_text">#FFFFFFFF</color>
<color name="monitor_status_normal">#2E7D32</color>
<color name="monitor_status_stale">#C62828</color>
<color name="monitor_status_pending">#616161</color>
</resources>

@ -0,0 +1,77 @@
package com.example.pdaplccontrol;
import org.junit.Test;
import java.util.LinkedHashMap;
import java.util.Map;
import Moka7.S7Client;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class DiagnosticSupportTest {
@Test
public void errorClassifier_mapsMoka7Errors() {
assertEquals("TCP_CONNECTION_FAILED",
PlcErrorClassifier.category(S7Client.errTCPConnectionFailed));
assertEquals("TCP_RECEIVE_TIMEOUT",
PlcErrorClassifier.category(S7Client.errTCPDataRecvTout));
assertEquals("ISO_HANDSHAKE_FAILED",
PlcErrorClassifier.category(S7Client.errISOConnectionFailed));
assertEquals("S7_READ_FAILED",
PlcErrorClassifier.category(S7Client.errS7DataRead));
assertEquals("S7_WRITE_FAILED",
PlcErrorClassifier.category(S7Client.errS7DataWrite));
assertEquals("UNKNOWN_MOKA7_ERROR", PlcErrorClassifier.category(0x1234));
assertEquals("0x0004", PlcErrorClassifier.errorHex(S7Client.errTCPDataRecvTout));
}
@Test
public void diagnosticJson_serializesAndEscapesValues() {
Map<String, Object> values = new LinkedHashMap<>();
values.put("event", "read\nfailure");
values.put("error_code", 4);
values.put("connected", false);
values.put("optional", null);
assertEquals(
"{\"event\":\"read\\nfailure\",\"error_code\":4,"
+ "\"connected\":false,\"optional\":null}",
DiagnosticJson.toJson(values)
);
}
@Test
public void filePolicy_usesOneFilePerSessionAndNumberedParts() {
assertEquals(
"PdaPlc-diagnostics-20260608-143025.jsonl",
DiagnosticLogFilePolicy.fileName("20260608-143025", 1)
);
assertEquals(
"PdaPlc-diagnostics-20260608-143025-part-2.jsonl",
DiagnosticLogFilePolicy.fileName("20260608-143025", 2)
);
}
@Test
public void filePolicy_rotatesOnlyWhenCurrentFileHasContentAndWouldExceedLimit() {
assertFalse(DiagnosticLogFilePolicy.shouldRotate(0, Integer.MAX_VALUE));
assertFalse(DiagnosticLogFilePolicy.shouldRotate(
DiagnosticLogFilePolicy.MAX_FILE_BYTES - 100,
100
));
assertTrue(DiagnosticLogFilePolicy.shouldRotate(
DiagnosticLogFilePolicy.MAX_FILE_BYTES - 100,
101
));
}
@Test
public void filePolicy_keepsNineExistingFilesBeforeCreatingNextFile() {
assertFalse(DiagnosticLogFilePolicy.shouldDeleteBeforeCreating(8));
assertTrue(DiagnosticLogFilePolicy.shouldDeleteBeforeCreating(9));
}
}

@ -43,6 +43,39 @@ public class ExampleUnitTest {
assertEquals("12", WeightInputParser.kgToDisplayText((short) 12));
}
@Test
public void monitorPolicy_successUsesNormalInterval() {
assertEquals(PlcConfig.MONITOR_INTERVAL_MILLIS, PlcMonitorPolicy.nextDelayMillis(0));
}
@Test
public void monitorPolicy_failuresUseLightweightBackoff() {
assertEquals(PlcConfig.MONITOR_FIRST_FAILURE_RETRY_MILLIS, PlcMonitorPolicy.nextDelayMillis(1));
assertEquals(PlcConfig.MONITOR_SECOND_FAILURE_RETRY_MILLIS, PlcMonitorPolicy.nextDelayMillis(2));
assertEquals(PlcConfig.MONITOR_MAX_FAILURE_RETRY_MILLIS, PlcMonitorPolicy.nextDelayMillis(3));
assertEquals(PlcConfig.MONITOR_MAX_FAILURE_RETRY_MILLIS, PlcMonitorPolicy.nextDelayMillis(4));
}
@Test
public void monitorPolicy_firstTwoFailuresRemainNormal() {
assertFalse(PlcMonitorPolicy.isCommunicationAbnormal(1, true, 1000L));
assertFalse(PlcMonitorPolicy.isCommunicationAbnormal(2, true, 1000L));
}
@Test
public void monitorPolicy_thirdFailureIsAbnormal() {
assertTrue(PlcMonitorPolicy.isCommunicationAbnormal(3, true, 1000L));
}
@Test
public void monitorPolicy_staleThresholdIsAbnormalAfterSuccess() {
assertTrue(PlcMonitorPolicy.isCommunicationAbnormal(
1,
true,
PlcConfig.MONITOR_STALE_THRESHOLD_MILLIS
));
}
private void assertInvalidWeight(String input) {
try {
WeightInputParser.parseKg(input);

@ -82,6 +82,16 @@ public class PlcManagerTest {
assertNull(callback.errorMessage);
}
@Test
public void disconnectAsync_delegatesDisconnect() {
RecordingPlcValueSender sender = new RecordingPlcValueSender();
PlcManager manager = new PlcManager(sender, new DirectExecutor(), new DirectExecutor());
manager.disconnectAsync();
assertTrue(sender.disconnected);
}
private static class DirectExecutor implements Executor {
@Override
public void execute(Runnable command) {
@ -93,6 +103,7 @@ public class PlcManagerTest {
private Short lineNumber;
private Short weightKg;
private PlcCurrentValues currentValues = new PlcCurrentValues((short) 10, (short) 0);
private boolean disconnected;
@Override
public void sendLineNumber(short lineNumber) {
@ -108,6 +119,11 @@ public class PlcManagerTest {
public PlcCurrentValues readCurrentValues() {
return currentValues;
}
@Override
public void disconnect() {
disconnected = true;
}
}
private static class RecordingCallback implements PlcManager.PlcCallback {

Loading…
Cancel
Save