+ app
This commit is contained in:
parent
11677dfa2b
commit
3fad2f0b2d
|
|
@ -0,0 +1,26 @@
|
||||||
|
Summary: %{getenv:DESCRIPTION}
|
||||||
|
Name: %{getenv:PACKAGE}
|
||||||
|
Version: %{getenv:VERSION}
|
||||||
|
Release: %{getenv:BUILD_NUMBER}+svn%{getenv:SVN_REVISION}
|
||||||
|
URL: kirillius.ru
|
||||||
|
BuildArch: noarch
|
||||||
|
License: GPL
|
||||||
|
Group: System Environment/Libraries
|
||||||
|
Requires: java >= 10
|
||||||
|
|
||||||
|
%description
|
||||||
|
%{getenv:DESCRIPTION}
|
||||||
|
|
||||||
|
|
||||||
|
%build
|
||||||
|
cd %{buildroot}
|
||||||
|
mkdir -p usr/bin
|
||||||
|
|
||||||
|
cp %{getenv:WORKSPACE}/target/coolcfg usr/bin/coolcfg
|
||||||
|
chmod +x usr/bin/coolcfg
|
||||||
|
|
||||||
|
|
||||||
|
%files
|
||||||
|
%defattr(-,root,root)
|
||||||
|
/usr/bin
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/sh
|
||||||
|
java -jar "$0" "$@"
|
||||||
|
exit $?
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>ru.kirillius</groupId>
|
||||||
|
<artifactId>cooler.controller</artifactId>
|
||||||
|
<version>2.0.0.0</version>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>10</source>
|
||||||
|
<target>10</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>1.4</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>module-info.class</exclude>
|
||||||
|
<exclude>JDOMAbout*class</exclude>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
<transformers>
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<manifestEntries>
|
||||||
|
<Main-Class>ru.kirillius.cooler.controller.Application</Main-Class>
|
||||||
|
</manifestEntries>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
<shadedArtifactAttached>true
|
||||||
|
</shadedArtifactAttached> <!-- Make the shaded artifact not the main one -->
|
||||||
|
<shadedClassifierName>shaded</shadedClassifierName> <!-- set the suffix to the shaded jar -->
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<!-- This calls launch4j to create the program EXE -->
|
||||||
|
<groupId>com.akathist.maven.plugins.launch4j</groupId>
|
||||||
|
<artifactId>launch4j-maven-plugin</artifactId>
|
||||||
|
<version>2.1.2</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>l4j-wrapper</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>launch4j</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<headerType>gui</headerType>
|
||||||
|
<downloadUrl>https://aws.amazon.com/ru/corretto/</downloadUrl>
|
||||||
|
<outfile>target/coolcfg.exe</outfile>
|
||||||
|
<jar>
|
||||||
|
${project.build.directory}/${project.artifactId}-${project.version}-shaded.jar
|
||||||
|
</jar>
|
||||||
|
<errTitle/>
|
||||||
|
<classPath>
|
||||||
|
<mainClass>ru.kirillius.cooler.controller.Application</mainClass>
|
||||||
|
<addDependencies>false</addDependencies>
|
||||||
|
<preCp>anything</preCp>
|
||||||
|
</classPath>
|
||||||
|
<icon>src/main/resources/icon.ico</icon>
|
||||||
|
<jre>
|
||||||
|
<path>./runtime</path>
|
||||||
|
<minVersion>10.0</minVersion>
|
||||||
|
<maxVersion/>
|
||||||
|
<opts>
|
||||||
|
<opt>-Dfile.encoding=UTF-8</opt>
|
||||||
|
</opts>
|
||||||
|
</jre>
|
||||||
|
|
||||||
|
<versionInfo>
|
||||||
|
<fileVersion>${project.version}</fileVersion>
|
||||||
|
<txtFileVersion>${project.version}</txtFileVersion>
|
||||||
|
<fileDescription>cooler controller configurator</fileDescription>
|
||||||
|
<copyright>copy left</copyright>
|
||||||
|
<productVersion>${project.version}</productVersion>
|
||||||
|
<txtProductVersion>${project.version}</txtProductVersion>
|
||||||
|
<productName>cooler controller configurator</productName>
|
||||||
|
<companyName>kirillius.ru</companyName>
|
||||||
|
<internalName>coolcfg</internalName>
|
||||||
|
<originalFilename>coolcfg.exe</originalFilename>
|
||||||
|
<language>ENGLISH_US</language>
|
||||||
|
</versionInfo>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>l4j-wrapper-console</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>launch4j</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<headerType>console</headerType>
|
||||||
|
<downloadUrl>https://aws.amazon.com/ru/corretto/</downloadUrl>
|
||||||
|
<outfile>target/coolcfg-debug.exe</outfile>
|
||||||
|
<jar>
|
||||||
|
${project.build.directory}/${project.artifactId}-${project.version}-shaded.jar
|
||||||
|
</jar>
|
||||||
|
<errTitle/>
|
||||||
|
<classPath>
|
||||||
|
<mainClass>ru.kirillius.cooler.controller.Application</mainClass>
|
||||||
|
<addDependencies>false</addDependencies>
|
||||||
|
<preCp>anything</preCp>
|
||||||
|
</classPath>
|
||||||
|
<icon>src/main/resources/icon.ico</icon>
|
||||||
|
<jre>
|
||||||
|
<path>./runtime</path>
|
||||||
|
<minVersion>10.0</minVersion>
|
||||||
|
<maxVersion/>
|
||||||
|
<opts>
|
||||||
|
<opt>-Dfile.encoding=UTF-8</opt>
|
||||||
|
<opt>-Dyabba=ICING</opt>
|
||||||
|
</opts>
|
||||||
|
</jre>
|
||||||
|
|
||||||
|
<versionInfo>
|
||||||
|
<fileVersion>${project.version}</fileVersion>
|
||||||
|
<txtFileVersion>${project.version}</txtFileVersion>
|
||||||
|
<fileDescription>cooler controller configurator</fileDescription>
|
||||||
|
<copyright>copy left</copyright>
|
||||||
|
<productVersion>${project.version}</productVersion>
|
||||||
|
<txtProductVersion>${project.version}</txtProductVersion>
|
||||||
|
<productName>cooler controller configurator</productName>
|
||||||
|
<companyName>kirillius.ru</companyName>
|
||||||
|
<internalName>coolcfg</internalName>
|
||||||
|
<originalFilename>coolcfg.exe</originalFilename>
|
||||||
|
<language>ENGLISH_US</language>
|
||||||
|
</versionInfo>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>10</maven.compiler.source>
|
||||||
|
<maven.compiler.target>10</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.fazecast/jSerialComm -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fazecast</groupId>
|
||||||
|
<artifactId>jSerialComm</artifactId>
|
||||||
|
<version>2.9.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.formdev</groupId>
|
||||||
|
<artifactId>flatlaf</artifactId>
|
||||||
|
<version>2.1</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.intellij/forms_rt -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.intellij</groupId>
|
||||||
|
<artifactId>forms_rt</artifactId>
|
||||||
|
<version>7.0.3</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
public enum ActionType {
|
||||||
|
SAVE(0x0),
|
||||||
|
PING(0x1),
|
||||||
|
REBOOT(0x2),
|
||||||
|
READ(0x3),
|
||||||
|
WRITE(0x4),
|
||||||
|
ERROR(0x5),
|
||||||
|
UNKNOWN(0x255);
|
||||||
|
|
||||||
|
private final byte value;
|
||||||
|
|
||||||
|
ActionType(int value) {
|
||||||
|
this.value = (byte) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ActionType getByValue(byte value) {
|
||||||
|
for (ActionType type : values()) {
|
||||||
|
if (type.value == value) return type;
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
public enum DataType {
|
||||||
|
NONE(0x0),
|
||||||
|
TEMP(0x1),
|
||||||
|
LEVEL(0x2),
|
||||||
|
START_TEMP(0x3),
|
||||||
|
STOP_TEMP(0x4),
|
||||||
|
MAX_TEMP(0x5),
|
||||||
|
MIN_LEVEL(0x6),
|
||||||
|
MAX_LEVEL(0x7),
|
||||||
|
UNKNOWN(0x255);
|
||||||
|
|
||||||
|
private final byte value;
|
||||||
|
|
||||||
|
DataType(int value) {
|
||||||
|
this.value = (byte) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DataType getByValue(byte value) {
|
||||||
|
for (DataType type : values()) {
|
||||||
|
if (type.value == value) return type;
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
public final class DummyMessageSystem extends MessageSystem {
|
||||||
|
private final Worker worker;
|
||||||
|
private final Map<DataType, Byte> values = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public DummyMessageSystem() throws MessageException {
|
||||||
|
super();
|
||||||
|
addDefaults();
|
||||||
|
worker = new Worker();
|
||||||
|
worker.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDefaults() {
|
||||||
|
values.put(DataType.TEMP, (byte) 0x20);
|
||||||
|
values.put(DataType.LEVEL, (byte) 128);
|
||||||
|
values.put(DataType.MIN_LEVEL, (byte) 10);
|
||||||
|
values.put(DataType.MAX_LEVEL, (byte) 255);
|
||||||
|
values.put(DataType.MAX_TEMP, (byte) 40);
|
||||||
|
values.put(DataType.START_TEMP, (byte) 25);
|
||||||
|
values.put(DataType.STOP_TEMP, (byte) 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(Message message) {
|
||||||
|
switch (message.getActionType()) {
|
||||||
|
case SAVE:
|
||||||
|
case PING:
|
||||||
|
case REBOOT:
|
||||||
|
worker.add(message);
|
||||||
|
break;
|
||||||
|
case READ:
|
||||||
|
if (values.containsKey(message.getDataType())) {
|
||||||
|
message.setValue(values.get(message.getDataType()));
|
||||||
|
worker.add(message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WRITE:
|
||||||
|
if (values.containsKey(message.getDataType())) {
|
||||||
|
values.put(message.getDataType(), message.getValue());
|
||||||
|
worker.add(message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void OnClose() {
|
||||||
|
worker.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Worker extends Thread {
|
||||||
|
private final Queue<Message> messageQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
@SuppressWarnings("BusyWait")
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int temp = 20;
|
||||||
|
int level = 0;
|
||||||
|
int ping = 0;
|
||||||
|
|
||||||
|
while (!interrupted()) {
|
||||||
|
if (ping++ % 5 == 0) {
|
||||||
|
Message m = new Message(ActionType.PING, DataType.NONE, (byte) 123);
|
||||||
|
m.validate();
|
||||||
|
OnMessageReceived(m);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!messageQueue.isEmpty()) {
|
||||||
|
OnMessageReceived(messageQueue.poll());
|
||||||
|
}
|
||||||
|
temp++;
|
||||||
|
level++;
|
||||||
|
if (temp >= 100) temp = 15;
|
||||||
|
if (level >= 256) level = 0;
|
||||||
|
|
||||||
|
values.put(DataType.LEVEL, (byte) (level & 0xFF));
|
||||||
|
values.put(DataType.TEMP, (byte) (temp & 0xFF));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(Message m) {
|
||||||
|
m.validate();
|
||||||
|
messageQueue.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
public enum ErrorCode {
|
||||||
|
ERR_UNSUPPORTED_OPERATION(0x0),
|
||||||
|
ERR_UNKNOWN_DATA_TYPE(0x1),
|
||||||
|
ERR_UNKNOWN_ACTION_TYPE(0x2),
|
||||||
|
ERR_INVALID_CHECKSUM(0x3),
|
||||||
|
UNKNOWN(0x255);
|
||||||
|
|
||||||
|
private final byte value;
|
||||||
|
|
||||||
|
ErrorCode(int value) {
|
||||||
|
this.value = (byte) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ErrorCode getByValue(byte value) {
|
||||||
|
for (var type : values()) {
|
||||||
|
if (type.value == value) return type;
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
public final class Message {
|
||||||
|
private ActionType actionType;
|
||||||
|
private DataType dataType;
|
||||||
|
private byte value;
|
||||||
|
private byte checksum;
|
||||||
|
|
||||||
|
public Message(byte[] bytes) {
|
||||||
|
if (bytes.length != 4) throw new RuntimeException("Invalid data size");
|
||||||
|
this.actionType = ActionType.getByValue(bytes[0]);
|
||||||
|
this.dataType = DataType.getByValue(bytes[1]);
|
||||||
|
this.value = bytes[2];
|
||||||
|
this.checksum = bytes[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message(ActionType actionType, DataType dataType, byte value) {
|
||||||
|
this.actionType = actionType;
|
||||||
|
this.dataType = dataType;
|
||||||
|
this.value = value;
|
||||||
|
checksum = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message(ActionType actionType, DataType dataType) {
|
||||||
|
this.actionType = actionType;
|
||||||
|
this.dataType = dataType;
|
||||||
|
this.value = 0;
|
||||||
|
checksum = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Message(ActionType type) {
|
||||||
|
this.actionType = type;
|
||||||
|
this.value = 0;
|
||||||
|
checksum = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataType getDataType() {
|
||||||
|
return dataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataType(DataType dataType) {
|
||||||
|
this.dataType = dataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Message{" +
|
||||||
|
"actionType=" + actionType +
|
||||||
|
", dataType=" + dataType +
|
||||||
|
", value=" + Byte.toUnsignedInt(value) +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionType getActionType() {
|
||||||
|
return actionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActionType(ActionType actionType) {
|
||||||
|
this.actionType = actionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(byte value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getChecksum() {
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChecksum(byte checksum) {
|
||||||
|
this.checksum = checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validate() {
|
||||||
|
checksum = calcChecksum();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return checksum == calcChecksum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte calcChecksum() {
|
||||||
|
return (byte) ((byte) 18 + (byte) value + actionType.getValue() * (byte) 23 + (byte) 42 * (byte) value + (byte) 5 * dataType.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toBytes() {
|
||||||
|
return new byte[]{actionType.getValue(), dataType.getValue(), value, checksum};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
public class MessageException extends Exception {
|
||||||
|
public MessageException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
import ru.kirillius.cooler.controller.Application;
|
||||||
|
import ru.kirillius.cooler.controller.EventSystem.EventHandler;
|
||||||
|
import ru.kirillius.cooler.controller.EventSystem.EventListener;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public abstract class MessageSystem {
|
||||||
|
private final Map<ActionType, Handler> subscribers = new ConcurrentHashMap<>();
|
||||||
|
private volatile long lastReceiveTime = 0;
|
||||||
|
|
||||||
|
protected volatile boolean alive;
|
||||||
|
|
||||||
|
public MessageSystem() {
|
||||||
|
if (Application.DEBUG) SubscribeDebug();
|
||||||
|
alive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void SubscribeDebug() {
|
||||||
|
addListener(ActionType.ERROR, eventData -> System.err.println("Remote error received: " + ErrorCode.getByValue(eventData.getValue())));
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void addListener(ActionType type, Listener listener) {
|
||||||
|
if (!subscribers.containsKey(type)) {
|
||||||
|
subscribers.put(type, new Handler());
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = subscribers.get(type);
|
||||||
|
handler.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void removeListener(ActionType type, Listener listener) {
|
||||||
|
if (!subscribers.containsKey(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = subscribers.get(type);
|
||||||
|
handler.removeListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void sendMessage(Message message);
|
||||||
|
|
||||||
|
public final long getLastReceiveTime() {
|
||||||
|
return lastReceiveTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void OnMessageReceived(Message message) {
|
||||||
|
lastReceiveTime = System.currentTimeMillis();
|
||||||
|
var handler = subscribers.get(message.getActionType());
|
||||||
|
if (handler != null) {
|
||||||
|
handler.invoke(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public final void close() throws MessageException {
|
||||||
|
for (var handler : subscribers.values()) {
|
||||||
|
handler.removeAllListeners();
|
||||||
|
}
|
||||||
|
subscribers.clear();
|
||||||
|
OnClose();
|
||||||
|
alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void OnClose() throws MessageException;
|
||||||
|
|
||||||
|
public final Operation setValue(DataType dataType, byte value) {
|
||||||
|
var op = new WriteOperation(dataType, value);
|
||||||
|
op.start();
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Operation getValue(DataType dataType) {
|
||||||
|
var op = new ReadOperation(dataType);
|
||||||
|
op.start();
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Operation sendCommand(ActionType type) {
|
||||||
|
if (type == ActionType.READ || type == ActionType.WRITE)
|
||||||
|
throw new UnsupportedOperationException("Action type " + type + " is unsupported");
|
||||||
|
var op = new CommandOperation(type);
|
||||||
|
op.start();
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener extends EventListener<Message> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static class Handler extends EventHandler<Listener, Message> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class AbstractOperation implements Operation {
|
||||||
|
private final static long Timeout = 10000L;
|
||||||
|
protected volatile boolean complete = false;
|
||||||
|
protected volatile boolean successful = false;
|
||||||
|
protected ActionType actionType = ActionType.UNKNOWN;
|
||||||
|
protected DataType dataType = DataType.NONE;
|
||||||
|
protected volatile byte value = 0x0;
|
||||||
|
protected Listener listener = null;
|
||||||
|
protected EventHandler<EventListener<Operation>, Operation> handler = new EventHandler<>();
|
||||||
|
private volatile Thread executor = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isComplete() {
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSuccessful() {
|
||||||
|
return successful;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
if (executor != null) throw new IllegalStateException("Operation is started already");
|
||||||
|
complete = false;
|
||||||
|
executor = new Thread(AbstractOperation.this::run);
|
||||||
|
executor.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void interrupt() {
|
||||||
|
if (executor != null) executor.interrupt();
|
||||||
|
executor = null;
|
||||||
|
successful = false;
|
||||||
|
complete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void waitForReturn() {
|
||||||
|
while (!complete && executor != null && executor.isAlive()) {
|
||||||
|
try {
|
||||||
|
//noinspection BusyWait
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void run() {
|
||||||
|
if (!alive) {
|
||||||
|
complete = true;
|
||||||
|
handler.invoke(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addListener(actionType, listener);
|
||||||
|
var start = System.currentTimeMillis();
|
||||||
|
do {
|
||||||
|
if (System.currentTimeMillis() - start > AbstractOperation.Timeout) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sendMessage(new Message(actionType, dataType, value));
|
||||||
|
try {
|
||||||
|
//noinspection BusyWait
|
||||||
|
Thread.sleep(1000L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} while (!successful);
|
||||||
|
removeListener(actionType, listener);
|
||||||
|
complete = true;
|
||||||
|
handler.invoke(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCompleteListener(EventListener<Operation> listener) {
|
||||||
|
handler.addListener(listener);
|
||||||
|
if (complete) listener.invoke(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ReadOperation extends AbstractOperation {
|
||||||
|
public ReadOperation(DataType dataType) {
|
||||||
|
actionType = ActionType.READ;
|
||||||
|
this.dataType = dataType;
|
||||||
|
listener = eventData -> {
|
||||||
|
if (eventData.getDataType() != dataType) return;
|
||||||
|
successful = true;
|
||||||
|
value = eventData.getValue();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class WriteOperation extends AbstractOperation {
|
||||||
|
public WriteOperation(DataType dataType, byte value) {
|
||||||
|
this.dataType = dataType;
|
||||||
|
actionType = ActionType.WRITE;
|
||||||
|
this.value = value;
|
||||||
|
listener = eventData -> successful = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CommandOperation extends AbstractOperation {
|
||||||
|
public CommandOperation(ActionType actionType) {
|
||||||
|
this.actionType = actionType;
|
||||||
|
listener = eventData -> {
|
||||||
|
if (eventData.getDataType() != dataType) return;
|
||||||
|
successful = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
import ru.kirillius.cooler.controller.EventSystem.EventListener;
|
||||||
|
|
||||||
|
public interface Operation {
|
||||||
|
|
||||||
|
boolean isComplete();
|
||||||
|
|
||||||
|
boolean isSuccessful();
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
void interrupt();
|
||||||
|
|
||||||
|
void waitForReturn();
|
||||||
|
|
||||||
|
byte getValue();
|
||||||
|
|
||||||
|
void addCompleteListener(EventListener<Operation> listener);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package ru.kirillius.cooler.controller.API;
|
||||||
|
|
||||||
|
import com.fazecast.jSerialComm.SerialPort;
|
||||||
|
import ru.kirillius.cooler.controller.Application;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public final class SerialMessageSystem extends MessageSystem {
|
||||||
|
private final SerialPort port;
|
||||||
|
private final InputStream inputStream;
|
||||||
|
private final OutputStream outputStream;
|
||||||
|
private final Thread readingThread;
|
||||||
|
|
||||||
|
|
||||||
|
public SerialMessageSystem(SerialPort port) throws MessageException {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.port = port;
|
||||||
|
if (!port.openPort()) {
|
||||||
|
throw new MessageException("Failed to open serial port");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
port.setBaudRate(9600);
|
||||||
|
|
||||||
|
inputStream = port.getInputStream();
|
||||||
|
outputStream = port.getOutputStream();
|
||||||
|
|
||||||
|
readingThread = new Thread(() -> {
|
||||||
|
int errors = 0;
|
||||||
|
|
||||||
|
while (!Thread.interrupted()) {
|
||||||
|
try {
|
||||||
|
if (inputStream.available() >= 4) {
|
||||||
|
Message message = readMessage();
|
||||||
|
if (message.isValid()) {
|
||||||
|
if (Application.DEBUG && message.getActionType() != ActionType.ERROR) {
|
||||||
|
System.out.println("<<< Message received: " + message);
|
||||||
|
}
|
||||||
|
OnMessageReceived(message);
|
||||||
|
} else {
|
||||||
|
System.err.println("Invalid message received: " + message);
|
||||||
|
errors++;
|
||||||
|
if (errors >= 10) {
|
||||||
|
errors = 0;
|
||||||
|
System.err.println("Too many errors. Cleaning stream buffer...");
|
||||||
|
while (inputStream.available() > 0) //noinspection ResultOfMethodCallIgnored
|
||||||
|
inputStream.read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (MessageException | IOException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
//noinspection BusyWait
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alive = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
readingThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public synchronized void sendMessage(Message message) {
|
||||||
|
message.validate();
|
||||||
|
if (Application.DEBUG) {
|
||||||
|
System.out.println(">>> Message sent: " + message);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
outputStream.write(message.toBytes());
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Failed to send message: " + message);
|
||||||
|
e.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Message readMessage() throws MessageException {
|
||||||
|
|
||||||
|
byte[] buffer = new byte[4];
|
||||||
|
try {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
inputStream.read(buffer, 0, 4);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new MessageException("Failed to read message", e);
|
||||||
|
}
|
||||||
|
return new Message(buffer);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void OnClose() {
|
||||||
|
readingThread.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
outputStream.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
port.closePort();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package ru.kirillius.cooler.controller;
|
||||||
|
|
||||||
|
import ru.kirillius.cooler.controller.CLI.ConsoleApp;
|
||||||
|
import ru.kirillius.cooler.controller.UI.MainForm;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class Application {
|
||||||
|
public final static boolean DEBUG;
|
||||||
|
public final static String title = "Cooler configurator";
|
||||||
|
public final static Properties properties = new Properties();
|
||||||
|
public final static boolean WINDOWS_HOST = System.getProperty("os.name").toLowerCase().contains("windows");
|
||||||
|
private final static File configFile;
|
||||||
|
|
||||||
|
static {
|
||||||
|
var props = System.getProperties();
|
||||||
|
DEBUG = props.containsKey("debug") && props.getProperty("debug").equals("true");
|
||||||
|
if (DEBUG) System.out.println("Debug mode is enabled");
|
||||||
|
|
||||||
|
File location = new File(".");
|
||||||
|
if (WINDOWS_HOST) try {
|
||||||
|
location = new File(Application.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getParentFile();
|
||||||
|
} catch (URISyntaxException ignored) {
|
||||||
|
}
|
||||||
|
if (!location.exists()) {
|
||||||
|
location = new File(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile = new File(location, "config.ini");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveConfig() throws IOException {
|
||||||
|
try (FileOutputStream stream = new FileOutputStream(configFile)) {
|
||||||
|
properties.store(stream, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
if (configFile.exists()) {
|
||||||
|
try (FileInputStream stream = new FileInputStream(configFile)) {
|
||||||
|
properties.load(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean gui = true;
|
||||||
|
for (String arg : args) {
|
||||||
|
if (arg.equals("--cli")) {
|
||||||
|
gui = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gui) {
|
||||||
|
MainForm mainForm = new MainForm();
|
||||||
|
mainForm.setVisible(true);
|
||||||
|
} else {
|
||||||
|
new ConsoleApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
public abstract class CallbackMenuItem<T> implements MenuItem<T> {
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
public CallbackMenuItem(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String render() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
import ru.kirillius.cooler.controller.API.*;
|
||||||
|
import ru.kirillius.cooler.controller.Application;
|
||||||
|
import ru.kirillius.cooler.controller.ExtMath;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
|
||||||
|
public final class ConsoleApp {
|
||||||
|
public final static BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
|
||||||
|
private final Menu<Boolean> mainMenu;
|
||||||
|
private volatile MessageSystem messageSystem = null;
|
||||||
|
|
||||||
|
public ConsoleApp() {
|
||||||
|
try {
|
||||||
|
selectPort(true);
|
||||||
|
|
||||||
|
} catch (MessageException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
mainMenu = new Menu<>("Main menu") {
|
||||||
|
@Override
|
||||||
|
protected Boolean OnSelect(int index, MenuItem<Boolean> item, Boolean value) {
|
||||||
|
if (!value) close();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reload();
|
||||||
|
//noinspection StatementWithEmptyBody
|
||||||
|
while (Boolean.TRUE.equals(mainMenu.show())) ;
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showError(Throwable t) {
|
||||||
|
t.printStackTrace(System.err);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reload() {
|
||||||
|
mainMenu.removeAll();
|
||||||
|
mainMenu.add(new CallbackMenuItem<>("Change port") {
|
||||||
|
@Override
|
||||||
|
public Boolean select() {
|
||||||
|
try {
|
||||||
|
selectPort(false);
|
||||||
|
reload();
|
||||||
|
} catch (MessageException e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainMenu.add(new CallbackMenuItem<>("Read stats") {
|
||||||
|
@Override
|
||||||
|
public Boolean select() {
|
||||||
|
Operation tempOp = messageSystem.getValue(DataType.TEMP);
|
||||||
|
tempOp.waitForReturn();
|
||||||
|
if (!tempOp.isSuccessful()) {
|
||||||
|
System.err.println("Failed to read temp");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation levelOp = messageSystem.getValue(DataType.LEVEL);
|
||||||
|
levelOp.waitForReturn();
|
||||||
|
if (!levelOp.isSuccessful()) {
|
||||||
|
System.err.println("Failed to read power level");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Current temp = " + Byte.toUnsignedInt(tempOp.getValue()) + " °C");
|
||||||
|
System.out.println("Current power = " + (int) (ExtMath.inverseLerp(0, 255, Byte.toUnsignedInt(tempOp.getValue())) * 100) + "%");
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainMenu.add(new SubmenuItem<Boolean, SettingsMenu, Void>(new SettingsMenu(messageSystem)) {
|
||||||
|
@Override
|
||||||
|
protected Boolean OnSelected(Void value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainMenu.add(new CallbackMenuItem<>("Reboot") {
|
||||||
|
@Override
|
||||||
|
public Boolean select() {
|
||||||
|
Operation operation = messageSystem.sendCommand(ActionType.REBOOT);
|
||||||
|
operation.waitForReturn();
|
||||||
|
System.out.println(operation.isSuccessful() ? "Reboot OK" : "Reboot failed!");
|
||||||
|
try {
|
||||||
|
Thread.sleep(10000L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainMenu.add(new CallbackMenuItem<>("Exit") {
|
||||||
|
@Override
|
||||||
|
public Boolean select() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void selectPort(boolean useDefaults) throws MessageException {
|
||||||
|
if (messageSystem != null) messageSystem.close();
|
||||||
|
var menu = new PortSelectionMenu();
|
||||||
|
var port = useDefaults ? menu.selectByName(Application.properties.getProperty("port")) : menu.show();
|
||||||
|
if (port != null) {
|
||||||
|
messageSystem = new SerialMessageSystem(port);
|
||||||
|
Application.properties.setProperty("port", port.getSystemPortName());
|
||||||
|
try {
|
||||||
|
Application.saveConfig();
|
||||||
|
} catch (IOException e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messageSystem = new DummyMessageSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
|
public abstract class Menu<T> {
|
||||||
|
private final String name;
|
||||||
|
private final List<MenuItem<T>> items = new ArrayList<>();
|
||||||
|
private boolean closed = false;
|
||||||
|
|
||||||
|
public Menu(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void add(MenuItem<T> item) {
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void removeAll() {
|
||||||
|
items.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final T selectByName(String name) {
|
||||||
|
for (MenuItem<T> item : items) {
|
||||||
|
if (item.render().equals(name)) {
|
||||||
|
return item.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final T show() {
|
||||||
|
T value = null;
|
||||||
|
closed = false;
|
||||||
|
do {
|
||||||
|
System.out.println("-=[" + name + "]=-");
|
||||||
|
StringJoiner joiner = new StringJoiner("\r\n");
|
||||||
|
int i = 1;
|
||||||
|
for (var item : items) {
|
||||||
|
joiner.add("[" + (i++) + "] " + item.render());
|
||||||
|
}
|
||||||
|
System.out.println(joiner);
|
||||||
|
System.out.print("Your input: ");
|
||||||
|
try {
|
||||||
|
String line = ConsoleApp.input.readLine();
|
||||||
|
try {
|
||||||
|
int index = Integer.parseInt(line) - 1;
|
||||||
|
if (index < 0 || index >= items.size()) throw new NumberFormatException("Number is out of range");
|
||||||
|
|
||||||
|
var item = items.get(index);
|
||||||
|
value = OnSelect(index, item, item.select());
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
System.err.println("Invalid number: " + line);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} while (!closed);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected T OnSelect(int index, MenuItem<T> item, T value) {
|
||||||
|
close();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void close() {
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
public interface MenuItem<T> {
|
||||||
|
String render();
|
||||||
|
|
||||||
|
T select();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
import com.fazecast.jSerialComm.SerialPort;
|
||||||
|
import ru.kirillius.cooler.controller.Application;
|
||||||
|
|
||||||
|
public class PortSelectionMenu extends Menu<SerialPort> {
|
||||||
|
public PortSelectionMenu() {
|
||||||
|
super("Select serial port");
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reload() {
|
||||||
|
add(new SimpleMenuItem<>(null, "Refresh..."));
|
||||||
|
if (Application.DEBUG) {
|
||||||
|
add(new SimpleMenuItem<>(null, "debug/virtual"));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (SerialPort port : SerialPort.getCommPorts()) {
|
||||||
|
add(new SimpleMenuItem<>(port, port.getSystemPortName()));
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new RuntimeException("Failed to get serial port list", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SerialPort OnSelect(int index, MenuItem<SerialPort> item, SerialPort value) {
|
||||||
|
if (value == null && index == 0) {
|
||||||
|
removeAll();
|
||||||
|
reload();
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
import ru.kirillius.cooler.controller.API.DataType;
|
||||||
|
import ru.kirillius.cooler.controller.API.MessageSystem;
|
||||||
|
import ru.kirillius.cooler.controller.API.Operation;
|
||||||
|
import ru.kirillius.cooler.controller.ExtMath;
|
||||||
|
|
||||||
|
public class ReactiveMenuItemBinding implements MenuItem<Void> {
|
||||||
|
protected MessageSystem messageSystem;
|
||||||
|
protected DataType dataType;
|
||||||
|
protected String title;
|
||||||
|
protected String units;
|
||||||
|
protected float minValue;
|
||||||
|
protected float maxValue;
|
||||||
|
protected float multiplier;
|
||||||
|
private Integer valueCache = null;
|
||||||
|
|
||||||
|
public ReactiveMenuItemBinding(MessageSystem messageSystem, DataType dataType, String title, String units, float minValue, float maxValue, float multiplier) {
|
||||||
|
this.messageSystem = messageSystem;
|
||||||
|
this.dataType = dataType;
|
||||||
|
this.title = title;
|
||||||
|
this.units = units;
|
||||||
|
this.minValue = minValue;
|
||||||
|
this.maxValue = maxValue;
|
||||||
|
this.multiplier = multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String render() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
Operation operation;
|
||||||
|
int value;
|
||||||
|
if (valueCache == null) {
|
||||||
|
operation = messageSystem.getValue(dataType);
|
||||||
|
operation.waitForReturn();
|
||||||
|
if (!operation.isSuccessful()) return "Failed to read " + dataType;
|
||||||
|
value = Byte.toUnsignedInt(operation.getValue());
|
||||||
|
valueCache = value;
|
||||||
|
} else {
|
||||||
|
value = valueCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
var t = ExtMath.clamp(ExtMath.inverseLerp(minValue, maxValue, value), 100, 0) * 10;
|
||||||
|
builder.append(title).append(" [");
|
||||||
|
for (int i = 0; i < t; i++) builder.append("+");
|
||||||
|
for (int i = (int) t; i < 10; i++) builder.append("-");
|
||||||
|
builder.append("] ").append(value / multiplier).append(units);
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void select() {
|
||||||
|
float value;
|
||||||
|
System.out.print("Set new value [" + (minValue / multiplier) + "-" + (maxValue / multiplier) + "]: ");
|
||||||
|
try {
|
||||||
|
value = Float.parseFloat(ConsoleApp.input.readLine());
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Invalid value!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation operation = messageSystem.setValue(dataType, (byte) ExtMath.clamp(value * multiplier, maxValue, minValue));
|
||||||
|
operation.waitForReturn();
|
||||||
|
if (!operation.isSuccessful()) {
|
||||||
|
System.err.println("Failed to set value!");
|
||||||
|
valueCache = null;
|
||||||
|
} else {
|
||||||
|
valueCache = Byte.toUnsignedInt(operation.getValue());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
import ru.kirillius.cooler.controller.API.ActionType;
|
||||||
|
import ru.kirillius.cooler.controller.API.DataType;
|
||||||
|
import ru.kirillius.cooler.controller.API.MessageSystem;
|
||||||
|
import ru.kirillius.cooler.controller.API.Operation;
|
||||||
|
|
||||||
|
public class SettingsMenu extends Menu<Void> {
|
||||||
|
public SettingsMenu(MessageSystem messageSystem) {
|
||||||
|
super("Settings");
|
||||||
|
add(new ReactiveMenuItemBinding(
|
||||||
|
messageSystem,
|
||||||
|
DataType.START_TEMP,
|
||||||
|
"Start temperature",
|
||||||
|
"°C",
|
||||||
|
0,
|
||||||
|
40,
|
||||||
|
1
|
||||||
|
));
|
||||||
|
add(new ReactiveMenuItemBinding(
|
||||||
|
messageSystem,
|
||||||
|
DataType.STOP_TEMP,
|
||||||
|
"Stop temperature",
|
||||||
|
"°C",
|
||||||
|
0,
|
||||||
|
40,
|
||||||
|
1
|
||||||
|
));
|
||||||
|
add(new ReactiveMenuItemBinding(
|
||||||
|
messageSystem,
|
||||||
|
DataType.MAX_TEMP,
|
||||||
|
"Max temperature",
|
||||||
|
"°C",
|
||||||
|
25,
|
||||||
|
80,
|
||||||
|
1
|
||||||
|
));
|
||||||
|
add(new ReactiveMenuItemBinding(
|
||||||
|
messageSystem,
|
||||||
|
DataType.MAX_LEVEL,
|
||||||
|
"Max power level",
|
||||||
|
"%",
|
||||||
|
0,
|
||||||
|
255,
|
||||||
|
2.55f
|
||||||
|
));
|
||||||
|
add(new ReactiveMenuItemBinding(
|
||||||
|
messageSystem,
|
||||||
|
DataType.MIN_LEVEL,
|
||||||
|
"Min power level",
|
||||||
|
"%",
|
||||||
|
0,
|
||||||
|
255,
|
||||||
|
2.55f
|
||||||
|
));
|
||||||
|
add(new CallbackMenuItem<>("Save settings") {
|
||||||
|
@Override
|
||||||
|
public Void select() {
|
||||||
|
Operation operation = messageSystem.sendCommand(ActionType.SAVE);
|
||||||
|
operation.waitForReturn();
|
||||||
|
System.out.println(operation.isSuccessful() ? "Save OK" : "Save failed!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(new CallbackMenuItem<>("Exit") {
|
||||||
|
@Override
|
||||||
|
public Void select() {
|
||||||
|
close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void OnSelect(int index, MenuItem<Void> item, Void value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
public class SimpleMenuItem<T> implements MenuItem<T> {
|
||||||
|
private final T value;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
public SimpleMenuItem(T value, String name) {
|
||||||
|
this.value = value;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String render() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T select() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ru.kirillius.cooler.controller.CLI;
|
||||||
|
|
||||||
|
public abstract class SubmenuItem<T, M extends Menu<V>, V> implements MenuItem<T> {
|
||||||
|
private final Menu<V> menu;
|
||||||
|
|
||||||
|
public SubmenuItem(Menu<V> menu) {
|
||||||
|
this.menu = menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String render() {
|
||||||
|
return menu.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final T select() {
|
||||||
|
return OnSelected(menu.show());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract T OnSelected(V value);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package ru.kirillius.cooler.controller.EventSystem;
|
||||||
|
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
public class EventHandler<T extends EventListener<D>, D> {
|
||||||
|
private final Queue<T> listeners = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
public void addListener(T listener) {
|
||||||
|
if (listener == null) return;
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(T listener) {
|
||||||
|
if (listener == null) return;
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeAllListeners() {
|
||||||
|
listeners.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invoke(D data) {
|
||||||
|
for (T listener : listeners) {
|
||||||
|
listener.invoke(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package ru.kirillius.cooler.controller.EventSystem;
|
||||||
|
|
||||||
|
public interface EventListener<T> {
|
||||||
|
void invoke(T eventData);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package ru.kirillius.cooler.controller;
|
||||||
|
|
||||||
|
public final class ExtMath {
|
||||||
|
private ExtMath() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float clamp(float value, float max, float min) {
|
||||||
|
if (value < min) return min;
|
||||||
|
if (value > max) return max;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float lerp(float start, float end, float t) {
|
||||||
|
return start * (1 - t) + end * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float inverseLerp(float a, float b, float v) {
|
||||||
|
return (v - a) / (b - a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,286 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ru.kirillius.cooler.controller.UI.MainForm">
|
||||||
|
<grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="6" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||||
|
<margin top="15" left="15" bottom="15" right="15"/>
|
||||||
|
<constraints>
|
||||||
|
<xy x="20" y="20" width="752" height="402"/>
|
||||||
|
</constraints>
|
||||||
|
<properties/>
|
||||||
|
<border type="none"/>
|
||||||
|
<children>
|
||||||
|
<grid id="77be2" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||||
|
<margin top="0" left="0" bottom="0" right="0"/>
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties/>
|
||||||
|
<border type="none"/>
|
||||||
|
<children>
|
||||||
|
<component id="3825" class="javax.swing.JLabel" binding="temperature">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<font size="48"/>
|
||||||
|
<horizontalAlignment value="0"/>
|
||||||
|
<horizontalTextPosition value="0"/>
|
||||||
|
<text value="?"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="9d459" class="javax.swing.JLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<font size="20"/>
|
||||||
|
<horizontalAlignment value="0"/>
|
||||||
|
<text value="Температура"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="c08f6" class="javax.swing.JLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<font size="20"/>
|
||||||
|
<text value="Мощность"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="2359c" class="javax.swing.JLabel" binding="power">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<font size="48"/>
|
||||||
|
<text value="?"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
</children>
|
||||||
|
</grid>
|
||||||
|
<component id="71bc0" class="javax.swing.JSeparator">
|
||||||
|
<constraints>
|
||||||
|
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false">
|
||||||
|
<preferred-size width="-1" height="5"/>
|
||||||
|
<maximum-size width="-1" height="5"/>
|
||||||
|
</grid>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<orientation value="0"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="befc2" class="javax.swing.JSeparator">
|
||||||
|
<constraints>
|
||||||
|
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false">
|
||||||
|
<preferred-size width="-1" height="5"/>
|
||||||
|
<maximum-size width="-1" height="5"/>
|
||||||
|
</grid>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<orientation value="0"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<grid id="d71ac" layout-manager="GridLayoutManager" row-count="1" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||||
|
<margin top="0" left="0" bottom="0" right="0"/>
|
||||||
|
<constraints>
|
||||||
|
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false">
|
||||||
|
<preferred-size width="-1" height="25"/>
|
||||||
|
<maximum-size width="-1" height="25"/>
|
||||||
|
</grid>
|
||||||
|
</constraints>
|
||||||
|
<properties/>
|
||||||
|
<border type="none"/>
|
||||||
|
<children>
|
||||||
|
<component id="b9257" class="javax.swing.JButton" binding="saveButton">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<enabled value="false"/>
|
||||||
|
<horizontalAlignment value="0"/>
|
||||||
|
<text value="Схоронить"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="1e3bf" class="ru.kirillius.cooler.controller.UI.PortListComboBox" binding="portSelector">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties/>
|
||||||
|
</component>
|
||||||
|
<hspacer id="a7ac1">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
</hspacer>
|
||||||
|
<component id="4fe4e" class="javax.swing.JLabel" binding="status">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<font size="14" style="1"/>
|
||||||
|
<horizontalTextPosition value="0"/>
|
||||||
|
<text value="Not connected"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
</children>
|
||||||
|
</grid>
|
||||||
|
<grid id="6d010" layout-manager="GridLayoutManager" row-count="5" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||||
|
<margin top="0" left="0" bottom="0" right="0"/>
|
||||||
|
<constraints>
|
||||||
|
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties/>
|
||||||
|
<border type="none"/>
|
||||||
|
<children>
|
||||||
|
<component id="b980e" class="javax.swing.JLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false">
|
||||||
|
<preferred-size width="200" height="19"/>
|
||||||
|
</grid>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<horizontalAlignment value="2"/>
|
||||||
|
<text value="Температура включения"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="6ebf7" class="javax.swing.JSlider" binding="startTempSlider">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<enabled value="false"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="727ba" class="javax.swing.JLabel" binding="startTempLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<text value=" "/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<hspacer id="52430">
|
||||||
|
<constraints>
|
||||||
|
<grid row="0" column="3" row-span="5" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
</hspacer>
|
||||||
|
<component id="d33d" class="javax.swing.JLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false">
|
||||||
|
<preferred-size width="200" height="19"/>
|
||||||
|
</grid>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<horizontalAlignment value="2"/>
|
||||||
|
<text value="Температура отключения"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="e2ea7" class="javax.swing.JLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false">
|
||||||
|
<preferred-size width="200" height="19"/>
|
||||||
|
</grid>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<horizontalAlignment value="2"/>
|
||||||
|
<text value="Макс. мощность при температуре"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="925f5" class="javax.swing.JLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false">
|
||||||
|
<preferred-size width="200" height="19"/>
|
||||||
|
</grid>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<horizontalAlignment value="2"/>
|
||||||
|
<text value="Максимальная мощность"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="42539" class="javax.swing.JLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false">
|
||||||
|
<preferred-size width="200" height="19"/>
|
||||||
|
</grid>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<horizontalAlignment value="2"/>
|
||||||
|
<text value="Минимальная мощность"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="e24ce" class="javax.swing.JSlider" binding="stopTempSlider">
|
||||||
|
<constraints>
|
||||||
|
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<enabled value="false"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="3f281" class="javax.swing.JSlider" binding="maxTempSlider">
|
||||||
|
<constraints>
|
||||||
|
<grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<enabled value="false"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="54a94" class="javax.swing.JSlider" binding="maxLevelSlider">
|
||||||
|
<constraints>
|
||||||
|
<grid row="3" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<enabled value="false"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="ea527" class="javax.swing.JSlider" binding="minLevelSlider">
|
||||||
|
<constraints>
|
||||||
|
<grid row="4" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<enabled value="false"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="4b995" class="javax.swing.JLabel" binding="stopTempLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="1" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<text value=" "/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="15e3e" class="javax.swing.JLabel" binding="maxTempLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="2" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<text value=" "/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="73300" class="javax.swing.JLabel" binding="maxLevelLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="3" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<text value=" "/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<component id="d2913" class="javax.swing.JLabel" binding="minLevelLabel">
|
||||||
|
<constraints>
|
||||||
|
<grid row="4" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<text value=" "/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
</children>
|
||||||
|
</grid>
|
||||||
|
<component id="faf4c" class="javax.swing.JProgressBar" binding="progress">
|
||||||
|
<constraints>
|
||||||
|
<grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<foreground color="-12154416"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
</children>
|
||||||
|
</grid>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,425 @@
|
||||||
|
package ru.kirillius.cooler.controller.UI;
|
||||||
|
|
||||||
|
import com.fazecast.jSerialComm.SerialPort;
|
||||||
|
import com.formdev.flatlaf.FlatDarculaLaf;
|
||||||
|
import com.intellij.uiDesigner.core.GridConstraints;
|
||||||
|
import com.intellij.uiDesigner.core.GridLayoutManager;
|
||||||
|
import com.intellij.uiDesigner.core.Spacer;
|
||||||
|
import ru.kirillius.cooler.controller.API.*;
|
||||||
|
import ru.kirillius.cooler.controller.Application;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.plaf.FontUIResource;
|
||||||
|
import javax.swing.text.StyleContext;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
public class MainForm extends JFrame {
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private final Queue<ReactiveComponentController> reactiveControllers = new ConcurrentLinkedQueue<>();
|
||||||
|
private JPanel mainPanel;
|
||||||
|
private JLabel temperature;
|
||||||
|
private JLabel power;
|
||||||
|
private JButton saveButton;
|
||||||
|
private PortListComboBox portSelector;
|
||||||
|
private JLabel status;
|
||||||
|
private JSlider startTempSlider;
|
||||||
|
private JLabel startTempLabel;
|
||||||
|
private JSlider stopTempSlider;
|
||||||
|
private JSlider maxTempSlider;
|
||||||
|
private JSlider maxLevelSlider;
|
||||||
|
private JSlider minLevelSlider;
|
||||||
|
private JLabel stopTempLabel;
|
||||||
|
private JLabel maxTempLabel;
|
||||||
|
private JLabel maxLevelLabel;
|
||||||
|
private JLabel minLevelLabel;
|
||||||
|
private volatile MessageSystem messageSystem = null;
|
||||||
|
private JProgressBar progress;
|
||||||
|
|
||||||
|
public MainForm() {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(new FlatDarculaLaf());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
}
|
||||||
|
$$$setupUI$$$();
|
||||||
|
setTitle(Application.title);
|
||||||
|
try {
|
||||||
|
Image icon = new ImageIcon(Objects.requireNonNull(getClass().getResource("/icon24.png"))).getImage();
|
||||||
|
setIconImage(icon);
|
||||||
|
} catch (Exception e) {
|
||||||
|
MainForm.showError(e);
|
||||||
|
}
|
||||||
|
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
|
|
||||||
|
setContentPane(mainPanel);
|
||||||
|
setSize(500, 500);
|
||||||
|
setResizable(false);
|
||||||
|
setLocationRelativeTo(null);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
t.printStackTrace();
|
||||||
|
showError(t);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setListeners();
|
||||||
|
startConnectionChecker();
|
||||||
|
startTelemetryListeners();
|
||||||
|
selectDefaultPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showError(Throwable t) {
|
||||||
|
t.printStackTrace();
|
||||||
|
StringBuilder buffer = new StringBuilder();
|
||||||
|
while (t != null) {
|
||||||
|
buffer.append(t.getClass().getSimpleName());
|
||||||
|
if (t.getMessage() != null) buffer.append(": ").append(t.getMessage());
|
||||||
|
t = t.getCause();
|
||||||
|
if (t != null) buffer.append("\r\nCaused by ");
|
||||||
|
}
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(null, buffer.toString(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectDefaultPort() {
|
||||||
|
SerialPort selectedPort = portSelector.selectPortByName(Application.properties.getProperty("port"));
|
||||||
|
if (selectedPort != null) {
|
||||||
|
openConnection(selectedPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startConnectionChecker() {
|
||||||
|
asyncExecute(() -> {
|
||||||
|
while (!Thread.interrupted()) {
|
||||||
|
try {
|
||||||
|
//noinspection BusyWait
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var disconnected = (messageSystem == null || (System.currentTimeMillis() - messageSystem.getLastReceiveTime() > 10000L));
|
||||||
|
SwingUtilities.invokeLater(() -> status.setText(disconnected ? "Disconnected" : "Connected"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openConnection(SerialPort port) {
|
||||||
|
try {
|
||||||
|
if (messageSystem != null) {
|
||||||
|
messageSystem.close();
|
||||||
|
for (var controller : reactiveControllers) {
|
||||||
|
controller.destroy();
|
||||||
|
}
|
||||||
|
reactiveControllers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
messageSystem = port != null ? new SerialMessageSystem(port) : new DummyMessageSystem();
|
||||||
|
|
||||||
|
createReactiveControllers();
|
||||||
|
asyncExecute(this::loadConfiguration);
|
||||||
|
} catch (MessageException e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startTelemetryListeners() {
|
||||||
|
asyncExecute(() -> {
|
||||||
|
while (!Thread.interrupted()) {
|
||||||
|
try {
|
||||||
|
//noinspection BusyWait
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (messageSystem == null) continue;
|
||||||
|
var temp = messageSystem.getValue(DataType.TEMP);
|
||||||
|
temp.waitForReturn();
|
||||||
|
if (temp.isSuccessful()) temperature.setText((0xFF & temp.getValue()) + " °C");
|
||||||
|
var level = messageSystem.getValue(DataType.LEVEL);
|
||||||
|
level.waitForReturn();
|
||||||
|
if (level.isSuccessful()) power.setText(
|
||||||
|
(int) ((float) (level.getValue() & 0xFF) / 255f * 100) + " %"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void asyncExecute(Runnable runnable) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
showError(t);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createReactiveControllers() {
|
||||||
|
reactiveControllers.add(
|
||||||
|
new ReactiveSliderController(
|
||||||
|
DataType.MIN_LEVEL,
|
||||||
|
messageSystem,
|
||||||
|
minLevelSlider,
|
||||||
|
minLevelLabel,
|
||||||
|
"%",
|
||||||
|
0,
|
||||||
|
255,
|
||||||
|
2.55f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
reactiveControllers.add(
|
||||||
|
new ReactiveSliderController(
|
||||||
|
DataType.MAX_LEVEL,
|
||||||
|
messageSystem,
|
||||||
|
maxLevelSlider,
|
||||||
|
maxLevelLabel,
|
||||||
|
"%",
|
||||||
|
0,
|
||||||
|
255,
|
||||||
|
2.55f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
reactiveControllers.add(
|
||||||
|
new ReactiveSliderController(
|
||||||
|
DataType.START_TEMP,
|
||||||
|
messageSystem,
|
||||||
|
startTempSlider,
|
||||||
|
startTempLabel,
|
||||||
|
"°C",
|
||||||
|
0,
|
||||||
|
40,
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
reactiveControllers.add(
|
||||||
|
new ReactiveSliderController(
|
||||||
|
DataType.STOP_TEMP,
|
||||||
|
messageSystem,
|
||||||
|
stopTempSlider,
|
||||||
|
stopTempLabel,
|
||||||
|
"°C",
|
||||||
|
0,
|
||||||
|
40,
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
reactiveControllers.add(
|
||||||
|
new ReactiveSliderController(
|
||||||
|
DataType.MAX_TEMP,
|
||||||
|
messageSystem,
|
||||||
|
maxTempSlider,
|
||||||
|
maxTempLabel,
|
||||||
|
"°C",
|
||||||
|
25,
|
||||||
|
80,
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProgress(int value) {
|
||||||
|
SwingUtilities.invokeLater(() -> progress.setValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfiguration() {
|
||||||
|
saveButton.setEnabled(false);
|
||||||
|
|
||||||
|
for (var controller : reactiveControllers) {
|
||||||
|
controller.setLock(true);
|
||||||
|
}
|
||||||
|
var ms = messageSystem;
|
||||||
|
setProgress(0);
|
||||||
|
int i = 0;
|
||||||
|
int size = reactiveControllers.size();
|
||||||
|
for (var controller : reactiveControllers) {
|
||||||
|
if (ms != messageSystem) return; //interrupt if MS was changed!
|
||||||
|
controller.reloadValue();
|
||||||
|
setProgress(100 * (++i) / size);
|
||||||
|
}
|
||||||
|
setProgress(100);
|
||||||
|
|
||||||
|
for (var controller : reactiveControllers) {
|
||||||
|
controller.setLock(false);
|
||||||
|
}
|
||||||
|
saveButton.setEnabled(true);
|
||||||
|
setProgress(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setListeners() {
|
||||||
|
portSelector.addListener(eventData -> {
|
||||||
|
openConnection(eventData.getPort());
|
||||||
|
Application.properties.setProperty("port", eventData.getText());
|
||||||
|
try {
|
||||||
|
Application.saveConfig();
|
||||||
|
} catch (IOException e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
saveButton.addActionListener(e -> {
|
||||||
|
saveButton.setEnabled(false);
|
||||||
|
Operation operation = messageSystem.sendCommand(ActionType.SAVE);
|
||||||
|
operation.addCompleteListener(eventData -> {
|
||||||
|
if (!eventData.isSuccessful()) showError(new RuntimeException("Save failed"));
|
||||||
|
saveButton.setEnabled(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method generated by IntelliJ IDEA GUI Designer
|
||||||
|
* >>> IMPORTANT!! <<<
|
||||||
|
* DO NOT edit this method OR call it in your code!
|
||||||
|
*
|
||||||
|
* @noinspection ALL
|
||||||
|
*/
|
||||||
|
private void $$$setupUI$$$() {
|
||||||
|
mainPanel = new JPanel();
|
||||||
|
mainPanel.setLayout(new GridLayoutManager(6, 1, new Insets(15, 15, 15, 15), -1, -1));
|
||||||
|
final JPanel panel1 = new JPanel();
|
||||||
|
panel1.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1));
|
||||||
|
mainPanel.add(panel1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
|
||||||
|
temperature = new JLabel();
|
||||||
|
Font temperatureFont = this.$$$getFont$$$(null, -1, 48, temperature.getFont());
|
||||||
|
if (temperatureFont != null) temperature.setFont(temperatureFont);
|
||||||
|
temperature.setHorizontalAlignment(0);
|
||||||
|
temperature.setHorizontalTextPosition(0);
|
||||||
|
temperature.setText("?");
|
||||||
|
panel1.add(temperature, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
final JLabel label1 = new JLabel();
|
||||||
|
Font label1Font = this.$$$getFont$$$(null, -1, 20, label1.getFont());
|
||||||
|
if (label1Font != null) label1.setFont(label1Font);
|
||||||
|
label1.setHorizontalAlignment(0);
|
||||||
|
label1.setText("Температура");
|
||||||
|
panel1.add(label1, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
final JLabel label2 = new JLabel();
|
||||||
|
Font label2Font = this.$$$getFont$$$(null, -1, 20, label2.getFont());
|
||||||
|
if (label2Font != null) label2.setFont(label2Font);
|
||||||
|
label2.setText("Мощность");
|
||||||
|
panel1.add(label2, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
power = new JLabel();
|
||||||
|
Font powerFont = this.$$$getFont$$$(null, -1, 48, power.getFont());
|
||||||
|
if (powerFont != null) power.setFont(powerFont);
|
||||||
|
power.setText("?");
|
||||||
|
panel1.add(power, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
final JSeparator separator1 = new JSeparator();
|
||||||
|
separator1.setOrientation(0);
|
||||||
|
mainPanel.add(separator1, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(-1, 5), new Dimension(-1, 5), 0, false));
|
||||||
|
final JSeparator separator2 = new JSeparator();
|
||||||
|
separator2.setOrientation(0);
|
||||||
|
mainPanel.add(separator2, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(-1, 5), new Dimension(-1, 5), 0, false));
|
||||||
|
final JPanel panel2 = new JPanel();
|
||||||
|
panel2.setLayout(new GridLayoutManager(1, 4, new Insets(0, 0, 0, 0), -1, -1));
|
||||||
|
mainPanel.add(panel2, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(-1, 25), new Dimension(-1, 25), 0, false));
|
||||||
|
saveButton = new JButton();
|
||||||
|
saveButton.setEnabled(false);
|
||||||
|
saveButton.setHorizontalAlignment(0);
|
||||||
|
saveButton.setText("Схоронить");
|
||||||
|
panel2.add(saveButton, new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
portSelector = new PortListComboBox();
|
||||||
|
panel2.add(portSelector, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
final Spacer spacer1 = new Spacer();
|
||||||
|
panel2.add(spacer1, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false));
|
||||||
|
status = new JLabel();
|
||||||
|
Font statusFont = this.$$$getFont$$$(null, Font.BOLD, 14, status.getFont());
|
||||||
|
if (statusFont != null) status.setFont(statusFont);
|
||||||
|
status.setHorizontalTextPosition(0);
|
||||||
|
status.setText("Not connected");
|
||||||
|
panel2.add(status, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
final JPanel panel3 = new JPanel();
|
||||||
|
panel3.setLayout(new GridLayoutManager(5, 4, new Insets(0, 0, 0, 0), -1, -1));
|
||||||
|
mainPanel.add(panel3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
|
||||||
|
final JLabel label3 = new JLabel();
|
||||||
|
label3.setHorizontalAlignment(2);
|
||||||
|
label3.setText("Температура включения");
|
||||||
|
panel3.add(label3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(200, 19), null, 0, false));
|
||||||
|
startTempSlider = new JSlider();
|
||||||
|
startTempSlider.setEnabled(false);
|
||||||
|
panel3.add(startTempSlider, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
startTempLabel = new JLabel();
|
||||||
|
startTempLabel.setText(" ");
|
||||||
|
panel3.add(startTempLabel, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
final Spacer spacer2 = new Spacer();
|
||||||
|
panel3.add(spacer2, new GridConstraints(0, 3, 5, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false));
|
||||||
|
final JLabel label4 = new JLabel();
|
||||||
|
label4.setHorizontalAlignment(2);
|
||||||
|
label4.setText("Температура отключения");
|
||||||
|
panel3.add(label4, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(200, 19), null, 0, false));
|
||||||
|
final JLabel label5 = new JLabel();
|
||||||
|
label5.setHorizontalAlignment(2);
|
||||||
|
label5.setText("Макс. мощность при температуре");
|
||||||
|
panel3.add(label5, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(200, 19), null, 0, false));
|
||||||
|
final JLabel label6 = new JLabel();
|
||||||
|
label6.setHorizontalAlignment(2);
|
||||||
|
label6.setText("Максимальная мощность");
|
||||||
|
panel3.add(label6, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(200, 19), null, 0, false));
|
||||||
|
final JLabel label7 = new JLabel();
|
||||||
|
label7.setHorizontalAlignment(2);
|
||||||
|
label7.setText("Минимальная мощность");
|
||||||
|
panel3.add(label7, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(200, 19), null, 0, false));
|
||||||
|
stopTempSlider = new JSlider();
|
||||||
|
stopTempSlider.setEnabled(false);
|
||||||
|
panel3.add(stopTempSlider, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
maxTempSlider = new JSlider();
|
||||||
|
maxTempSlider.setEnabled(false);
|
||||||
|
panel3.add(maxTempSlider, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
maxLevelSlider = new JSlider();
|
||||||
|
maxLevelSlider.setEnabled(false);
|
||||||
|
panel3.add(maxLevelSlider, new GridConstraints(3, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
minLevelSlider = new JSlider();
|
||||||
|
minLevelSlider.setEnabled(false);
|
||||||
|
panel3.add(minLevelSlider, new GridConstraints(4, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
stopTempLabel = new JLabel();
|
||||||
|
stopTempLabel.setText(" ");
|
||||||
|
panel3.add(stopTempLabel, new GridConstraints(1, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
maxTempLabel = new JLabel();
|
||||||
|
maxTempLabel.setText(" ");
|
||||||
|
panel3.add(maxTempLabel, new GridConstraints(2, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
maxLevelLabel = new JLabel();
|
||||||
|
maxLevelLabel.setText(" ");
|
||||||
|
panel3.add(maxLevelLabel, new GridConstraints(3, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
minLevelLabel = new JLabel();
|
||||||
|
minLevelLabel.setText(" ");
|
||||||
|
panel3.add(minLevelLabel, new GridConstraints(4, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
progress = new JProgressBar();
|
||||||
|
progress.setForeground(new Color(-12154416));
|
||||||
|
mainPanel.add(progress, new GridConstraints(5, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection ALL
|
||||||
|
*/
|
||||||
|
private Font $$$getFont$$$(String fontName, int style, int size, Font currentFont) {
|
||||||
|
if (currentFont == null) return null;
|
||||||
|
String resultName;
|
||||||
|
if (fontName == null) {
|
||||||
|
resultName = currentFont.getName();
|
||||||
|
} else {
|
||||||
|
Font testFont = new Font(fontName, Font.PLAIN, 10);
|
||||||
|
if (testFont.canDisplay('a') && testFont.canDisplay('1')) {
|
||||||
|
resultName = fontName;
|
||||||
|
} else {
|
||||||
|
resultName = currentFont.getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Font font = new Font(resultName, style >= 0 ? style : currentFont.getStyle(), size >= 0 ? size : currentFont.getSize());
|
||||||
|
boolean isMac = System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).startsWith("mac");
|
||||||
|
Font fontWithFallback = isMac ? new Font(font.getFamily(), font.getStyle(), font.getSize()) : new StyleContext().getFont(font.getFamily(), font.getStyle(), font.getSize());
|
||||||
|
return fontWithFallback instanceof FontUIResource ? fontWithFallback : new FontUIResource(fontWithFallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection ALL
|
||||||
|
*/
|
||||||
|
public JComponent $$$getRootComponent$$$() {
|
||||||
|
return mainPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package ru.kirillius.cooler.controller.UI;
|
||||||
|
|
||||||
|
import com.fazecast.jSerialComm.SerialPort;
|
||||||
|
import ru.kirillius.cooler.controller.Application;
|
||||||
|
import ru.kirillius.cooler.controller.EventSystem.EventHandler;
|
||||||
|
import ru.kirillius.cooler.controller.EventSystem.EventListener;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
|
||||||
|
public class PortListComboBox extends JComboBox<PortListComboBox.PortItem> {
|
||||||
|
private final EventHandler<EventListener<PortItem>, PortItem> handler = new EventHandler<>();
|
||||||
|
|
||||||
|
public PortListComboBox() {
|
||||||
|
reload();
|
||||||
|
bindActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SerialPort selectPortByName(String name) {
|
||||||
|
if (name != null) {
|
||||||
|
for (var i = 0; i < getItemCount(); i++) {
|
||||||
|
PortItem item = getItemAt(i);
|
||||||
|
if (item.text.equals(name)) {
|
||||||
|
setSelectedIndex(i);
|
||||||
|
return item.port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(EventListener<PortItem> listener) {
|
||||||
|
handler.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reload() {
|
||||||
|
removeAllItems();
|
||||||
|
addItem(new PortItem("Refresh...", null));
|
||||||
|
if (Application.DEBUG) {
|
||||||
|
addItem(new PortItem("debug/virtual", null));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (SerialPort port : SerialPort.getCommPorts()) {
|
||||||
|
addItem(new PortItem(port.getSystemPortName(), port));
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new RuntimeException("Failed to get serial port list", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindActions() {
|
||||||
|
super.addActionListener(e -> {
|
||||||
|
int index = getSelectedIndex();
|
||||||
|
if (index == -1) return;
|
||||||
|
PortItem item = getItemAt(index);
|
||||||
|
|
||||||
|
if (item.port == null && index == 0) {
|
||||||
|
reload();
|
||||||
|
} else {
|
||||||
|
handler.invoke(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addActionListener(ActionListener l) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static final class PortItem {
|
||||||
|
private final String text;
|
||||||
|
private final SerialPort port;
|
||||||
|
|
||||||
|
public PortItem(String text, SerialPort port) {
|
||||||
|
this.text = text;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SerialPort getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package ru.kirillius.cooler.controller.UI;
|
||||||
|
|
||||||
|
import ru.kirillius.cooler.controller.API.DataType;
|
||||||
|
import ru.kirillius.cooler.controller.API.MessageSystem;
|
||||||
|
import ru.kirillius.cooler.controller.API.Operation;
|
||||||
|
import ru.kirillius.cooler.controller.ExtMath;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
public abstract class ReactiveComponentController<C extends JComponent> {
|
||||||
|
protected DataType dataType;
|
||||||
|
protected MessageSystem messageSystem;
|
||||||
|
protected C component;
|
||||||
|
protected String units;
|
||||||
|
protected float minValue;
|
||||||
|
protected float maxValue;
|
||||||
|
protected float multiplier;
|
||||||
|
protected boolean lock = true;
|
||||||
|
protected JLabel label;
|
||||||
|
private volatile float lastUpdateValue = Float.NEGATIVE_INFINITY;
|
||||||
|
private volatile Operation updateOperation = null;
|
||||||
|
|
||||||
|
public ReactiveComponentController(DataType dataType, MessageSystem messageSystem, C component, JLabel label, String units, float minValue, float maxValue, float multiplier) {
|
||||||
|
this.dataType = dataType;
|
||||||
|
this.multiplier = multiplier;
|
||||||
|
this.label = label;
|
||||||
|
this.messageSystem = messageSystem;
|
||||||
|
this.component = component;
|
||||||
|
this.units = units;
|
||||||
|
this.minValue = minValue;
|
||||||
|
this.maxValue = maxValue;
|
||||||
|
|
||||||
|
label.setText(units);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadValue() {
|
||||||
|
Operation operation = messageSystem.getValue(dataType);
|
||||||
|
operation.waitForReturn();
|
||||||
|
if (operation.isSuccessful()) setValue((int) operation.getValue() & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLock(boolean lock) {
|
||||||
|
this.lock = lock;
|
||||||
|
component.setEnabled(!lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateSystem(float value) {
|
||||||
|
if (messageSystem == null) return;
|
||||||
|
if (!lock) {
|
||||||
|
value = ExtMath.lerp(minValue, maxValue, ExtMath.clamp(value, 100, 0) / 100);
|
||||||
|
updateLabel(value);
|
||||||
|
startUpdateOperation(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void startUpdateOperation(float value) {
|
||||||
|
if (updateOperation != null && !updateOperation.isComplete()) {
|
||||||
|
lastUpdateValue = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastUpdateValue = value;
|
||||||
|
updateOperation = messageSystem.setValue(dataType, (byte) (0xFF & (int) value));
|
||||||
|
updateOperation.addCompleteListener(eventData -> {
|
||||||
|
if (value != lastUpdateValue) startUpdateOperation(lastUpdateValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(float value) {
|
||||||
|
var t = ExtMath.inverseLerp(minValue, maxValue, value);
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
changeComponentValue(ExtMath.clamp(t * 100f, 100, 0));
|
||||||
|
updateLabel(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateLabel(float value) {
|
||||||
|
label.setText((int) (value / multiplier) + " " + units);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void changeComponentValue(float value);
|
||||||
|
|
||||||
|
public abstract void destroy();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ru.kirillius.cooler.controller.UI;
|
||||||
|
|
||||||
|
import ru.kirillius.cooler.controller.API.DataType;
|
||||||
|
import ru.kirillius.cooler.controller.API.MessageSystem;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.event.ChangeListener;
|
||||||
|
|
||||||
|
public class ReactiveSliderController extends ReactiveComponentController<JSlider> {
|
||||||
|
private final ChangeListener listener;
|
||||||
|
|
||||||
|
public ReactiveSliderController(DataType dataType, MessageSystem messageSystem, JSlider component, JLabel label, String units, float minValue, float maxValue, float multiplier) {
|
||||||
|
super(dataType, messageSystem, component, label, units, minValue, maxValue, multiplier);
|
||||||
|
listener = e -> updateSystem(component.getValue());
|
||||||
|
component.addChangeListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void changeComponentValue(float value) {
|
||||||
|
component.setValue((int) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
component.removeChangeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 786 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Reference in New Issue