添加读取监控

main
yangk 3 weeks ago
parent 4dc308f5e5
commit 56fdf39d8e

@ -1,11 +1,15 @@
package com.example.pdaplccontrol;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
@ -14,18 +18,45 @@ import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private final Button[] lineButtons = new Button[9];
private final Handler monitorHandler = new Handler(Looper.getMainLooper());
private final Runnable monitorRunnable = new Runnable() {
@Override
public void run() {
readCurrentValues();
}
};
private EditText editWeightKg;
private TextView textMonitorStatus;
private LinearLayout rootLayout;
private boolean monitorRunning;
private boolean monitorRequestInFlight;
private ColorStateList defaultLineButtonBackgroundTint;
private ColorStateList defaultLineButtonTextColors;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootLayout = findViewById(R.id.rootLayout);
editWeightKg = findViewById(R.id.editWeightKg);
textMonitorStatus = findViewById(R.id.textMonitorStatus);
setupWeightInput();
setupLineButtons();
}
@Override
protected void onStart() {
super.onStart();
startMonitor();
}
@Override
protected void onStop() {
stopMonitor();
super.onStop();
}
private void setupWeightInput() {
editWeightKg.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
@ -35,6 +66,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
&& event.getAction() == KeyEvent.ACTION_UP
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER);
if (isDone) {
rootLayout.requestFocus();
sendWeight();
return true;
}
@ -58,6 +90,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
lineButtons[i].setTag((short) (PlcConfig.MIN_LINE_NUMBER + i));
lineButtons[i].setOnClickListener(this);
}
defaultLineButtonBackgroundTint = lineButtons[0].getBackgroundTintList();
defaultLineButtonTextColors = lineButtons[0].getTextColors();
}
@Override
@ -73,18 +107,19 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
private void sendWeight() {
final short weightGrams;
final short weightKg;
try {
weightGrams = WeightInputParser.kgToGrams(editWeightKg.getText().toString());
weightKg = WeightInputParser.parseKg(editWeightKg.getText().toString());
} catch (IllegalArgumentException e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
return;
}
setControlsEnabled(false);
Toast.makeText(this, "正在向 PLC 发送重量 " + weightGrams + "g...", Toast.LENGTH_SHORT).show();
Toast.makeText(this, "正在向 PLC 发送重量 "
+ WeightInputParser.kgToDisplayText(weightKg) + "kg...", Toast.LENGTH_SHORT).show();
PlcManager.getInstance().sendWeightGramsAsync(weightGrams, createCallback());
PlcManager.getInstance().sendWeightKgAsync(weightKg, createCallback());
}
private PlcManager.PlcCallback createCallback() {
@ -111,4 +146,99 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
}
}
private void startMonitor() {
monitorRunning = true;
monitorHandler.removeCallbacks(monitorRunnable);
renderMonitorPending();
monitorHandler.post(monitorRunnable);
}
private void stopMonitor() {
monitorRunning = false;
monitorHandler.removeCallbacks(monitorRunnable);
}
private void readCurrentValues() {
if (!monitorRunning || monitorRequestInFlight) {
return;
}
monitorRequestInFlight = true;
PlcManager.getInstance().readCurrentValuesAsync(new PlcManager.PlcCurrentValuesCallback() {
@Override
public void onSuccess(PlcCurrentValues values) {
monitorRequestInFlight = false;
if (monitorRunning) {
renderCurrentValues(values);
}
scheduleNextMonitorRead();
}
@Override
public void onError(String error) {
monitorRequestInFlight = false;
if (monitorRunning) {
renderMonitorStale();
}
scheduleNextMonitorRead();
}
});
}
private void scheduleNextMonitorRead() {
if (monitorRunning) {
monitorHandler.postDelayed(monitorRunnable, PlcConfig.MONITOR_INTERVAL_MILLIS);
}
}
private void renderCurrentValues(PlcCurrentValues values) {
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.setTextColor(getColor(R.color.monitor_status_normal));
}
private void renderMonitorStale() {
textMonitorStatus.setText("已失联/数据过期");
textMonitorStatus.setTextColor(getColor(R.color.monitor_status_stale));
clearCurrentLineNumberHighlight();
}
private void renderCurrentLineNumber(short currentLineNumber) {
for (Button btn : lineButtons) {
if (btn == null) {
continue;
}
short buttonLineNumber = (short) btn.getTag();
if (buttonLineNumber == currentLineNumber) {
btn.setBackgroundTintList(ColorStateList.valueOf(getColor(R.color.line_monitor_active)));
btn.setTextColor(getColor(R.color.line_monitor_active_text));
} else {
btn.setBackgroundTintList(defaultLineButtonBackgroundTint);
btn.setTextColor(defaultLineButtonTextColors);
}
}
}
private void clearCurrentLineNumberHighlight() {
for (Button btn : lineButtons) {
if (btn != null) {
btn.setBackgroundTintList(defaultLineButtonBackgroundTint);
btn.setTextColor(defaultLineButtonTextColors);
}
}
}
}

