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> items = new ArrayList<>(); + private boolean closed = false; + + public Menu(String name) { + this.name = name; + } + + protected final void add(MenuItem item) { + items.add(item); + } + + protected final void removeAll() { + items.clear(); + } + + public final T selectByName(String name) { + for (MenuItem 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 item, T value) { + close(); + return value; + } + + public final void close() { + closed = true; + } + + + public String getName() { + return name; + } +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/MenuItem.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/MenuItem.java new file mode 100644 index 0000000..8e61394 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/MenuItem.java @@ -0,0 +1,7 @@ +package ru.kirillius.cooler.controller.CLI; + +public interface MenuItem { + String render(); + + T select(); +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/PortSelectionMenu.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/PortSelectionMenu.java new file mode 100644 index 0000000..bfb8548 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/PortSelectionMenu.java @@ -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 { + 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 item, SerialPort value) { + if (value == null && index == 0) { + removeAll(); + reload(); + } else { + close(); + return value; + } + return null; + } +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/ReactiveMenuItemBinding.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/ReactiveMenuItemBinding.java new file mode 100644 index 0000000..e9b87a3 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/ReactiveMenuItemBinding.java @@ -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 { + 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; + } +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/SettingsMenu.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/SettingsMenu.java new file mode 100644 index 0000000..3c14c96 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/SettingsMenu.java @@ -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 { + 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 item, Void value) { + return null; + } +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/SimpleMenuItem.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/SimpleMenuItem.java new file mode 100644 index 0000000..6cf3bc0 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/SimpleMenuItem.java @@ -0,0 +1,21 @@ +package ru.kirillius.cooler.controller.CLI; + +public class SimpleMenuItem implements MenuItem { + 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; + } +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/CLI/SubmenuItem.java b/App/src/main/java/ru/kirillius/cooler/controller/CLI/SubmenuItem.java new file mode 100644 index 0000000..1139ad1 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/CLI/SubmenuItem.java @@ -0,0 +1,21 @@ +package ru.kirillius.cooler.controller.CLI; + +public abstract class SubmenuItem, V> implements MenuItem { + private final Menu menu; + + public SubmenuItem(Menu 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); +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/EventSystem/EventHandler.java b/App/src/main/java/ru/kirillius/cooler/controller/EventSystem/EventHandler.java new file mode 100644 index 0000000..8cb0eef --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/EventSystem/EventHandler.java @@ -0,0 +1,28 @@ +package ru.kirillius.cooler.controller.EventSystem; + +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +public class EventHandler, D> { + private final Queue 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); + } + } +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/EventSystem/EventListener.java b/App/src/main/java/ru/kirillius/cooler/controller/EventSystem/EventListener.java new file mode 100644 index 0000000..4cfee76 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/EventSystem/EventListener.java @@ -0,0 +1,5 @@ +package ru.kirillius.cooler.controller.EventSystem; + +public interface EventListener { + void invoke(T eventData); +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/ExtMath.java b/App/src/main/java/ru/kirillius/cooler/controller/ExtMath.java new file mode 100644 index 0000000..4346dab --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/ExtMath.java @@ -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); + } +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/UI/MainForm.form b/App/src/main/java/ru/kirillius/cooler/controller/UI/MainForm.form new file mode 100644 index 0000000..eca1cb3 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/UI/MainForm.form @@ -0,0 +1,286 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/App/src/main/java/ru/kirillius/cooler/controller/UI/MainForm.java b/App/src/main/java/ru/kirillius/cooler/controller/UI/MainForm.java new file mode 100644 index 0000000..2d107b2 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/UI/MainForm.java @@ -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 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; + } + + +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/UI/PortListComboBox.java b/App/src/main/java/ru/kirillius/cooler/controller/UI/PortListComboBox.java new file mode 100644 index 0000000..dd86ae6 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/UI/PortListComboBox.java @@ -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 { + private final EventHandler, 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 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; + } + } +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/UI/ReactiveComponentController.java b/App/src/main/java/ru/kirillius/cooler/controller/UI/ReactiveComponentController.java new file mode 100644 index 0000000..2ae2038 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/UI/ReactiveComponentController.java @@ -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 { + 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(); + + +} diff --git a/App/src/main/java/ru/kirillius/cooler/controller/UI/ReactiveSliderController.java b/App/src/main/java/ru/kirillius/cooler/controller/UI/ReactiveSliderController.java new file mode 100644 index 0000000..ebfeba8 --- /dev/null +++ b/App/src/main/java/ru/kirillius/cooler/controller/UI/ReactiveSliderController.java @@ -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 { + 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); + } +} diff --git a/App/src/main/resources/icon.ico b/App/src/main/resources/icon.ico new file mode 100644 index 0000000..6b11b5c Binary files /dev/null and b/App/src/main/resources/icon.ico differ diff --git a/App/src/main/resources/icon24.png b/App/src/main/resources/icon24.png new file mode 100644 index 0000000..ec54429 Binary files /dev/null and b/App/src/main/resources/icon24.png differ diff --git a/App/src/main/resources/icon32.png b/App/src/main/resources/icon32.png new file mode 100644 index 0000000..3cf6cb0 Binary files /dev/null and b/App/src/main/resources/icon32.png differ diff --git a/App/src/main/resources/icon96.png b/App/src/main/resources/icon96.png new file mode 100644 index 0000000..ca08d14 Binary files /dev/null and b/App/src/main/resources/icon96.png differ