diff --git a/App/package.spec b/App/package.spec
new file mode 100644
index 0000000..c79062e
--- /dev/null
+++ b/App/package.spec
@@ -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
+
diff --git a/App/platform/linux-wrapper.sh b/App/platform/linux-wrapper.sh
new file mode 100644
index 0000000..2152c80
--- /dev/null
+++ b/App/platform/linux-wrapper.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/sh
+java -jar "$0" "$@"
+exit $?
diff --git a/App/pom.xml b/App/pom.xml
new file mode 100644
index 0000000..c91ab46
--- /dev/null
+++ b/App/pom.xml
@@ -0,0 +1,183 @@
+
+
+ 4.0.0
+
+ ru.kirillius
+ cooler.controller
+ 2.0.0.0
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 10
+ 10
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 1.4
+
+
+ package
+
+ shade
+
+
+
+
+
+
+ *:*
+
+ module-info.class
+ JDOMAbout*class
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+ ru.kirillius.cooler.controller.Application
+
+
+
+ true
+
+ shaded
+
+
+
+
+ com.akathist.maven.plugins.launch4j
+ launch4j-maven-plugin
+ 2.1.2
+
+
+ l4j-wrapper
+ package
+
+ launch4j
+
+
+ gui
+ https://aws.amazon.com/ru/corretto/
+ target/coolcfg.exe
+
+ ${project.build.directory}/${project.artifactId}-${project.version}-shaded.jar
+
+
+
+ ru.kirillius.cooler.controller.Application
+ false
+ anything
+
+ src/main/resources/icon.ico
+
+ ./runtime
+ 10.0
+
+
+ -Dfile.encoding=UTF-8
+
+
+
+
+ ${project.version}
+ ${project.version}
+ cooler controller configurator
+ copy left
+ ${project.version}
+ ${project.version}
+ cooler controller configurator
+ kirillius.ru
+ coolcfg
+ coolcfg.exe
+ ENGLISH_US
+
+
+
+
+ l4j-wrapper-console
+ package
+
+ launch4j
+
+
+ console
+ https://aws.amazon.com/ru/corretto/
+ target/coolcfg-debug.exe
+
+ ${project.build.directory}/${project.artifactId}-${project.version}-shaded.jar
+
+
+
+ ru.kirillius.cooler.controller.Application
+ false
+ anything
+
+ src/main/resources/icon.ico
+
+ ./runtime
+ 10.0
+
+
+ -Dfile.encoding=UTF-8
+ -Dyabba=ICING
+
+
+
+
+ ${project.version}
+ ${project.version}
+ cooler controller configurator
+ copy left
+ ${project.version}
+ ${project.version}
+ cooler controller configurator
+ kirillius.ru
+ coolcfg
+ coolcfg.exe
+ ENGLISH_US
+
+
+
+
+
+
+
+
+ 10
+ 10
+
+
+
+
+
+ com.fazecast
+ jSerialComm
+ 2.9.1
+
+
+ com.formdev
+ flatlaf
+ 2.1
+
+
+
+ com.intellij
+ forms_rt
+ 7.0.3
+
+
+
\ No newline at end of file
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/ActionType.java b/App/src/main/java/ru/kirillius/cooler/controller/API/ActionType.java
new file mode 100644
index 0000000..aa3df85
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/ActionType.java
@@ -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;
+ }
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/DataType.java b/App/src/main/java/ru/kirillius/cooler/controller/API/DataType.java
new file mode 100644
index 0000000..92ff6ed
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/DataType.java
@@ -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;
+ }
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/DummyMessageSystem.java b/App/src/main/java/ru/kirillius/cooler/controller/API/DummyMessageSystem.java
new file mode 100644
index 0000000..8c5aaf6
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/DummyMessageSystem.java
@@ -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 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 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);
+ }
+ }
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/ErrorCode.java b/App/src/main/java/ru/kirillius/cooler/controller/API/ErrorCode.java
new file mode 100644
index 0000000..ff67eb6
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/ErrorCode.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/Message.java b/App/src/main/java/ru/kirillius/cooler/controller/API/Message.java
new file mode 100644
index 0000000..6ccbff3
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/Message.java
@@ -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};
+ }
+
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/MessageException.java b/App/src/main/java/ru/kirillius/cooler/controller/API/MessageException.java
new file mode 100644
index 0000000..341c62d
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/MessageException.java
@@ -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);
+ }
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/MessageSystem.java b/App/src/main/java/ru/kirillius/cooler/controller/API/MessageSystem.java
new file mode 100644
index 0000000..862008e
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/MessageSystem.java
@@ -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 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 {
+
+ }
+
+ private final static class Handler extends EventHandler {
+
+ }
+
+ 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, 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 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;
+ };
+ }
+ }
+
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/Operation.java b/App/src/main/java/ru/kirillius/cooler/controller/API/Operation.java
new file mode 100644
index 0000000..4e4ad3d
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/Operation.java
@@ -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 listener);
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/API/SerialMessageSystem.java b/App/src/main/java/ru/kirillius/cooler/controller/API/SerialMessageSystem.java
new file mode 100644
index 0000000..38708d5
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/API/SerialMessageSystem.java
@@ -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();
+ }
+
+
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/Application.java b/App/src/main/java/ru/kirillius/cooler/controller/Application.java
new file mode 100644
index 0000000..cb5ee8b
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/Application.java
@@ -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();
+ }
+
+ }
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/CallbackMenuItem.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/CallbackMenuItem.java
new file mode 100644
index 0000000..52408d3
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/CallbackMenuItem.java
@@ -0,0 +1,16 @@
+package ru.kirillius.cooler.controller.CLI;
+
+public abstract class CallbackMenuItem implements MenuItem {
+ private final String name;
+
+ public CallbackMenuItem(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String render() {
+ return name;
+ }
+
+
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/ConsoleApp.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/ConsoleApp.java
new file mode 100644
index 0000000..9c6bf53
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/ConsoleApp.java
@@ -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 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 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(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();
+ }
+
+
+ }
+}
diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/Menu.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/Menu.java
new file mode 100644
index 0000000..9b72200
--- /dev/null
+++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/Menu.java
@@ -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 {
+ private final String name;
+ private final List