@ -11,18 +11,23 @@ class Moka7PlcValueSender implements PlcValueSender {
}
@Override
public void sendWeightGrams(short weightGrams) throws PlcCommunicationException {
writeWord(PlcConfig.WEIGHT_GRAMS_OFFSET, weightGrams);
public void sendWeightKg(short weightKg) throws PlcCommunicationException {
writeWord(PlcConfig.WEIGHT_KG_OFFSET, weightKg);
}
@Override
public PlcCurrentValues readCurrentValues() throws PlcCommunicationException {
byte[] data = readBytes(PlcConfig.MONITOR_START_OFFSET, PlcConfig.MONITOR_BYTE_LENGTH);
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 {
client.SetConnectionType(S7.OP);
int connectRes = client.ConnectTo(PlcConfig.PLC_IP, PlcConfig.RACK, PlcConfig.SLOT);
if (connectRes != 0) {
throw new PlcCommunicationException("PLC连接失败错误码" + connectRes);
}
connect(client);
int writeRes = client.WriteArea(
S7.S7AreaDB,
@ -43,10 +48,42 @@ class Moka7PlcValueSender implements PlcValueSender {
}
}
private byte[] readBytes(int offset, int amount) throws PlcCommunicationException {
S7Client client = new S7Client();
try {
connect(client);
byte[] data = new byte[amount];
int readRes = client.ReadArea(S7.S7AreaDB, PlcConfig.DB_NUMBER, offset, amount, data);
if (readRes != 0) {
throw new PlcCommunicationException("读取数据失败,错误码:" + S7Client.ErrorText(readRes));
}
return data;
} catch (PlcCommunicationException e) {
throw e;
} catch (Exception e) {
throw new PlcCommunicationException("通讯发生异常: " + e.getMessage(), e);
} finally {
client.Disconnect();
}
}
private void connect(S7Client client) throws PlcCommunicationException {
client.SetConnectionType(S7.OP);
int connectRes = client.ConnectTo(PlcConfig.PLC_IP, PlcConfig.RACK, PlcConfig.SLOT);
if (connectRes != 0) {
throw new PlcCommunicationException("PLC连接失败错误码" + connectRes);
}
}
static byte[] encodeWord(short value) {
byte[] data = new byte[PlcConfig.WORD_BYTE_LENGTH];
// S7 的 Word/Int 都是 2 字节大端格式;当前值范围都在正数区间内。
S7.SetShortAt(data, 0, value);
return data;
}
static short decodeWord(byte[] data, int offset) {
return (short) S7.GetShortAt(data, offset);
}
}

@ -8,12 +8,15 @@ final class PlcConfig {
static final int SLOT = 1;
static final int DB_NUMBER = 49;
static final int LINE_NUMBER_OFFSET = 32;
static final int WEIGHT_GRAMS_OFFSET = 34;
static final int WEIGHT_KG_OFFSET = 34;
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;
static final int MIN_LINE_NUMBER = 10;
static final int MAX_LINE_NUMBER = 18;
static final int MIN_WEIGHT_GRAMS = 0;
static final int MAX_WEIGHT_GRAMS = 30000;
static final int MIN_WEIGHT_KG = 0;
static final int MAX_WEIGHT_KG = 30;
private PlcConfig() {
}

@ -0,0 +1,20 @@
package com.example.pdaplccontrol;
final class PlcCurrentValues {
private final short lineNumber;
private final short weightKg;
PlcCurrentValues(short lineNumber, short weightKg) {
this.lineNumber = lineNumber;
this.weightKg = weightKg;
}
short getLineNumber() {
return lineNumber;
}
short getWeightKg() {
return weightKg;
}
}

@ -35,6 +35,11 @@ public class PlcManager {
void onError(String error);
}
public interface PlcCurrentValuesCallback {
void onSuccess(PlcCurrentValues values);
void onError(String error);
}
public void sendLineNumberAsync(final short lineNumber, final PlcCallback callback) {
if (!isValidLineNumber(lineNumber)) {
notifyError(callback, "线体号无效,只允许 10-18");
@ -54,8 +59,8 @@ public class PlcManager {
});
}
public void sendWeightGramsAsync(final short weightGrams, final PlcCallback callback) {
if (!isValidWeightGrams(weightGrams)) {
public void sendWeightKgAsync(final short weightKg, final PlcCallback callback) {
if (!isValidWeightKg(weightKg)) {
notifyError(callback, "重量范围为 0-30kg");
return;
}
@ -64,8 +69,9 @@ public class PlcManager {
@Override
public void run() {
try {
plcValueSender.sendWeightGrams(weightGrams);
notifySuccess(callback, "成功下发重量: " + weightGrams + "g");
plcValueSender.sendWeightKg(weightKg);
notifySuccess(callback, "成功下发重量: "
+ WeightInputParser.kgToDisplayText(weightKg) + "kg");
} catch (PlcCommunicationException e) {
notifyError(callback, e.getMessage());
}
@ -73,12 +79,26 @@ public class PlcManager {
});
}
public void readCurrentValuesAsync(final PlcCurrentValuesCallback callback) {
commandExecutor.execute(new Runnable() {
@Override
public void run() {
try {
notifyCurrentValuesSuccess(callback, plcValueSender.readCurrentValues());
} catch (PlcCommunicationException e) {
notifyCurrentValuesError(callback, e.getMessage());
}
}
});
}
static boolean isValidLineNumber(short lineNumber) {
return lineNumber >= PlcConfig.MIN_LINE_NUMBER && lineNumber <= PlcConfig.MAX_LINE_NUMBER;
}
static boolean isValidWeightGrams(short weightGrams) {
return weightGrams >= PlcConfig.MIN_WEIGHT_GRAMS && weightGrams <= PlcConfig.MAX_WEIGHT_GRAMS;
static boolean isValidWeightKg(short weightKg) {
return weightKg >= PlcConfig.MIN_WEIGHT_KG
&& weightKg <= PlcConfig.MAX_WEIGHT_KG;
}
private void notifySuccess(final PlcCallback callback, final String message) {
@ -103,6 +123,31 @@ public class PlcManager {
});
}
private void notifyCurrentValuesSuccess(
final PlcCurrentValuesCallback callback,
final PlcCurrentValues values
) {
callbackExecutor.execute(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onSuccess(values);
}
}
});
}
private void notifyCurrentValuesError(final PlcCurrentValuesCallback callback, final String error) {
callbackExecutor.execute(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onError(error);
}
}
});
}
private static class MainThreadExecutor implements Executor {
private final Handler mainHandler = new Handler(Looper.getMainLooper());

@ -8,7 +8,12 @@ public interface PlcValueSender {
void sendLineNumber(short lineNumber) throws PlcCommunicationException;
/**
* PLC
* PLC kg
*/
void sendWeightGrams(short weightGrams) throws PlcCommunicationException;
void sendWeightKg(short weightKg) throws PlcCommunicationException;
/**
* PLC
*/
PlcCurrentValues readCurrentValues() throws PlcCommunicationException;
}

@ -1,36 +1,30 @@
package com.example.pdaplccontrol;
import java.math.BigDecimal;
final class WeightInputParser {
private static final BigDecimal GRAMS_PER_KG = new BigDecimal("1000");
private WeightInputParser() {
}
static short kgToGrams(String input) {
static short parseKg(String input) {
if (input == null || input.trim().isEmpty()) {
throw new IllegalArgumentException("请输入重量");
}
BigDecimal kg;
int kg;
try {
kg = new BigDecimal(input.trim());
kg = Integer.parseInt(input.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("重量格式不正确");
throw new IllegalArgumentException("重量必须为整数kg");
}
BigDecimal grams = kg.movePointRight(3).stripTrailingZeros();
if (kg.compareTo(BigDecimal.ZERO) < 0
|| grams.compareTo(BigDecimal.valueOf(PlcConfig.MAX_WEIGHT_GRAMS)) > 0) {
if (kg < PlcConfig.MIN_WEIGHT_KG || kg > PlcConfig.MAX_WEIGHT_KG) {
throw new IllegalArgumentException("重量范围为 0-30kg");
}
try {
return grams.shortValueExact();
} catch (ArithmeticException e) {
throw new IllegalArgumentException("重量最多精确到 0.001kg");
}
return (short) kg;
}
static String kgToDisplayText(short weightKg) {
return String.valueOf(weightKg);
}
}

@ -1,11 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:gravity="center"
android:text="西线重投工位产品信息输入"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:layout_width="220dp"
android:layout_height="wrap_content"
@ -19,13 +31,23 @@
android:id="@+id/editWeightKg"
android:layout_width="220dp"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:hint="0.000"
android:layout_marginBottom="8dp"
android:hint="0"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:inputType="number"
android:singleLine="true"
android:textSize="20sp" />
<TextView
android:id="@+id/textMonitorStatus"
android:layout_width="220dp"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:gravity="center"
android:text="正在读取PLC..."
android:textColor="@color/monitor_status_pending"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -2,4 +2,9 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
<color name="line_monitor_active">#2E7D32</color>
<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>

@ -20,25 +20,32 @@ public class ExampleUnitTest {
}
@Test
public void kgToGrams_acceptsZeroToThirtyKgWithGramPrecision() {
assertEquals(0, WeightInputParser.kgToGrams("0"));
assertEquals(1000, WeightInputParser.kgToGrams("1"));
assertEquals(12345, WeightInputParser.kgToGrams("12.345"));
assertEquals(30000, WeightInputParser.kgToGrams("30"));
public void parseKg_acceptsZeroToThirtyIntegerKg() {
assertEquals(0, WeightInputParser.parseKg("0"));
assertEquals(1, WeightInputParser.parseKg("1"));
assertEquals(12, WeightInputParser.parseKg("12"));
assertEquals(30, WeightInputParser.parseKg("30"));
}
@Test
public void kgToGrams_rejectsInvalidInput() {
public void parseKg_rejectsInvalidInput() {
assertInvalidWeight("");
assertInvalidWeight("abc");
assertInvalidWeight("-0.001");
assertInvalidWeight("30.001");
assertInvalidWeight("1.2345");
assertInvalidWeight("-1");
assertInvalidWeight("31");
assertInvalidWeight("1.12");
}
@Test
public void kgToDisplayText_showsIntegerKg() {
assertEquals("0", WeightInputParser.kgToDisplayText((short) 0));
assertEquals("1", WeightInputParser.kgToDisplayText((short) 1));
assertEquals("12", WeightInputParser.kgToDisplayText((short) 12));
}
private void assertInvalidWeight(String input) {
try {
WeightInputParser.kgToGrams(input);
WeightInputParser.parseKg(input);
fail("Expected invalid weight for input: " + input);
} catch (IllegalArgumentException expected) {
// 测试只关心非法输入会被拦截,具体提示由业务层统一维护。

@ -19,4 +19,12 @@ public class Moka7PlcValueSenderTest {
assertEquals(value, (short) S7.GetShortAt(data, 0));
}
}
@Test
public void decodeWord_validPositiveValues_readsS7BigEndianBytes() {
short value = 12;
byte[] data = Moka7PlcValueSender.encodeWord(value);
assertEquals(value, Moka7PlcValueSender.decodeWord(data, 0));
}
}

@ -19,7 +19,7 @@ public class PlcManagerTest {
assertNotNull(sender.lineNumber);
assertEquals((int) lineNumber, (int) sender.lineNumber);
assertNull(sender.weightGrams);
assertNull(sender.weightKg);
assertEquals("成功下发线体号: " + lineNumber, callback.successMessage);
assertNull(callback.errorMessage);
}
@ -34,39 +34,54 @@ public class PlcManagerTest {
manager.sendLineNumberAsync((short) 9, callback);
assertNull(sender.lineNumber);
assertNull(sender.weightGrams);
assertNull(sender.weightKg);
assertNull(callback.successMessage);
assertEquals("线体号无效,只允许 10-18", callback.errorMessage);
}
@Test
public void sendWeightGramsAsync_validWeight_delegatesExactGrams() {
public void sendWeightKgAsync_validWeight_delegatesExactKg() {
RecordingPlcValueSender sender = new RecordingPlcValueSender();
RecordingCallback callback = new RecordingCallback();
PlcManager manager = new PlcManager(sender, new DirectExecutor(), new DirectExecutor());
manager.sendWeightGramsAsync((short) 12345, callback);
manager.sendWeightKgAsync((short) 12, callback);
assertNull(sender.lineNumber);
assertEquals(12345, (int) sender.weightGrams);
assertEquals("成功下发重量: 12345g", callback.successMessage);
assertEquals(12, (int) sender.weightKg);
assertEquals("成功下发重量: 12kg", callback.successMessage);
assertNull(callback.errorMessage);
}
@Test
public void sendWeightGramsAsync_invalidWeight_doesNotCallSender() {
public void sendWeightKgAsync_invalidWeight_doesNotCallSender() {
RecordingPlcValueSender sender = new RecordingPlcValueSender();
RecordingCallback callback = new RecordingCallback();
PlcManager manager = new PlcManager(sender, new DirectExecutor(), new DirectExecutor());
manager.sendWeightGramsAsync((short) -1, callback);
manager.sendWeightKgAsync((short) -1, callback);
assertNull(sender.lineNumber);
assertNull(sender.weightGrams);
assertNull(sender.weightKg);
assertNull(callback.successMessage);
assertEquals("重量范围为 0-30kg", callback.errorMessage);
}
@Test
public void readCurrentValuesAsync_delegatesCurrentValues() {
RecordingPlcValueSender sender = new RecordingPlcValueSender();
sender.currentValues = new PlcCurrentValues((short) 11, (short) 12);
RecordingCurrentValuesCallback callback = new RecordingCurrentValuesCallback();
PlcManager manager = new PlcManager(sender, new DirectExecutor(), new DirectExecutor());
manager.readCurrentValuesAsync(callback);
assertNotNull(callback.values);
assertEquals(11, callback.values.getLineNumber());
assertEquals(12, callback.values.getWeightKg());
assertNull(callback.errorMessage);
}
private static class DirectExecutor implements Executor {
@Override
public void execute(Runnable command) {
@ -76,7 +91,8 @@ public class PlcManagerTest {
private static class RecordingPlcValueSender implements PlcValueSender {
private Short lineNumber;
private Short weightGrams;
private Short weightKg;
private PlcCurrentValues currentValues = new PlcCurrentValues((short) 10, (short) 0);
@Override
public void sendLineNumber(short lineNumber) {
@ -84,8 +100,13 @@ public class PlcManagerTest {
}
@Override
public void sendWeightGrams(short weightGrams) {
this.weightGrams = weightGrams;
public void sendWeightKg(short weightKg) {
this.weightKg = weightKg;
}
@Override
public PlcCurrentValues readCurrentValues() {
return currentValues;
}
}
@ -103,4 +124,19 @@ public class PlcManagerTest {
errorMessage = error;
}
}
private static class RecordingCurrentValuesCallback implements PlcManager.PlcCurrentValuesCallback {
private PlcCurrentValues values;
private String errorMessage;
@Override
public void onSuccess(PlcCurrentValues values) {
this.values = values;
}
@Override
public void onError(String error) {
errorMessage = error;
}
}
}

Loading…
Cancel
Save