This commit is contained in:
kirill.labutin 2025-01-09 20:34:02 +03:00
parent 11677dfa2b
commit 3fad2f0b2d
34 changed files with 2353 additions and 0 deletions

26
App/package.spec Normal file
View File

@ -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

View File

@ -0,0 +1,3 @@
#!/usr/bin/sh
java -jar "$0" "$@"
exit $?

183
App/pom.xml Normal file
View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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};
}
}

View File

@ -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);
}
}

View File

@ -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;
};
}
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
package ru.kirillius.cooler.controller.CLI;
public interface MenuItem<T> {
String render();
T select();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,5 @@
package ru.kirillius.cooler.controller.EventSystem;
public interface EventListener<T> {
void invoke(T eventData);
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}

View File

@ -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