diff --git a/.gitignore b/.gitignore index 812329a46a..02e68c5554 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,21 @@ Icon Network Trash Folder Temporary Items .apdisk +<<<<<<< Updated upstream +.gradle/ +======= + +# Auto-generated files +firmware/config/boards/**/generated_ts_name_by_pin.cpp +*_generated.h +auto_generated_*.h +firmware/fome_generated.mk +firmware/tunerstudio/generated/*.ini +firmware/generated/.gitignore +java_console/generated/ +fome-can-bridge/src/main/java/com/rusefi/config/generated/FuelComputer.java +fome-can-bridge/src/main/java/com/rusefi/config/generated/IgnitionState.java +fome-can-bridge/src/main/java/com/rusefi/config/generated/readme.md +firmware/generated/ +/firmware/ext +>>>>>>> Stashed changes diff --git a/.vscode/settings.json b/.vscode/settings.json index a57705b48d..0bbe17d899 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,4 +32,5 @@ "editor.formatOnSave": true }, "C_Cpp.clang_format_style": "file", + "java.compile.nullAnalysis.mode": "automatic", } diff --git a/build-bridge.sh b/build-bridge.sh new file mode 100755 index 0000000000..12189fc7e7 --- /dev/null +++ b/build-bridge.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BRIDGE_DIR="$SCRIPT_DIR/fome-can-bridge" +OUTPUT_DIR="$SCRIPT_DIR/current_bridge_output" + +echo "Building FOME CAN Bridge..." +java -version + +cd "$BRIDGE_DIR" +./gradlew shadowJar + +[ -d "$OUTPUT_DIR" ] || mkdir -p "$OUTPUT_DIR" + +JAR_PATH="$OUTPUT_DIR/fome-can-bridge.jar" + +if [ -f "$JAR_PATH" ]; then + echo "Build successful!" + echo "Artifact: $JAR_PATH" + ls -lh "$JAR_PATH" +else + echo "ERROR: Bridge JAR not found at $JAR_PATH!" + exit 1 +fi diff --git a/current_bridge_output/fome-can-bridge.jar b/current_bridge_output/fome-can-bridge.jar new file mode 100644 index 0000000000..27fa17e6a9 Binary files /dev/null and b/current_bridge_output/fome-can-bridge.jar differ diff --git a/fome-can-bridge/README.md b/fome-can-bridge/README.md new file mode 100644 index 0000000000..8be6395605 --- /dev/null +++ b/fome-can-bridge/README.md @@ -0,0 +1,50 @@ +# FOME CAN Bridge + +The FOME CAN Bridge is a lightweight, high-performance proxy application designed to bridge Controller Area Network (CAN) traffic from hardware (ECUs) over to a desktop tuning software like TunerStudio. + +## Building the Bridge +The bridge avoids legacy bloat and compiles rapidly using Gradle. + +### Prerequisites +- Java JDK 11 or higher +- Bash environment (Linux, macOS, or Git Bash for Windows) + +### Compilation +To compile the bridge simply execute the provided build script located in the repository root: +```bash +./build-bridge.sh +``` +This automatically compiles the project and generates a standalone FAT JAR with all dependencies bundled here: `current_bridge_output/fome-can-bridge.jar`. + +Alternatively, if you'd like to build the project directly using gradle: +```bash +cd fome-can-bridge +./gradlew shadowJar +``` + +## Running the Bridge + +Once compiled, you can launch the application by running the executable JAR: +```bash +java -jar current_bridge_output/fome-can-bridge.jar +``` +This will launch the graphical interface where you can specify your ECU parameters and select the connected adapter. + +### Linux (SocketCAN) +The application natively supports SocketCAN (`can0`, `can1`, `vcan0`, etc) on Linux. +Alternatively, there is a convenient helper script provided that runs the JAR and takes CLI arguments directly to bypass the UI: +```bash +./fome_bridge.sh can0 802 809 +``` +*(Usage: `./fome_bridge.sh [device_name] [rx_id] [tx_id]`)* + +### Windows (COM Ports / SLCAN) +On Windows, you can connect using a standard COM port (e.g. `COM1`, `COM3`, etc) mapping directly to a serial CAN bridge using the SLCAN protocol (such as CANable flashed with slcan firmware). Select your COM port within the graphical interface, and click "Start Server" to host the TunerStudio networking socket on port 29001. + +## TunerStudio Integration +Once the console indicates that the TCP port is listening on `29001`, open TunerStudio: +1. Open your ECU's Project. +2. Go to `Communications -> Settings`. +3. Set the communication type to `TCP/IP`. +4. Port "29001", IP Address "localhost" +5. Run "Test port" to verify communication. diff --git a/fome-can-bridge/build.gradle b/fome-can-bridge/build.gradle new file mode 100644 index 0000000000..2d17787fa9 --- /dev/null +++ b/fome-can-bridge/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'java' + id 'com.gradleup.shadow' version '8.3.5' +} + +group 'com.fome' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + // Serial port access (SLCAN) + implementation 'com.fazecast:jSerialComm:2.11.4' + + // SocketCAN (Linux) + implementation group: 'tel.schich', name: 'javacan-core', version: '3.2.4' + implementation group: 'tel.schich', name: 'javacan-core', version: '3.2.4', classifier: 'x86_64' + + // Annotations + implementation 'org.jetbrains:annotations:16.0.1' + implementation 'net.jcip:jcip-annotations:1.0' + implementation 'com.google.code.findbugs:jsr305:3.0.2' + + // UI + implementation 'com.formdev:flatlaf:3.4.1' + implementation 'com.miglayout:miglayout-swing:11.3' +} + +jar { + manifest { + attributes( + 'Main-Class': 'com.fome.canbridge.Main' + ) + } +} + +shadowJar { + archiveBaseName.set('fome-can-bridge') + archiveClassifier.set('') + archiveVersion.set('') + destinationDirectory.set(file("../current_bridge_output")) +} diff --git a/fome-can-bridge/gradle/wrapper/gradle-wrapper.jar b/fome-can-bridge/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..c1962a79e2 Binary files /dev/null and b/fome-can-bridge/gradle/wrapper/gradle-wrapper.jar differ diff --git a/fome-can-bridge/gradle/wrapper/gradle-wrapper.properties b/fome-can-bridge/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..a3a17a86c9 --- /dev/null +++ b/fome-can-bridge/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/fome-can-bridge/gradlew b/fome-can-bridge/gradlew new file mode 100755 index 0000000000..aeb74cbb43 --- /dev/null +++ b/fome-can-bridge/gradlew @@ -0,0 +1,245 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/fome-can-bridge/settings.gradle b/fome-can-bridge/settings.gradle new file mode 100644 index 0000000000..19a47c9cda --- /dev/null +++ b/fome-can-bridge/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'fome-can-bridge' diff --git a/fome-can-bridge/src/main/java/com/devexperts/logging/DefaultLogging.java b/fome-can-bridge/src/main/java/com/devexperts/logging/DefaultLogging.java new file mode 100644 index 0000000000..b7acfed7bc --- /dev/null +++ b/fome-can-bridge/src/main/java/com/devexperts/logging/DefaultLogging.java @@ -0,0 +1,162 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * Logging implementation that uses {@link java.util.logging} logging facilities. + */ +class DefaultLogging { + + Map configure() { + // Heuristically check if there was an attempt to manually configure logging + if (getProperty("java.util.logging.config.class", null) != null || + getProperty("java.util.logging.config.file", null) != null || + !hasDefaultHandlers(Logger.getLogger(""))) + { + return Collections.emptyMap(); // logging was already manually configured + } + return configureLogFile(getProperty(Logging.LOG_FILE_PROPERTY, null)); + } + + private boolean hasDefaultHandlers(Logger root) { + // Default configuration is 1 ConsoleHandler with SimpleFormatter and INFO level + Handler[] handlers = root.getHandlers(); + if (handlers.length != 1) + return false; + Handler handler = handlers[0]; + return handler.getClass() == ConsoleHandler.class && + handler.getFormatter().getClass() == SimpleFormatter.class && + handler.getLevel() == Level.INFO; + } + + Map configureLogFileAndConsole(String log_file) { + Map result = configureLogFile(log_file); + initAndAdd(new ConsoleHandler(), Level.ALL, getRootLogger()); + return result; + } + + Map configureLogFile(String log_file) { + Logger root = getRootLogger(); + Map errors = new LinkedHashMap(); + + try { + // Don't reset configuration. Retain all manually configured loggers, but + // reconfigure the root logger, which (as we checked) has a default configuration with + // 1 ConsoleHandler with SimpleFormatter and INFO level + for (Handler handler : root.getHandlers()) + root.removeHandler(handler); + + // configure "log" file or console + Handler handler = null; + if (log_file != null) { + try { + handler = new FileHandler(log_file, getLimit(Logging.LOG_MAX_FILE_SIZE_PROPERTY, errors), 2, true); + } catch (IOException e) { + errors.put(log_file, e); + } + } + if (handler == null) + handler = new ConsoleHandler(); + initAndAdd(handler, Level.ALL, root); + + // configure "err" file + String err_file = getProperty(Logging.ERR_FILE_PROPERTY, null); + if (err_file != null) { + try { + handler = new FileHandler(err_file, getLimit(Logging.ERR_MAX_FILE_SIZE_PROPERTY, errors), 2, true); + initAndAdd(handler, Level.WARNING, root); + } catch (IOException e) { + errors.put(err_file, e); + } + } + } catch (SecurityException e) { + // ignore -- does not have persmission to change configuration + } + return errors; + } + + private void initAndAdd(Handler handler, Level all, Logger root) { + handler.setFormatter(new LogFormatter()); + handler.setLevel(all); + root.addHandler(handler); + } + + private Logger getRootLogger() { + return Logger.getLogger(""); + } + + Object getPeer(String name) { + return Logger.getLogger(name); + } + + String getName(Object peer) { + return ((Logger)peer).getName(); + } + + boolean debugEnabled(Object peer) { + return ((Logger)peer).isLoggable(Level.FINE); + } + + void setDebugEnabled(Object peer, boolean debug_enabled) { + ((Logger)peer).setLevel(debug_enabled ? Level.ALL : Level.INFO); + } + + void log(Object peer, Level level, String msg, Throwable t) { + ((Logger)peer).log(level, msg, t); + } + + // ========== Utility methods ========== + + /** + * Safely, from security point of view, gets system property. + */ + static String getProperty(String key, String def) { + // For applets we need to be ready for security exception in getProperty() call + try { + return System.getProperty(key, def); + } catch (SecurityException e) { + return def; + } + } + + static int getLimit(String key, Map errors) { + String value = getProperty(key, Logging.DEFAULT_MAX_FILE_SIZE).trim(); + int multiplier = 1; + if (value.endsWith("K") || value.endsWith("k")) { + multiplier = 1024; + value = value.substring(0, value.length() - 1); + } else if (value.endsWith("M") || value.endsWith("m")) { + multiplier = 1024 * 1024; + value = value.substring(0, value.length() - 1); + } else if (value.endsWith("G") || value.endsWith("g")) { + multiplier = 1024 * 1024 * 1024; + value = value.substring(0, value.length() - 1); + } + try { + return Integer.valueOf(value) * multiplier; + } catch (NumberFormatException e) { + errors.put(key, e); + return 0; + } + } +} diff --git a/fome-can-bridge/src/main/java/com/devexperts/logging/FileLogger.java b/fome-can-bridge/src/main/java/com/devexperts/logging/FileLogger.java new file mode 100644 index 0000000000..171884dfd3 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/devexperts/logging/FileLogger.java @@ -0,0 +1,40 @@ +package com.devexperts.logging; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class FileLogger { + public static final String DIR = "logs/"; + + public static final String date = getDate(); + public static final String DATE_PATTERN = "yyyy-MM-dd_HH_mm_ss_SSS"; + + static { + try { + FileLogger.createFolderIfNeeded(); + Logging.configureLogFile(FileLogger.DIR + "efi_log_" + date + ".log"); + } catch (Throwable e) { + e.printStackTrace(System.err); + System.err.println("Error starting logging" + e); + } + } + + public static void init() { + // just need to touch the class + } + + public static String getDate() { + return new SimpleDateFormat(FileLogger.DATE_PATTERN).format(new Date()); + } + + public static void createFolderIfNeeded() { + File dir = new File(DIR); + if (dir.exists()) + return; + System.out.println("Creating " + DIR); + boolean created = dir.mkdirs(); + if (!created) + throw new IllegalStateException("Failed to create " + DIR + " folder"); + } +} diff --git a/fome-can-bridge/src/main/java/com/devexperts/logging/LogFormatter.java b/fome-can-bridge/src/main/java/com/devexperts/logging/LogFormatter.java new file mode 100644 index 0000000000..2c3eb86700 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/devexperts/logging/LogFormatter.java @@ -0,0 +1,185 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import com.devexperts.util.TimeUtil; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Calendar; +import java.util.TimeZone; +import java.util.function.BiConsumer; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Thread-safe formatter for log messages. + * It is used for formatting log4j, log4j2 and {@link java.util.logging} log messages. + * Performs conversion of thread names according to patterns specified in configuration file. + *

+ * If the system property {@code logformatter.properties} is specified, then it should contain + * an URL to the configuration file. Otherwise, configuration is loaded from classpath, using + * /META-INF/logformatter.properties file. + *

+ * The format of the file is: + *

    + *
  • pattern=replacement + *
  • "Pattern" uses regular expression syntax. + * You can escape "=" in pattern with "\=" syntax. + *
  • "Replacement" string can refer to capturing groups defined in pattern using usual + * regular expression syntax "$n", where "n" stands for the number of the group. + *
  • ISO 8859-1 encoding is used. + *
  • Empty lines and lines starting with # or ! are ignored. + * Lines containing wrong patterns are ignored. + *
+ * Configuration file is loaded during class loading. + * Any errors which occur in this class are printed in {@code System.err}. + *

+ * Sample configuration file can be found in etc/logformatter.properties. + *

+ * This class is not intended to be used standalone. + * It is a part of implementation of {@link com.devexperts.logging} package. + * + * @see DetailedLogLayout + * @see DxFeedPatternLayout + */ +@ThreadSafe +public class LogFormatter extends Formatter { + public static final String CONFIG_FILE_PROPERTY = "logformatter.properties"; + public static final String DEFAULT_CONFIG_FILE = "/META-INF/logformatter.properties"; + + private static final String LINE_SEP = DefaultLogging.getProperty("line.separator", "\n"); + private static final BiConsumer STRING_FORMAT_CONSUMER = (s, sb) -> sb.append(s); + + // ============== Instance ================ + private final ThreadLocal formatter; + + public LogFormatter() { + this(TimeZone.getDefault()); + } + + public LogFormatter(TimeZone zone) { + formatter = ThreadLocal.withInitial(() -> new LocalFormatter(zone)); + } + + /** + * Used by {@link java.util.logging} logging. + * Formats messages with the same format as for log4j. + */ + @Override + public String format(LogRecord record) { + String s = format(getLevelChar(record.getLevel()), + record.getMillis(), Thread.currentThread().getName(), + record.getLoggerName(), formatMessage(record)); + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + sw.write(s); + record.getThrown().printStackTrace(new PrintWriter(sw)); + s = sw.toString(); + } + return s; + } + + /** + * Formats log message. + * + * @return Formatted message. + * @throws NullPointerException when threadName, loggerName, or msg are {@code null}. + */ + public String format(char levelChar, long time, String threadName, String loggerName, String msg) { + StringBuilder out = formatter.get().appendTo; + out.setLength(0); + try { + format(levelChar, time, threadName, loggerName, STRING_FORMAT_CONSUMER, msg, out); + return out.toString(); + } finally { + boolean trim = out.length() > 1000; + out.setLength(0); + if (trim) + out.trimToSize(); + } + } + + void format(char levelChar, long time, String threadName, String loggerName, + BiConsumer msgConsumer, Object msg, StringBuilder out) + { + out.append(levelChar).append(" "); + formatter.get().appendTime(time, out); + out.append(" "); + int threadPosition = out.length(); + out.append("["); + out.append(ThreadNameFormatter.formatThreadName(time, threadName)); + out.append("] "); + out.append(loggerName, loggerName.lastIndexOf('.') + 1, loggerName.length()); + out.append(" - "); + int messagePosition = out.length(); + msgConsumer.accept(msg, out); + out.append(LINE_SEP); + if (out.length() > messagePosition && out.charAt(messagePosition) == '\b') + out.delete(threadPosition, messagePosition + 1); + } + + static char getLevelChar(Level level) { + int levelInt = level.intValue(); + if (levelInt <= Level.FINEST.intValue()) + return 'T'; + if (levelInt <= Level.FINE.intValue()) + return 'D'; + if (levelInt <= Level.INFO.intValue()) + return 'I'; + if (levelInt <= Level.WARNING.intValue()) + return 'W'; + return 'E'; + } + + private static class LocalFormatter { + private final Calendar calendar; + private final char[] timeBuffer = new char[17]; // fixed-size buffer for time data "yyMMdd HHmmss.SSS" + private final StringBuilder appendTo = new StringBuilder(); + + private long translatedMinute; + + private LocalFormatter(TimeZone zone) { + calendar = Calendar.getInstance(zone); + Arrays.fill(timeBuffer, 0, 17, ' '); + timeBuffer[13] = '.'; + } + + private void appendTime(long time, StringBuilder out) { + if (time < translatedMinute || time >= translatedMinute + TimeUtil.MINUTE) { + // set year, month, day, hour and minute + calendar.setTimeInMillis(time); + translatedMinute = calendar.getTime().getTime() - calendar.get(Calendar.SECOND) * 1000 - calendar.get(Calendar.MILLISECOND); + print2(0, calendar.get(Calendar.YEAR)); + print2(2, calendar.get(Calendar.MONTH) + 1); + print2(4, calendar.get(Calendar.DAY_OF_MONTH)); + print2(7, calendar.get(Calendar.HOUR_OF_DAY)); + print2(9, calendar.get(Calendar.MINUTE)); + } + + // set seconds and milliseconds + int millis = (int)(time - translatedMinute); + print2(11, millis / 1000); + print2(14, millis / 10); + timeBuffer[16] = (char)('0' + millis % 10); + out.append(timeBuffer); + } + + private void print2(int offset, int value) { + timeBuffer[offset] = (char)('0' + (value / 10) % 10); + timeBuffer[offset + 1] = (char)('0' + value % 10); + } + } +} diff --git a/fome-can-bridge/src/main/java/com/devexperts/logging/Logging.java b/fome-can-bridge/src/main/java/com/devexperts/logging/Logging.java new file mode 100644 index 0000000000..c18f98dae1 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/devexperts/logging/Logging.java @@ -0,0 +1,237 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Level; + +/** + * Main logging class. + * It supports use of both log4j and {@link java.util.logging} logging facilities. + *

First it tries to use log4j logging. If this attempt fails, it uses {@link java.util.logging} logging, + * so you'll always have some logging running. + *

Usage pattern: + *
public class SomeClass { + *
private static final Logging log = Logging.getLogging(SomeClass.class); + *
} + *
+ * + * @see Log4jLogging + * @see DefaultLogging + * @see LogFormatter + */ +public class Logging { + private static final boolean TRACE_LOGGING = Logging.class.desiredAssertionStatus(); + + private static final int FINEST_INT = Level.FINEST.intValue(); + private static final int FINE_INT = Level.FINE.intValue(); + + public static final String LOG_CLASS_NAME = "log.className"; + public static final String LOG_FILE_PROPERTY = "log.file"; + public static final String ERR_FILE_PROPERTY = "err.file"; + public static final String LOG_MAX_FILE_SIZE_PROPERTY = "log.maxFileSize"; + public static final String ERR_MAX_FILE_SIZE_PROPERTY = "err.maxFileSize"; + public static final String DEFAULT_MAX_FILE_SIZE = "900M"; + + private static final ConcurrentMap INSTANCES = new ConcurrentHashMap<>(); + private static final DefaultLogging IMPL = configure(DefaultLogging.getProperty(LOG_CLASS_NAME, "com.devexperts.logging.Log4j2Logging")); + + public static Logging getLogging(Class clazz) { + return getLogging(clazz.getName()); + } + + private static Logging getLogging(String name) { + Logging logging = INSTANCES.get(name); + if (logging != null) + return logging; + INSTANCES.putIfAbsent(name, new Logging(name)); + return INSTANCES.get(name); + } + + /** + * Programmatically reconfigures logging to a specified file. This method + * overrides the value of {@link #LOG_FILE_PROPERTY} system property. + */ + public static void configureLogFile(String log_file) { + reportErrors(IMPL, IMPL.configureLogFileAndConsole(log_file)); + } + + // ========== Instance ========= + + private final Object peer; + + /** + * This constructor is designed for abstract framework classes like BeanBase or + * DAOBase that extend Logging to decorate messages by + * overriding {@link #decorateLogMessage(String)} method. + */ + protected Logging() { + peer = IMPL.getPeer(getClass().getName()); + } + + protected Logging(String name) { + peer = IMPL.getPeer(name); + } + + /** + * Returns category name of this logging. + */ + public final String getName() { + return IMPL.getName(peer); + } + + /** + * Changes default {@link #debugEnabled()} behaviour for this logging instance. + * Use this method to turn off debugging information for classes that do not + * need to print their debugging information in production environment. + */ + public final void configureDebugEnabled(boolean defaultDebugEnabled) { + IMPL.setDebugEnabled(peer, Boolean.valueOf(DefaultLogging.getProperty(getName() + ".debug", + String.valueOf(defaultDebugEnabled)))); + } + + public final boolean debugEnabled() { + return IMPL.debugEnabled(peer); + } + + public final void trace(String message) { + log(Level.FINEST, message, null); + } + + public final void debug(String message) { + log(Level.FINE, message, null); + } + + public final void debug(String message, Throwable t) { + log(Level.FINE, message, t); + } + + public final void info(String message) { + log(Level.INFO, message, null); + } + + public final void info(String message, Throwable t) { + log(Level.INFO, message, t); + } + + public final void warn(String message) { + log(Level.WARNING, message, null); + } + + public final void warn(String message, Throwable t) { + log(Level.WARNING, message, t); + } + + public final void error(String message) { + log(Level.SEVERE, message, null); + } + + public final void error(String message, Throwable t) { + log(Level.SEVERE, message, t); + } + + public final RuntimeException log(RuntimeException e) { + log(Level.SEVERE, e.getMessage(), e); + return e; + } + + /** + * Decorates log message (reformatting, auditing, etc). + * This method is invoked one time for each logging event. + */ + protected String decorateLogMessage(String msg) { + return msg; + } + + // ========== Internal ========== + + private void log(Level level, String msg, Throwable t) { + if (TRACE_LOGGING) + TraceLogging.log(getName(), level, decorateLogMessage(msg), t); + int levelInt = level.intValue(); + if (levelInt <= FINEST_INT) + return; // trace never goes to regular log + if (levelInt <= FINE_INT && !IMPL.debugEnabled(peer)) + return; + try { + msg = decorateLogMessage(msg == null ? "" : msg); + } catch (Throwable tt) { + IMPL.log(peer, Level.SEVERE, "Failed to decorate log message", tt); + } + IMPL.log(peer, level, msg, t); + } + + /** + * At first tries to use logging from passed class name. If this attempt fails, tries to use log4j logging. + * If this attempt fails, it uses log4j2 logging. If this attempt fails, it uses {@link java.util.logging} logging. + * + * @return Logging implementation + */ + private static DefaultLogging configure(String className) { + DefaultLogging impl = null; + Map errors = new LinkedHashMap<>(); + if (!className.isEmpty()) { + try { + impl = (DefaultLogging)Class.forName(className).newInstance(); + errors.putAll(impl.configure()); + } catch (Throwable t) { + // failed to configure with passed class name + impl = null; + if (!(t instanceof LinkageError) && !(t.getCause() instanceof LinkageError)) { +// errors.put(className + " link", new IllegalStateException(t)); + } + } + } +/* + if (impl == null) { + try { + impl = (DefaultLogging)Class.forName("com.devexperts.logging.Log4jLogging").newInstance(); + errors.putAll(impl.configure()); + } catch (Throwable t) { + // failed to configure log4j + impl = null; + // LinkageError means that log4j is not found at all, otherwise it was found but our config is wrong + if (!(t instanceof LinkageError) && !(t.getCause() instanceof LinkageError)) { + errors.put("log4j link", new IllegalStateException(t)); + } + } + } + */ + if (impl == null) { + try { + impl = (DefaultLogging)Class.forName("com.devexperts.logging.Log4j2Logging").newInstance(); + errors.putAll(impl.configure()); + } catch (Throwable t) { + // failed to configure log4j2 + impl = null; + if (!(t instanceof LinkageError) && !(t.getCause() instanceof LinkageError)) { + //errors.put("log4j2 link", new IllegalStateException(t)); + } + } + } + if (impl == null) { + impl = new DefaultLogging(); + errors.putAll(impl.configure()); + } + + reportErrors(impl, errors); + return impl; + } + + private static void reportErrors(DefaultLogging impl, Map errors) { + for (Map.Entry entry : errors.entrySet()) + impl.log(impl.getPeer("config"), Level.SEVERE, entry.getKey(), entry.getValue()); + } +} diff --git a/fome-can-bridge/src/main/java/com/devexperts/logging/ThreadNameFormatter.java b/fome-can-bridge/src/main/java/com/devexperts/logging/ThreadNameFormatter.java new file mode 100644 index 0000000000..9bf571b17e --- /dev/null +++ b/fome-can-bridge/src/main/java/com/devexperts/logging/ThreadNameFormatter.java @@ -0,0 +1,40 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Simplified ThreadNameFormatter — returns thread names as-is. + * The original used IndexedSet for complex pattern-based renaming; bridge doesn't need that. + */ +class ThreadNameFormatter implements Comparable { + + private static final ConcurrentHashMap NAME_CACHE = new ConcurrentHashMap<>(); + + static String formatThreadName(long time, String threadName) { + return NAME_CACHE.computeIfAbsent(threadName, k -> k); + } + + final String thread_name; + final String replacement_name; + long last_time; + + ThreadNameFormatter(String thread_name, String replacement_name) { + this.thread_name = thread_name; + this.replacement_name = replacement_name; + } + + public int compareTo(ThreadNameFormatter o) { + return Long.compare(last_time, o.last_time); + } +} diff --git a/fome-can-bridge/src/main/java/com/devexperts/logging/TraceLogging.java b/fome-can-bridge/src/main/java/com/devexperts/logging/TraceLogging.java new file mode 100644 index 0000000000..860655a901 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/devexperts/logging/TraceLogging.java @@ -0,0 +1,146 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +/** + * This is a small in-memory cyclic buffer to keep the log for debugging concurrency problems in order to + * reconstruct what was going on immediately before tests crashes, without actually writing all debug logging + * to the log file or console normally. It is used by some tests when assertions are enabled, + * otherwise this class should not even be loaded. + */ +public class TraceLogging { + private static final int STOPPED_INDEX = -1; + private static final int SIZE = Integer.parseInt(System.getProperty("TraceLogging.size", "4096")); // must be power of 2 + private static final int MASK = SIZE - 1; + + private static final int THREAD_OFS = 0; + private static final int NAME_OFS = 1; + private static final int LEVEL_OFS = 2; + private static final int MSG_OFS = 3; + private static final int THROWABLE_OFS = 4; + private static final int DATA_CNT = 5; + + private static final long[] timeQueue = new long[SIZE]; + private static final Object[] dataQueue = new Object[SIZE * DATA_CNT]; + private static final AtomicInteger index = new AtomicInteger(STOPPED_INDEX); + private static int lastIndex = STOPPED_INDEX; + + static { + if ((SIZE & MASK) != 0) + throw new RuntimeException("Size must be a power of two"); + } + + /** + * Restarts trace logging from scratch (old log entries are cleared). + * Use it at the beginning of the test. + */ + public static synchronized void restart() { + Arrays.fill(dataQueue, null); + lastIndex = STOPPED_INDEX; + index.compareAndSet(STOPPED_INDEX, 0); + } + + /** + * Stops trace logging. + */ + public static void stop() { + stopIndex(-1); + } + + /** + * Adds log entry. It is invoked from {@link Logging#log(Level, String, Throwable)} method when + * assertions are enabled. + */ + public static void log(String loggerName, Level level, String msg, Throwable t) { + append(nextIndex(), loggerName, level, msg, t); + } + + /** + * Adds last entry and stops trace logging. + */ + public static void logAndStop(Class where, String msg) { + logAndStop(where, msg, null); + } + + /** + * Adds last entry with exception and stops trace logging. + */ + public static void logAndStop(Class where, String msg, Throwable t) { + append(stopIndex(0), where.getName(), Level.INFO, msg, t); + } + + private static void append(int i, String loggerName, Level level, String msg, Throwable t) { + if (i < 0) + return; + timeQueue[i] = System.currentTimeMillis(); + dataQueue[i * DATA_CNT + THREAD_OFS] = Thread.currentThread(); + dataQueue[i * DATA_CNT + NAME_OFS] = loggerName; + dataQueue[i * DATA_CNT + LEVEL_OFS] = level; + dataQueue[i * DATA_CNT + MSG_OFS] = msg; + dataQueue[i * DATA_CNT + THROWABLE_OFS] = t; + } + + /** + * Dumps last entries from this trace log. + * It should be called after {@link #stop()} or {@link #logAndStop(Class, String)}. + * It does nothing if called more than once after stop or before stop. + */ + public static synchronized void dump(PrintStream out, String title) { + int stop = lastIndex; + if (stop < 0) + return; + lastIndex = STOPPED_INDEX; + LogFormatter formatter = new LogFormatter(); + out.println("********************** Dump trace log for " + title); + int i = stop; + do { + i = (i + 1) & MASK; + Thread thread = (Thread)dataQueue[i * DATA_CNT + THREAD_OFS]; + if (thread == null) + continue; + String loggerName = (String)dataQueue[i * DATA_CNT + NAME_OFS]; + Level level = (Level)dataQueue[i * DATA_CNT + LEVEL_OFS]; + String msg = (String)dataQueue[i * DATA_CNT + MSG_OFS]; + Throwable t = (Throwable)dataQueue[i * DATA_CNT + THROWABLE_OFS]; + long time = timeQueue[i]; + out.print("* "); + out.print(formatter.format(LogFormatter.getLevelChar(level), time, thread.getName(), loggerName, msg)); + if (t != null) + t.printStackTrace(out); + } while (i != stop); + out.println("********************** Done trace log for " + title); + } + + private static int nextIndex() { + int result; + do { + result = index.get(); + } while (result >= 0 && !index.compareAndSet(result, (result + 1) & MASK)); + return result; + } + + private static synchronized int stopIndex(int lastOffset) { + int result; + do { + result = index.get(); + } while (result >= 0 && !index.compareAndSet(result, STOPPED_INDEX)); + if (result >= 0) + lastIndex = (result + lastOffset) & MASK; + return result; + } + +} diff --git a/fome-can-bridge/src/main/java/com/devexperts/util/TimeUtil.java b/fome-can-bridge/src/main/java/com/devexperts/util/TimeUtil.java new file mode 100644 index 0000000000..7b1d4875c6 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/devexperts/util/TimeUtil.java @@ -0,0 +1,9 @@ +package com.devexperts.util; + +/** + * Minimal time constants used by the logging library. + */ +public class TimeUtil { + public static final long SECOND = 1000L; + public static final long MINUTE = 60 * SECOND; +} diff --git a/fome-can-bridge/src/main/java/com/fome/canbridge/Main.java b/fome-can-bridge/src/main/java/com/fome/canbridge/Main.java new file mode 100644 index 0000000000..6f20a0b2b0 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/fome/canbridge/Main.java @@ -0,0 +1,107 @@ +package com.fome.canbridge; + +import com.rusefi.io.serial.AbstractIoStream; +import com.rusefi.io.tcp.BinaryProtocolProxy; +import com.rusefi.io.tcp.TcpConnector; +import com.rusefi.io.can.SocketCANIoStream; +import com.rusefi.io.can.SlCanIoStream; +import com.rusefi.ui.StatusConsumer; + +import java.io.IOException; +import java.util.concurrent.*; + +import com.fome.canbridge.view.BridgeGui; +import com.formdev.flatlaf.FlatDarkLaf; +import javax.swing.*; + +public class Main { + static { + // Workaround for Windows "Access Denied" when extracting to default %TEMP% + String tempDir = System.getProperty("user.home") + "/.jSerialComm"; + System.setProperty("java.io.tmpdir", tempDir); + java.io.File dir = new java.io.File(tempDir); + if (!dir.exists()) { + dir.mkdirs(); + } + } + + public static void main(String[] args) { + FlatDarkLaf.setup(); + if (args.length > 0) { + runCli(args); + } else { + SwingUtilities.invokeLater(() -> { + BridgeGui gui = new BridgeGui(); + gui.setOnConnect(config -> connect(config, gui)); + gui.setVisible(true); + }); + } + } + + private static void runCli(String[] args) { + System.out.println("FOME CAN Bridge starting (CLI mode)..."); + } + + private static void connect(BridgeGui.BridgeConfig config, BridgeGui gui) { + int retries = 3; + int timeoutSeconds = 10; + ExecutorService executor = Executors.newSingleThreadExecutor(); + + AbstractIoStream canStream = null; + + try { + for (int i = 1; i <= retries; i++) { + final int attempt = i; + gui.logLine("Connection attempt " + i + " of " + retries + " (Timeout: " + timeoutSeconds + "s)..."); + + Future future = executor.submit(() -> { + if ("SLCAN".equalsIgnoreCase(config.type)) { + return new SlCanIoStream(config.device, config.rxId, config.txId); + } else { + return new SocketCANIoStream(config.device, config.rxId, config.txId); + } + }); + + try { + canStream = future.get(timeoutSeconds, TimeUnit.SECONDS); + if (canStream != null) break; + } catch (TimeoutException e) { + gui.logLine("Attempt " + attempt + " timed out after " + timeoutSeconds + "s."); + future.cancel(true); + } catch (ExecutionException e) { + gui.logLine("Error: " + e.getCause().getMessage()); + } catch (InterruptedException e) { + gui.logLine("Connection interrupted."); + Thread.currentThread().interrupt(); + break; + } + + if (i < retries) { + try { Thread.sleep(2000); } catch (InterruptedException ignored) {} + } + } + } finally { + executor.shutdownNow(); + } + + if (canStream == null) { + gui.logLine("CRITICAL: Failed to connect after " + retries + " attempts."); + gui.setConnectEnabled(true); + return; + } + + gui.logLine("Connected successfully to CAN."); + try { + startProxy(canStream, gui); + gui.logLine("TCP Proxy started on port " + TcpConnector.DEFAULT_PORT); + } catch (IOException e) { + gui.logLine("Failed to start proxy: " + e.getMessage()); + gui.setConnectEnabled(true); + } + } + + private static void startProxy(AbstractIoStream tsStream, StatusConsumer status) throws IOException { + BinaryProtocolProxy.createProxy(tsStream, TcpConnector.DEFAULT_PORT, clientRequest -> { + }, status); + } +} diff --git a/fome-can-bridge/src/main/java/com/fome/canbridge/view/BridgeGui.java b/fome-can-bridge/src/main/java/com/fome/canbridge/view/BridgeGui.java new file mode 100644 index 0000000000..5e991bc108 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/fome/canbridge/view/BridgeGui.java @@ -0,0 +1,180 @@ +package com.fome.canbridge.view; + +import com.rusefi.ui.StatusConsumer; +import net.miginfocom.swing.MigLayout; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.util.function.Consumer; + +public class BridgeGui extends JFrame implements StatusConsumer { + private final JTextArea logArea = new JTextArea(); + private final JComboBox deviceCombo = new JComboBox<>(new String[]{"can0"}); + private final JButton scanButton = new JButton("Scan"); + private final JTextField txIdField = new JTextField("0x100"); + private final JTextField rxIdField = new JTextField("0x102"); + private final JComboBox typeCombo = new JComboBox<>(new String[]{"SLCAN", "SocketCAN"}); + + private final JButton connectButton = new JButton("Connect Bridge"); + private final JButton helpButton = new JButton("Help & Guide"); + + private Consumer onConnect; + + public static class BridgeConfig { + public String type; + public String device; + public int txId; + public int rxId; + } + + public BridgeGui() { + super("FOME CAN Bridge"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(600, 500); + setLayout(new BorderLayout()); + + JPanel mainPanel = new JPanel(new MigLayout("fillx, insets 20", "[right]10[grow]5[right]10[right]", "[]10[]10[]10[]10[]10[]20[]")); + mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + + // Header + JLabel header = new JLabel("Connection Settings"); + header.setFont(header.getFont().deriveFont(Font.BOLD, 16f)); + mainPanel.add(header, "span 2, wrap, gapbottom 15"); + + // Type + mainPanel.add(new JLabel("Adapter Type:")); + mainPanel.add(typeCombo, "growx"); + mainPanel.add(createHelpButton("SocketCAN: standard Linux driver.\nSLCAN: STM32 Virtual COM port, CANable, etc."), "wrap"); + + // Device + mainPanel.add(new JLabel("Device Name:")); + deviceCombo.setEditable(true); + mainPanel.add(deviceCombo, "growx"); + mainPanel.add(scanButton); + mainPanel.add(createHelpButton("Linux: can0, can1, etc.\nWindows COM Ports: COM1, COM2, etc.\nClick 'Scan' to find connected devices."), "wrap"); + + + // TX ID + mainPanel.add(new JLabel("PC -> ECU ID (TX):")); + mainPanel.add(txIdField, "growx"); + mainPanel.add(createHelpButton("The CAN ID that TunerStudio will use to send commands. Default: 0x100."), "wrap"); + + // RX ID + mainPanel.add(new JLabel("ECU -> PC ID (RX):")); + mainPanel.add(rxIdField, "growx"); + mainPanel.add(createHelpButton("The CAN ID that the ECU will use to send data back. Default: 0x102."), "wrap"); + + // Buttons + JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + btnPanel.add(helpButton); + btnPanel.add(connectButton); + mainPanel.add(btnPanel, "span 3, growx, gaptop 20"); + + // Log Area + logArea.setEditable(false); + logArea.setBackground(new Color(30, 30, 30)); + logArea.setForeground(new Color(100, 200, 100)); + logArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); + logArea.setBorder(new EmptyBorder(5, 5, 5, 5)); + JScrollPane scroll = new JScrollPane(logArea); + scroll.setBorder(BorderFactory.createTitledBorder("System Log")); + + add(mainPanel, BorderLayout.NORTH); + add(scroll, BorderLayout.CENTER); + + connectButton.addActionListener(this::handleConnect); + helpButton.addActionListener(e -> showGeneralHelp()); + scanButton.addActionListener(e -> handleScan()); + typeCombo.addActionListener(e -> handleScan()); // Auto-scan on type change + + setLocationRelativeTo(null); + handleScan(); // Initial scan + } + + private void handleScan() { + String type = (String) typeCombo.getSelectedItem(); + new Thread(() -> { + java.util.List devices = new java.util.ArrayList<>(); + if ("SLCAN".equalsIgnoreCase(type)) { + try { + for (com.fazecast.jSerialComm.SerialPort port : com.fazecast.jSerialComm.SerialPort.getCommPorts()) { + devices.add(port.getSystemPortName()); + } + } catch (Exception e) { + devices.add("COM1"); + } + } else { + devices.add("can0"); + } + SwingUtilities.invokeLater(() -> { + deviceCombo.removeAllItems(); + for (String d : devices) deviceCombo.addItem(d); + }); + }).start(); + } + + private JButton createHelpButton(String tip) { + JButton btn = new JButton("?"); + btn.setMargin(new Insets(2, 6, 2, 6)); + btn.setToolTipText(tip); + btn.addActionListener(e -> JOptionPane.showMessageDialog(this, tip, "Parameter Help", JOptionPane.INFORMATION_MESSAGE)); + return btn; + } + + private void showGeneralHelp() { + String helpText = "" + + "

FOME CAN Bridge Guide

" + + "

This tool allows TunerStudio to connect to your ECU over the CAN bus instead of USB/Serial.

" + + "

Requirements:

" + + "
    " + + "
  • Linux: SocketCAN (can0) or SLCAN (/dev/ttyACM0).
  • " + + "
  • Windows: SLCAN (COM port, e.g. CANable).
  • " + + "
  • Firmware: Yellowbox firmware with 'TS over CAN' enabled.
  • " + + "
" + + "

How to use:

" + + "
    " + + "
  1. Configure the IDs to match your ECU settings.
  2. " + + "
  3. Click 'Connect Bridge'.
  4. " + + "
  5. In TunerStudio, select Connection Type: TCP/IP.
  6. " + + "
  7. Set Server: localhost and Port: 29001.
  8. " + + "
" + + ""; + JOptionPane.showMessageDialog(this, helpText, "FOME CAN Bridge - General Help", JOptionPane.PLAIN_MESSAGE); + } + + private void handleConnect(ActionEvent e) { + if (onConnect != null) { + try { + BridgeConfig config = new BridgeConfig(); + config.type = (String) typeCombo.getSelectedItem(); + config.device = (String) deviceCombo.getSelectedItem(); + + config.txId = Integer.decode(txIdField.getText()); + config.rxId = Integer.decode(rxIdField.getText()); + + connectButton.setEnabled(false); + logLine("Starting connection..."); + new Thread(() -> onConnect.accept(config)).start(); + } catch (Exception ex) { + logLine("Error: " + ex.getMessage()); + } + } + } + + @Override + public void logLine(String status) { + SwingUtilities.invokeLater(() -> { + logArea.append(status + "\n"); + logArea.setCaretPosition(logArea.getDocument().getLength()); + }); + } + + public void setOnConnect(Consumer onConnect) { + this.onConnect = onConnect; + } + + public void setConnectEnabled(boolean enabled) { + SwingUtilities.invokeLater(() -> connectButton.setEnabled(enabled)); + } +} diff --git a/fome-can-bridge/src/main/java/com/opensr5/io/DataListener.java b/fome-can-bridge/src/main/java/com/opensr5/io/DataListener.java new file mode 100644 index 0000000000..ac396ee92c --- /dev/null +++ b/fome-can-bridge/src/main/java/com/opensr5/io/DataListener.java @@ -0,0 +1,8 @@ +package com.opensr5.io; + +/** + * Callback for incoming data bytes. + */ +public interface DataListener { + void onDataArrived(byte[] data); +} diff --git a/fome-can-bridge/src/main/java/com/opensr5/io/WriteStream.java b/fome-can-bridge/src/main/java/com/opensr5/io/WriteStream.java new file mode 100644 index 0000000000..ca2ad7de44 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/opensr5/io/WriteStream.java @@ -0,0 +1,27 @@ +package com.opensr5.io; + +import java.io.IOException; + +/** + * Basic write operations for a stream. + */ +public interface WriteStream { + void write(byte[] bytes) throws IOException; + void flush() throws IOException; + + default void writeShort(int v) throws IOException { + byte[] packet = new byte[2]; + packet[0] = (byte) (v >> 8); + packet[1] = (byte) v; + write(packet); + } + + default void writeInt(int v) throws IOException { + byte[] packet = new byte[4]; + packet[0] = (byte) (v >> 24); + packet[1] = (byte) (v >> 16); + packet[2] = (byte) (v >> 8); + packet[3] = (byte) v; + write(packet); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/CompatibleFunction.java b/fome-can-bridge/src/main/java/com/rusefi/CompatibleFunction.java new file mode 100644 index 0000000000..7e80aa63a4 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/CompatibleFunction.java @@ -0,0 +1,8 @@ +package com.rusefi; + +/** + * reducing Android level down from 24 + */ +public interface CompatibleFunction { + R apply(T t); +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/Listener.java b/fome-can-bridge/src/main/java/com/rusefi/Listener.java new file mode 100644 index 0000000000..064e0f8f85 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/Listener.java @@ -0,0 +1,10 @@ +package com.rusefi; + +public interface Listener { + void onResult(T parameter); + + static Listener empty() { + return parameter -> { + }; + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/NamedThreadFactory.java b/fome-can-bridge/src/main/java/com/rusefi/NamedThreadFactory.java new file mode 100644 index 0000000000..674f2d60d9 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/NamedThreadFactory.java @@ -0,0 +1,30 @@ +package com.rusefi; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class NamedThreadFactory implements ThreadFactory { + private final AtomicInteger counter = new AtomicInteger(); + private final String name; + private final boolean isDaemon; + + public NamedThreadFactory(String name) { + this(name, false); + } + + public NamedThreadFactory(String name, boolean isDaemon) { + this.name = name; + this.isDaemon = isDaemon; + } + + @Override + public Thread newThread(@NotNull Runnable r) { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setName(name + counter.incrementAndGet()); + t.setDaemon(isDaemon); + return t; + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/Timeouts.java b/fome-can-bridge/src/main/java/com/rusefi/Timeouts.java new file mode 100644 index 0000000000..3dd5f00eb6 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/Timeouts.java @@ -0,0 +1,24 @@ +package com.rusefi; + +/** + * All i/o timeouts in one place + */ +public interface Timeouts { + int SECOND = 1000; + int MINUTE = 60 * SECOND; + int COMMAND_TIMEOUT_SEC = 10; // seconds + + /** + * The longest justified binary communication delay would be related to stm32f407 flash saving time + */ + int BINARY_IO_TIMEOUT = Integer.getInteger("BINARY_IO_TIMEOUT", 10) * SECOND; + int READ_IMAGE_TIMEOUT = 60 * SECOND; + + // local connection is happy with 1 second, but remote TCP is asking for 5 seconds + int CONNECTION_RESTART_DELAY = Integer.getInteger("CONNECTION_RESTART_DELAY", 10) * SECOND; + + int CMD_TIMEOUT = 20 * SECOND; + int SET_ENGINE_TIMEOUT = 60 * SECOND; + int TEXT_PULL_PERIOD = 100; +} + diff --git a/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/BinaryProtocol.java b/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/BinaryProtocol.java new file mode 100644 index 0000000000..154644f1ef --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/BinaryProtocol.java @@ -0,0 +1,36 @@ +package com.rusefi.binaryprotocol; + +import com.rusefi.config.generated.Integration; + +/** + * Slimmed-down BinaryProtocol — only provides findCommand() for proxy logging. + */ +public class BinaryProtocol { + + public static String findCommand(byte command) { + switch (command) { + case Integration.TS_COMMAND_F: + return "PROTOCOL"; + case Integration.TS_CRC_CHECK_COMMAND: + return "CRC_CHECK"; + case Integration.TS_BURN_COMMAND: + return "BURN"; + case Integration.TS_HELLO_COMMAND: + return "HELLO"; + case Integration.TS_READ_COMMAND: + return "READ"; + case Integration.TS_GET_TEXT: + return "TS_GET_TEXT"; + case Integration.TS_GET_FIRMWARE_VERSION: + return "GET_FW_VERSION"; + case Integration.TS_CHUNK_WRITE_COMMAND: + return "WRITE_CHUNK"; + case Integration.TS_OUTPUT_COMMAND: + return "TS_OUTPUT_COMMAND"; + case Integration.TS_RESPONSE_OK: + return "TS_RESPONSE_OK"; + default: + return "command " + (char) command + "/" + command; + } + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/IncomingDataBuffer.java b/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/IncomingDataBuffer.java new file mode 100644 index 0000000000..30737586ca --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/IncomingDataBuffer.java @@ -0,0 +1,234 @@ +package com.rusefi.binaryprotocol; + +import com.devexperts.logging.Logging; +import com.rusefi.Timeouts; + +import com.rusefi.util.HexBinary; +import com.rusefi.io.serial.AbstractIoStream; +import etch.util.CircularByteBuffer; +import net.jcip.annotations.ThreadSafe; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +import static com.devexperts.logging.Logging.getLogging; +import static com.rusefi.binaryprotocol.IoHelper.*; + +/** + * Thread-safe byte queue with blocking {@link #waitForBytes} method + *

+ * Andrey Belomutskiy, (c) 2013-2020 + * 6/20/2015. + */ +@ThreadSafe +public class IncomingDataBuffer { + private static final Logging log = getLogging(IncomingDataBuffer.class); + + static { + log.configureDebugEnabled(false); + } + + private static final int BUFFER_SIZE = 32768; + private final String loggingPrefix; + + /** + * buffer for queued response bytes from controller + */ + private final CircularByteBuffer cbb = new CircularByteBuffer(BUFFER_SIZE); + private final AbstractIoStream.StreamStats streamStats; + + public IncomingDataBuffer(String loggingPrefix, AbstractIoStream.StreamStats streamStats) { + this.loggingPrefix = loggingPrefix; + this.streamStats = Objects.requireNonNull(streamStats, "streamStats"); + } + + public byte[] getPacket(String msg) throws EOFException { + return getPacket(Timeouts.BINARY_IO_TIMEOUT, msg, System.currentTimeMillis()); + } + + public byte[] getPacket(int timeoutMs, String msg) throws EOFException { + return getPacket(timeoutMs, msg, System.currentTimeMillis()); + } + + /** + * why does this method return NULL in case of timeout?! + * todo: there is a very similar BinaryProtocolServer#readPromisedBytes which throws exception in case of timeout + */ + public byte[] getPacket(int timeoutMs, String msg, long start) throws EOFException { + boolean isTimeout = waitForBytes(msg + " header", start, 2); + if (isTimeout) { + if (log.debugEnabled()) + log.info("Timeout waiting for header"); + return null; + } + + int packetSize = swap16(getShort()); + // if (log.debugEnabled()) + // log.debug(loggingPrefix + "Got packet size " + packetSize); + if (packetSize < 0) + return null; + + isTimeout = waitForBytes(timeoutMs, loggingPrefix + msg + " body", start, packetSize + 4); + if (isTimeout) + return null; + + byte[] packet = new byte[packetSize]; + getData(packet); + + // Compare the sent and computed CRCs, make sure they match! + int packetCrc = swap32(getInt()); + int actualCrc = getCrc32(packet); + if (actualCrc != packetCrc) { + String errorMessage = String.format("CRC mismatch on recv packet for %s: got %x but expected %x", msg, actualCrc, packetCrc); + System.out.println(errorMessage); + log.warn(errorMessage); + return null; + } + if (log.debugEnabled() && packet.length < 10) + log.info("got packet: " + Arrays.toString(packet)); + + onPacketArrived(); + // if (log.debugEnabled()) + // log.trace("packet arrived: " + Arrays.toString(packet) + ": crc OK"); + + return packet; + } + + public void onPacketArrived() { + streamStats.onPacketArrived(); + } + + public void addData(byte[] freshData) { + synchronized (cbb) { + if (cbb.size() - cbb.length() < freshData.length) { + log.error("buffer overflow not expected"); + throw new IllegalStateException("buffer overflow not expected"); + } + cbb.put(freshData); + cbb.notifyAll(); + } + if (log.debugEnabled()) + log.info(freshData.length + " byte(s) arrived, total " + cbb.length()); + } + + /** + * Blocking method which would wait for specified amount of data + * + * @return true in case of timeout, false if everything is fine + */ + public boolean waitForBytes(String loggingMessage, long startTimestamp, int count) { + return waitForBytes(Timeouts.BINARY_IO_TIMEOUT, loggingMessage, startTimestamp, count); + } + + /** + * @return true in case of timeout, false if we have received count of bytes + */ + public boolean waitForBytes(int timeoutMs, String loggingMessage, long startTimestamp, int count) { + //log.info(loggingMessage + ": waiting for " + count + " byte(s)"); + synchronized (cbb) { + while (cbb.length() < count) { + int timeout = (int) (startTimestamp + timeoutMs - System.currentTimeMillis()); + if (timeout <= 0) { + log.info(loggingMessage + ": timeout " + timeoutMs + "ms. Got only " + cbb.length() + " byte(s) while expecting " + count); + return true; // timeout. Sad face. + } + try { + cbb.wait(timeout); + } catch (InterruptedException e) { + return true; // thread thrown away, handling like a timeout + } + } + } + return false; // looks good! + } + + public int getPendingCount() { + synchronized (cbb) { + return cbb.length(); + } + } + + public int dropPending() { + // todo: when exactly do we need this logic? + synchronized (cbb) { + int pending = cbb.length(); + if (pending > 0) { + log.error("dropPending: Unexpected pending data: " + pending + " byte(s)"); + byte[] bytes = new byte[pending]; + cbb.get(bytes); + log.error("DROPPED FROM BUFFER: " + HexBinary.printByteArray(bytes)); + } + return pending; + } + } + + public int getByte() throws EOFException { + streamStats.onArrived(1); + synchronized (cbb) { + return cbb.getByte(); + } + } + + public int getShort() throws EOFException { + streamStats.onArrived(2); + synchronized (cbb) { + int result = cbb.getShort(); + if (log.debugEnabled()) + log.info("Consumed short, " + cbb.length() + " remaining"); + return result; + } + } + + public int getInt() throws EOFException { + streamStats.onArrived(4); + synchronized (cbb) { + int result = cbb.getInt(); + if (log.debugEnabled()) + log.info("Consumed int, " + cbb.length() + " remaining"); + return result; + } + } + + public void getData(byte[] packet) { + synchronized (cbb) { + cbb.get(packet); + if (log.debugEnabled()) + log.info(packet.length + " consumed, " + cbb.length() + " remaining"); + } + streamStats.onArrived(packet.length); + } + + public byte readByte() throws IOException { + return readByte(Timeouts.BINARY_IO_TIMEOUT); + } + + public byte readByte(int timeoutMs) throws IOException { + boolean isTimeout = waitForBytes(timeoutMs, loggingPrefix + "readByte", System.currentTimeMillis(), 1); + if (isTimeout) + throw new EOFException("Timeout in readByte " + timeoutMs); + return (byte) getByte(); + } + + public int readInt() throws EOFException { + boolean isTimeout = waitForBytes(loggingPrefix + "readInt", System.currentTimeMillis(), 4); + if (isTimeout) + throw new EOFException("Timeout in readInt "); + return swap32(getInt()); + } + + public short readShort() throws EOFException { + boolean isTimeout = waitForBytes(loggingPrefix + "readShort", System.currentTimeMillis(), 2); + if (isTimeout) + throw new EOFException("Timeout in readShort"); + return (short) swap16(getShort()); + } + + public void read(byte[] packet) throws EOFException { + boolean isTimeout = waitForBytes(loggingPrefix + "read", System.currentTimeMillis(), packet.length); + if (isTimeout) + throw new EOFException("Timeout while waiting for " + packet.length + " byte(s)"); + getData(packet); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/IoHelper.java b/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/IoHelper.java new file mode 100644 index 0000000000..0469d61325 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/binaryprotocol/IoHelper.java @@ -0,0 +1,85 @@ +package com.rusefi.binaryprotocol; + +import com.devexperts.logging.Logging; +import com.rusefi.config.generated.Integration; +import com.rusefi.util.HexBinary; + +import java.util.zip.CRC32; + +/** + * Utility methods for {@link BinaryProtocol} + * + * Andrey Belomutskiy, (c) 2013-2020 + * 3/6/2015 + */ +public class IoHelper { + private static final Logging log = Logging.getLogging(IoHelper.class); + + static { + log.configureDebugEnabled(false); + } + + public static int getCrc32(byte[] packet) { + return getCrc32(packet, 0, packet.length); + } + + private static int getCrc32(byte[] packet, int offset, int length) { + CRC32 c = new CRC32(); + c.update(packet, offset, length); + return (int) c.getValue(); + } + + /** + * this method adds two bytes for packet size before and four bytes for IoHelper after + */ + public static byte[] makeCrc32Packet(byte[] command) { + if (log.debugEnabled()) + log.info("makeCrc32Packet: raw packet " + HexBinary.printByteArray(command)); + byte[] packet = new byte[command.length + 6]; + + putShort(packet, 0, command.length); + + System.arraycopy(command, 0, packet, 2, command.length); + int crc = getCrc32(command); + + if (log.debugEnabled()) + log.info(String.format("makeCrc32Packet: CRC 0x%08X", crc)); + putInt(packet, packet.length - 4, crc); + return packet; + } + + public static int swap16(int x) { + return (((x & 0xff) << 8) | ((x >> 8) & 0xff)); + } + + public static int swap32(int x) { + return (((x) >> 24) & 0xff) | (((x) << 8) & 0xff0000) | (((x) >> 8) & 0xff00) | (((x) << 24) & 0xff000000); + } + + public static void putInt(byte[] packet, int offset, int value) { + packet[offset + 3] = (byte) value; + packet[offset + 2] = (byte) (value >> 8); + packet[offset + 1] = (byte) (value >> 16); + packet[offset] = (byte) (value >> 24); + } + + public static void putShort(byte[] packet, int offset, int value) { + packet[offset + 1] = (byte) value; + packet[offset] = (byte) (value >> 8); + } + + public static boolean checkResponseCode(byte[] response) { + return checkResponseCode(response, (byte) Integration.TS_RESPONSE_OK); + } + + /** + * @return true if packet has the expected response code + */ + public static boolean checkResponseCode(byte[] response, byte code) { + return response != null && response.length > 0 && response[0] == code; + } + + public static int getInt(byte firstByte, byte secondByte) { + return firstByte * 256 + (secondByte & 0xFF); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/config/generated/Integration.java b/fome-can-bridge/src/main/java/com/rusefi/config/generated/Integration.java new file mode 100644 index 0000000000..6dd54cf745 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/config/generated/Integration.java @@ -0,0 +1,18 @@ +// Slimmed down from auto-generated — only constants used by the CAN bridge +package com.rusefi.config.generated; + +public class Integration { + public static final char TS_BURN_COMMAND = 'B'; + public static final char TS_CHUNK_WRITE_COMMAND = 'C'; + public static final char TS_COMMAND_F = 'F'; + public static final char TS_CRC_CHECK_COMMAND = 'k'; + public static final char TS_GET_COMPOSITE_BUFFER_DONE_DIFFERENTLY = '8'; + public static final char TS_GET_FIRMWARE_VERSION = 'V'; + public static final char TS_GET_PROTOCOL_VERSION_COMMAND_F = 'F'; + public static final char TS_GET_TEXT = 'G'; + public static final char TS_HELLO_COMMAND = 'S'; + public static final char TS_OUTPUT_COMMAND = 'O'; + public static final String TS_PROTOCOL = "001"; + public static final char TS_READ_COMMAND = 'R'; + public static final int TS_RESPONSE_OK = 0; +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/config/generated/VariableRegistryValues.java b/fome-can-bridge/src/main/java/com/rusefi/config/generated/VariableRegistryValues.java new file mode 100644 index 0000000000..fa9559becf --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/config/generated/VariableRegistryValues.java @@ -0,0 +1,13 @@ +package com.rusefi.config.generated; + +/** + * Slimmed down to only the constants actually used by the CAN bridge. + * Originally auto-generated with 1800+ lines of unused constants. + */ +public class VariableRegistryValues { + public static final int CAN_ECU_SERIAL_RX_ID = 0x710; + public static final int CAN_ECU_SERIAL_TX_ID = 0x720; + public static final String TS_PROTOCOL = "001"; + public static final int TS_RESPONSE_BURN_OK = 4; + public static final String TS_SIGNATURE = "rusEFI master.2026.02.26.f407-discovery.3105081426"; +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/config/generated/readme.md b/fome-can-bridge/src/main/java/com/rusefi/config/generated/readme.md new file mode 100644 index 0000000000..422aa6458a --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/config/generated/readme.md @@ -0,0 +1,3 @@ +All classes in this folder are generated using ConfigGenerator + +See ```gen_config.bat``` \ No newline at end of file diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/ByteReader.java b/fome-can-bridge/src/main/java/com/rusefi/io/ByteReader.java new file mode 100644 index 0000000000..7680bf2c6c --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/ByteReader.java @@ -0,0 +1,46 @@ +package com.rusefi.io; + +import com.devexperts.logging.Logging; +import com.opensr5.io.DataListener; +import com.rusefi.io.serial.AbstractIoStream; +import com.rusefi.io.tcp.BinaryProtocolServer; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import static com.devexperts.logging.Logging.getLogging; + +public interface ByteReader { + Logging log = getLogging(ByteReader.class); + + static void runReaderLoop(String loggingPrefix, DataListener listener, ByteReader reader, AbstractIoStream ioStream) { + /** + * Threading of the whole input/output does not look healthy at all! + * + * @see #COMMUNICATION_EXECUTOR + */ + Executor threadExecutor = Executors.newSingleThreadExecutor(BinaryProtocolServer.getThreadFactory(loggingPrefix + "TCP reader")); + + threadExecutor.execute(() -> { + log.info(loggingPrefix + "Running TCP connection loop"); + + byte[] inputBuffer = new byte[64 * 1024]; + while (!ioStream.isClosed()) { + try { + int result = reader.read(inputBuffer); + if (result == -1) + throw new IOException("TcpIoStream: End of input?"); + listener.onDataArrived(Arrays.copyOf(inputBuffer, result)); + } catch (IOException e) { + log.error("TcpIoStream: End of connection " + e); + ioStream.close(); + return; + } + } + }); + } + + int read(byte[] buffer) throws IOException; +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/IoStream.java b/fome-can-bridge/src/main/java/com/rusefi/io/IoStream.java new file mode 100644 index 0000000000..1f8c1999de --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/IoStream.java @@ -0,0 +1,76 @@ +package com.rusefi.io; + +import com.opensr5.io.DataListener; +import com.opensr5.io.WriteStream; +import com.rusefi.binaryprotocol.BinaryProtocol; +import com.rusefi.binaryprotocol.IncomingDataBuffer; +import com.rusefi.binaryprotocol.IoHelper; +import com.rusefi.io.serial.AbstractIoStream; +import com.rusefi.io.serial.StreamStatistics; +import com.rusefi.io.tcp.BinaryProtocolServer; +import org.jetbrains.annotations.NotNull; + +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; + +import static com.devexperts.logging.Logging.getLogging; + +/** + * Physical bi-directional controller communication level + *

+ * Andrey Belomutskiy, (c) 2013-2020 + *

+ * 5/11/2015. + */ +public interface IoStream extends WriteStream, Closeable, StreamStatistics { + + @NotNull + default BinaryProtocolServer.Packet readPacket() throws IOException { + short length = readShort(); + return BinaryProtocolServer.readPromisedBytes(getDataBuffer(), length); + } + + default void sendPacket(BinaryProtocolServer.Packet packet) throws IOException { + writeShort(packet.getPacket().length); + write(packet.getPacket()); + writeInt(packet.getCrc()); + flush(); + onActivity(); + } + + long latestActivityTime(); + + void addCloseListener(Runnable listener); + + Object getIoLock(); + + void onActivity(); + + default void sendPacket(byte[] plainPacket) throws IOException { + if (plainPacket.length == 0) + throw new IllegalArgumentException("Empty packets are not valid."); + byte[] packet = IoHelper.makeCrc32Packet(plainPacket); + // todo: verbose mode printHexBinary(plainPacket)) + //log.debug(getLoggingPrefix() + "Sending packet " + BinaryProtocol.findCommand(plainPacket[0]) + " length=" + plainPacket.length); + write(packet); + flush(); + } + + /** + * @param listener would be invoked from unknown implementation-dependent thread + */ + void setInputListener(DataListener listener); + + boolean isClosed(); + + AbstractIoStream.StreamStats getStreamStats(); + + void close(); + + IncomingDataBuffer getDataBuffer(); + + default short readShort() throws EOFException { + return getDataBuffer().readShort(); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/can/HexUtil.java b/fome-can-bridge/src/main/java/com/rusefi/io/can/HexUtil.java new file mode 100644 index 0000000000..0740877873 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/can/HexUtil.java @@ -0,0 +1,61 @@ +package com.rusefi.io.can; + +public class HexUtil { + private static final String HEX_STRING = "0123456789ABCDEF"; + public static final byte[] HEX_BYTE_ARRAY = HEX_STRING.getBytes(); + public static final char[] HEX_CHAR_ARRAY = HexUtil.HEX_STRING.toCharArray(); + + public static byte[] asBytes(String hex) { + if (hex.indexOf(' ') >= 0) { + hex = hex.replaceAll(" ", ""); + } + if (hex.startsWith("0x")) { + hex = hex.substring(2); + } + return hexToBytes(hex); + } + + public static String asString(byte[] input) { + StringBuilder sb = new StringBuilder(); + for (byte b : input) { + int i = b & 0xff; + sb.append(HEX_STRING.charAt(i / 16)); + sb.append(HEX_STRING.charAt(i % 16)); + } + return sb.toString(); + } + + public static byte[] hexToBytes(CharSequence s) { + return hexToBytes(s, 0); + } + + private static byte[] hexToBytes(CharSequence s, int off) { + byte[] bs = new byte[off + (1 + s.length()) / 2]; + hexToBytes(s, bs, off); + return bs; + } + + public static void hexToBytes(CharSequence s, byte[] out, int off) throws NumberFormatException, IndexOutOfBoundsException { + int slen = s.length(); + if ((slen % 2) != 0) { + byte nibble = (byte) Character.digit(s.charAt(0), 16); + out[off] = nibble; + hexToBytes(s.subSequence(1, s.length()), out, off + 1); + return; + } + if (out.length < off + slen / 2) { + throw new IndexOutOfBoundsException("Output buffer too small for input (" + out.length + "<" + off + slen / 2 + ")"); + } + // Safe to assume the string is even length + byte firstNibble, secondNibble; + for (int i = 0; i < slen; i += 2) { + firstNibble = (byte) Character.digit(s.charAt(i), 16); + secondNibble = (byte) Character.digit(s.charAt(i + 1), 16); + if ((firstNibble < 0) || (secondNibble < 0)) { + // todo: huh? when does char produce a negative byte?! + throw new NumberFormatException(); + } + out[off + i / 2] = (byte) (firstNibble << 4 | secondNibble); + } + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/can/SlCanIoStream.java b/fome-can-bridge/src/main/java/com/rusefi/io/can/SlCanIoStream.java new file mode 100644 index 0000000000..a83024862e --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/can/SlCanIoStream.java @@ -0,0 +1,135 @@ +package com.rusefi.io.can; + +import com.fazecast.jSerialComm.SerialPort; +import com.fazecast.jSerialComm.SerialPortDataListener; +import com.fazecast.jSerialComm.SerialPortEvent; +import com.rusefi.binaryprotocol.IncomingDataBuffer; +import com.rusefi.io.can.isotp.IsoTpCanDecoder; +import com.rusefi.io.can.isotp.IsoTpConnector; +import com.rusefi.io.serial.AbstractIoStream; +import com.rusefi.util.HexBinary; +import java.io.IOException; +public class SlCanIoStream extends AbstractIoStream { + private final SerialPort sp; + private final IncomingDataBuffer dataBuffer; + private final int rxId; + private final int txId; + private final StringBuilder lineBuffer = new StringBuilder(); + private java.util.concurrent.CountDownLatch waitLatch; + + public SlCanIoStream(String port, int rxId, int txId) throws IOException { + this.rxId = rxId; + this.txId = txId; + try { + this.sp = SerialPort.getCommPort(port); + } catch (LinkageError e) { + throw new IOException("jSerialComm native library failed to load. This can happen due to an architecture mismatch or blocked temp directory: " + e.getMessage(), e); + } + this.sp.setBaudRate(2000000); + if (!this.sp.openPort()) { + throw new IOException("Failed to open SLCAN port: " + port); + } + + // Setup SLCAN: Close, Set Speed (S6=500k), Open + sendString("C"); + sendString("S6"); + sendString("O"); + + this.dataBuffer = createDataBuffer(); + } + + private final IsoTpCanDecoder canDecoder = new IsoTpCanDecoder() { + @Override + protected void onTpFirstFrame() { + // Send flow control to the ECU's RX ID + sendString("t" + String.format("%03X", txId) + "83000000000000000"); // 0x30 = Flow Control + } + }; + + + private void sendString(String s) { + byte[] bytes = (s + "\r").getBytes(); + sp.writeBytes(bytes, bytes.length); + } + + @Override + public void write(byte[] bytes) throws IOException { + IsoTpConnector connector = new IsoTpConnector(txId) { + @Override + public void sendCanData(byte[] total) { + // Formatting as 'tiiildd...' + String msg = String.format("t%03X%d%s", + canId(), + total.length, + HexUtil.asString(total)); + sendString(msg); + } + + @Override + public void receiveData() { + waitLatch = new java.util.concurrent.CountDownLatch(1); + try { + if (!waitLatch.await(500, java.util.concurrent.TimeUnit.MILLISECONDS)) { + // Timeout waiting for flow control, but we'll continue anyway + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }; + IsoTpConnector.sendStrategy(bytes, connector); + } + + @Override + public void setInputListener(com.opensr5.io.DataListener listener) { + sp.addDataListener(new SerialPortDataListener() { + @Override + public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; } + @Override + public void serialEvent(SerialPortEvent event) { + byte[] data = new byte[sp.bytesAvailable()]; + sp.readBytes(data, data.length); + for (byte b : data) { + if (b == '\r') { + processLine(lineBuffer.toString(), listener); + lineBuffer.setLength(0); + } else if (b != '\n') { + lineBuffer.append((char)b); + } + } + } + }); + } + + private void processLine(String line, com.opensr5.io.DataListener listener) { + if (line.startsWith("t")) { // Standard frame + try { + int id = Integer.parseInt(line.substring(1, 4), 16); + int len = Integer.parseInt(line.substring(4, 5)); + if (id == rxId) { + String hexData = line.substring(5, 5 + (len * 2)); + byte[] data = HexUtil.hexToBytes(hexData); + byte[] decoded = canDecoder.decodePacket(data); + if (decoded != null) { + if (waitLatch != null) { + waitLatch.countDown(); + } + listener.onDataArrived(decoded); + } + } + } catch (Exception e) { + // Malformed or different ID + } + } + } + + @Override + public IncomingDataBuffer getDataBuffer() { return dataBuffer; } + + @Override + public void close() { + sendString("C"); + sp.closePort(); + super.close(); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/can/SocketCANHelper.java b/fome-can-bridge/src/main/java/com/rusefi/io/can/SocketCANHelper.java new file mode 100644 index 0000000000..068a6e0b8a --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/can/SocketCANHelper.java @@ -0,0 +1,63 @@ +package com.rusefi.io.can; + +import com.devexperts.logging.Logging; +import com.rusefi.uds.CanConnector; +import org.jetbrains.annotations.NotNull; +import tel.schich.javacan.CanChannels; +import tel.schich.javacan.CanFrame; +import tel.schich.javacan.NetworkDevice; +import tel.schich.javacan.RawCanChannel; + +import java.io.IOException; + +import static com.devexperts.logging.Logging.getLogging; +import static tel.schich.javacan.CanFrame.FD_NO_FLAGS; +import static tel.schich.javacan.CanSocketOptions.RECV_OWN_MSGS; + +public class SocketCANHelper { + private static Logging log = getLogging(SocketCANIoStream.class); + + @NotNull + public static RawCanChannel createSocket() { + final RawCanChannel socket; + try { + NetworkDevice canInterface = NetworkDevice.lookup(System.getProperty("CAN_DEVICE_NAME", "can0")); + socket = CanChannels.newRawChannel(); + socket.bind(canInterface); + + socket.configureBlocking(true); // we want reader thread to wait for messages + socket.setOption(RECV_OWN_MSGS, false); + } catch (IOException e) { + throw new IllegalStateException("Error looking up", e); + } + return socket; + } + + public static void send(int id, byte[] payload, RawCanChannel channel) { + CanFrame packet = CanFrame.create(id, FD_NO_FLAGS, payload); + try { + channel.write(packet); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static CanConnector.CanPacket read(RawCanChannel socket) throws IOException { + CanFrame rx = socket.read(); + if (log.debugEnabled()) + log.debug("GOT " + String.format("%X", rx)); + byte[] raw = new byte[rx.getDataLength()]; + rx.getData(raw, 0, raw.length); + return new CanConnector.CanPacket() { + @Override + public int id() { + return rx.getId(); + } + + @Override + public byte[] payload() { + return raw; + } + }; + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/can/SocketCANIoStream.java b/fome-can-bridge/src/main/java/com/rusefi/io/can/SocketCANIoStream.java new file mode 100644 index 0000000000..d3d635160c --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/can/SocketCANIoStream.java @@ -0,0 +1,148 @@ +package com.rusefi.io.can; + +import com.devexperts.logging.Logging; +import com.opensr5.io.DataListener; +import com.rusefi.binaryprotocol.IncomingDataBuffer; +import com.rusefi.uds.CanConnector; +import com.rusefi.util.HexBinary; +import com.rusefi.io.IoStream; +import com.rusefi.io.can.isotp.IsoTpCanDecoder; +import com.rusefi.io.can.isotp.IsoTpConnector; +import com.rusefi.io.serial.AbstractIoStream; +import com.rusefi.io.tcp.BinaryProtocolServer; +import org.jetbrains.annotations.Nullable; +import tel.schich.javacan.RawCanChannel; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import static com.devexperts.logging.Logging.getLogging; +import static com.rusefi.config.generated.VariableRegistryValues.*; + +public class SocketCANIoStream extends AbstractIoStream { + static Logging log = getLogging(SocketCANIoStream.class); + private final IncomingDataBuffer dataBuffer; + private final RawCanChannel socket; + + private int rxId; + private int txId; + + private final IsoTpCanDecoder canDecoder = new IsoTpCanDecoder() { + @Override + protected void onTpFirstFrame() { + sendCanPacket(IsoTpCanDecoder.FLOW_CONTROL); + } + }; + + private final IsoTpConnector isoTpConnector; + + private void sendCanPacket(byte[] total) { + if (log.debugEnabled()) + log.debug("-------sendIsoTp " + total.length + " byte(s):"); + + if (log.debugEnabled()) + log.debug("Sending " + HexBinary.printHexBinary(total)); + + SocketCANHelper.send(isoTpConnector.canId(), total, socket); + } + + public SocketCANIoStream(String deviceName, int rxId, int txId) { + this.rxId = rxId; + this.txId = txId; + this.isoTpConnector = new IsoTpConnector(txId) { // PC TX ID -> ECU RX ID + @Override + public void sendCanData(byte[] total) { + sendCanPacket(total); + } + + @Override + public void receiveData() { + readHardwarePacket(rxId); + } + }; + System.setProperty("CAN_DEVICE_NAME", deviceName); + socket = SocketCANHelper.createSocket(); + // buffer could only be created once socket variable is not null due to callback + dataBuffer = createDataBuffer(); + } + + public SocketCANIoStream() { + this("can0", CAN_ECU_SERIAL_TX_ID, CAN_ECU_SERIAL_RX_ID); + } + + @Nullable + public static SocketCANIoStream create() { + return new SocketCANIoStream(); + } + + @Override + public void write(byte[] bytes) throws IOException { + IsoTpConnector.sendStrategy(bytes, isoTpConnector); + } + + @Override + public void setInputListener(DataListener listener) { + Executor threadExecutor = Executors.newSingleThreadExecutor(BinaryProtocolServer.getThreadFactory("SocketCAN reader")); + threadExecutor.execute(() -> { + while (!isClosed()) { + readOnePacket(listener); + } + }); + } + + private interface SocketListener { + void onPacket(CanConnector.CanPacket msg); + } + + private void readOnePacket(DataListener listener) { + readHardwarePacket((rx) -> { + byte[] decode = canDecoder.decodePacket(rx.payload()); + if (decode != null) { + listener.onDataArrived(decode); + } + }); + } + + private void readHardwarePacket(int expectedId) { + readHardwarePacket((rx) -> { + if (rx.id() != expectedId) { + return; + } + canDecoder.decodePacket(rx.payload()); + }); + } + + private void readHardwarePacket(SocketListener listener) { + try { + CanConnector.CanPacket rx = SocketCANHelper.read(socket); + listener.onPacket(rx); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private void readOnePacket_old(DataListener listener) { + try { + CanConnector.CanPacket rx = SocketCANHelper.read(socket); + if (rx.id() != this.rxId) { + if (log.debugEnabled()) + log.debug("Skipping non " + String.format("%X", this.rxId) + " packet: " + String.format("%X", rx.id())); + return; + } + byte[] decode = canDecoder.decodePacket(rx.payload()); + listener.onDataArrived(decode); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public IncomingDataBuffer getDataBuffer() { + return dataBuffer; + } + + public static IoStream createStream() { + return new SocketCANIoStream(); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpCanDecoder.java b/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpCanDecoder.java new file mode 100644 index 0000000000..247bf7a2c3 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpCanDecoder.java @@ -0,0 +1,108 @@ +package com.rusefi.io.can.isotp; + +import com.devexperts.logging.Logging; +import com.rusefi.util.HexBinary; + +import java.util.Arrays; + +import static com.rusefi.util.HexBinary.printHexBinary; + +/** + * ISO 15765-2 or ISO-TP (Transport Layer) CAN multi-frame decoder state + * + * @see IsoTpConnector + */ +public abstract class IsoTpCanDecoder { + public static final byte[] FLOW_CONTROL = {0x30, 0, 0, 0, 0, 0, 0, 0}; + private static final Logging log = Logging.getLogging(IsoTpCanDecoder.class); + + static { + log.configureDebugEnabled(false); + } + + private final static int ISO_TP_FRAME_FLOW_CONTROL = 3; + + private final static int FC_ContinueToSend = 0; + private final int isoHeaderByteIndex; + + private int waitingForNumBytes = 0; + private int waitingForFrameIndex = 0; + private boolean isComplete; + + public IsoTpCanDecoder() { + this(0); + } + + public IsoTpCanDecoder(int isoHeaderByteIndex) { + this.isoHeaderByteIndex = isoHeaderByteIndex; + } + + @Deprecated // warning: Some native CAN drivers use larger data buffers + public byte[] decodePacket(byte[] data) { + return decodePacket(data, data.length); + } + + public byte[] decodePacket(byte[] data, int dataSize) { +// log.info("Decoding " + printHexBinary(data)); + int frameType = (data[isoHeaderByteIndex] >> 4) & 0xf; + int numBytesAvailable; + int frameIdx; + int dataOffset; + switch (frameType) { + case IsoTpConstants.ISO_TP_FRAME_SINGLE: + numBytesAvailable = data[isoHeaderByteIndex] & 0xf; + dataOffset = isoHeaderByteIndex + 1; + this.waitingForNumBytes = 0; + if (log.debugEnabled()) + log.debug("ISO_TP_FRAME_SINGLE " + numBytesAvailable); + setComplete(true); + break; + case IsoTpConstants.ISO_TP_FRAME_FIRST: + this.waitingForNumBytes = ((data[isoHeaderByteIndex] & 0xf) << 8) | data[isoHeaderByteIndex + 1]; + if (log.debugEnabled()) + log.debug("Total expected: " + waitingForNumBytes); + this.waitingForFrameIndex = 1; + numBytesAvailable = Math.min(this.waitingForNumBytes, dataSize - 2 - isoHeaderByteIndex); + waitingForNumBytes -= numBytesAvailable; + dataOffset = isoHeaderByteIndex + 2; + onTpFirstFrame(); + break; + case IsoTpConstants.ISO_TP_FRAME_CONSECUTIVE: + frameIdx = data[isoHeaderByteIndex] & 0xf; + if (this.waitingForNumBytes < 0 || this.waitingForFrameIndex != frameIdx) { + throw new IllegalStateException("ISO_TP_FRAME_CONSECUTIVE: That's an abnormal situation, and we probably should react? waitingForNumBytes=" + waitingForNumBytes + " waitingForFrameIndex=" + waitingForFrameIndex + " frameIdx=" + frameIdx); + } + this.waitingForFrameIndex = (this.waitingForFrameIndex + 1) & 0xf; + numBytesAvailable = Math.min(this.waitingForNumBytes, dataSize - 1 - isoHeaderByteIndex); + dataOffset = isoHeaderByteIndex + 1; + waitingForNumBytes -= numBytesAvailable; + if (log.debugEnabled()) + log.debug("ISO_TP_FRAME_CONSECUTIVE Got " + numBytesAvailable + " byte(s), still expecting: " + waitingForNumBytes + " byte(s)"); + setComplete(waitingForNumBytes == 0); + break; + case ISO_TP_FRAME_FLOW_CONTROL: + int flowStatus = data[isoHeaderByteIndex] & 0xf; + int blockSize = data[isoHeaderByteIndex + 1]; + int separationTime = data[isoHeaderByteIndex + 2]; + if (flowStatus == FC_ContinueToSend && blockSize == 0 && separationTime == 0) + return new byte[0]; + throw new IllegalStateException("ISO_TP_FRAME_FLOW_CONTROL: should we just ignore the FC frame? " + flowStatus + " " + blockSize + " " + separationTime); + default: + throw new IllegalStateException("Unknown frame type"); + } + byte[] bytes = Arrays.copyOfRange(data, dataOffset, dataOffset + numBytesAvailable); + if (log.debugEnabled()) + log.debug(numBytesAvailable + " bytes(s) arrived in this packet: " + HexBinary.printByteArray(bytes)); + return bytes; + } + + protected abstract void onTpFirstFrame(); + + public void setComplete(boolean isComplete) { + this.isComplete = isComplete; + } + + public boolean isComplete() { + return isComplete; + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpConnector.java b/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpConnector.java new file mode 100644 index 0000000000..541a828e1d --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpConnector.java @@ -0,0 +1,78 @@ +package com.rusefi.io.can.isotp; + +import com.devexperts.logging.Logging; +import com.rusefi.util.HexBinary; +import org.jetbrains.annotations.NotNull; + +/** + * @see IsoTpCanDecoder for RX operations + */ +public abstract class IsoTpConnector { + private final static Logging log = Logging.getLogging(IsoTpConnector.class); + + private final int canId; + + protected IsoTpConnector(int canId) { + this.canId = canId; + } + + public int canId() { + return canId; + } + + public static void sendStrategy(byte[] bytes, IsoTpConnector connector) { + log.info("-------sendBytesToCan " + bytes.length + " byte(s):"); + + log.info(HexBinary.printHexBinary(bytes)); + + + // 1 frame + if (bytes.length <= 7) { + connector.sendCanFrame((IsoTpConstants.ISO_TP_FRAME_SINGLE << 4) | bytes.length, bytes, 0, bytes.length); + return; + } + + // multiple frames + // send the first header frame + connector.sendCanFrame((IsoTpConstants.ISO_TP_FRAME_FIRST << 4) | ((bytes.length >> 8) & 0x0f), bytes.length & 0xff, bytes, 0, 6); + // get a flow control frame + connector.receiveData(); + + // send the rest of the data + int idx = 1; + int offset = 6; + int remaining = bytes.length - 6; + while (remaining > 0) { + int len = Math.min(remaining, 7); + // send the consecutive frames + connector.sendCanFrame((IsoTpConstants.ISO_TP_FRAME_CONSECUTIVE << 4) | ((idx++) & 0x0f), bytes, offset, len); + offset += len; + remaining -= len; + } + } + + public void sendCanFrame(int hdr0, byte[] data, int offset, int dataLength) { + sendCanData(new byte[]{(byte) hdr0}, data, offset, dataLength); + } + + public void sendCanFrame(int hdr0, int hdr1, byte[] data, int dataOffset, int dataLength) { + sendCanData(new byte[]{(byte) hdr0, (byte) hdr1}, data, dataOffset, dataLength); + } + + private void sendCanData(byte[] hdr, byte[] data, int dataOffset, int dataLength) { + byte[] total = combineArrays(hdr, data, dataOffset, dataLength); + sendCanData(total); + } + + @NotNull + public static byte[] combineArrays(byte[] hdr, byte[] data, int dataOffset, int dataLength) { + byte[] total = new byte[hdr.length + dataLength]; + System.arraycopy(hdr, 0, total, 0, hdr.length); + System.arraycopy(data, dataOffset, total, hdr.length, dataLength); + return total; + } + + public abstract void sendCanData(byte[] total); + + public abstract void receiveData(); +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpConstants.java b/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpConstants.java new file mode 100644 index 0000000000..288a9f4549 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/can/isotp/IsoTpConstants.java @@ -0,0 +1,7 @@ +package com.rusefi.io.can.isotp; + +public class IsoTpConstants { + final static int ISO_TP_FRAME_SINGLE = 0; + final static int ISO_TP_FRAME_FIRST = 1; + final static int ISO_TP_FRAME_CONSECUTIVE = 2; +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/serial/AbstractIoStream.java b/fome-can-bridge/src/main/java/com/rusefi/io/serial/AbstractIoStream.java new file mode 100644 index 0000000000..801b7429fd --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/serial/AbstractIoStream.java @@ -0,0 +1,113 @@ +package com.rusefi.io.serial; + +import com.rusefi.binaryprotocol.IncomingDataBuffer; +import com.rusefi.io.IoStream; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class AbstractIoStream implements IoStream { + private boolean isClosed; + + // todo: this ioLock needs better documentation! + private final Object ioLock = new Object(); + + protected final StreamStats streamStats = new StreamStats(); + private final AtomicInteger bytesOut = new AtomicInteger(); + private long latestActivity; + private final CopyOnWriteArrayList closeListeners = new CopyOnWriteArrayList<>(); + + public IncomingDataBuffer createDataBuffer() { + IncomingDataBuffer incomingData = new IncomingDataBuffer(getClass().getSimpleName(), getStreamStats()); + setInputListener(incomingData::addData); + return incomingData; + } + + @Override + public StreamStats getStreamStats() { + return streamStats; + } + + @Override + public void close() { + if (isClosed) + return; + isClosed = true; + for (Runnable listener : closeListeners) + listener.run(); + } + + @Override + public void addCloseListener(Runnable listener) { + closeListeners.add(listener); + } + + @Override + public Object getIoLock() { + return ioLock; + } + + @Override + public void write(byte[] bytes) throws IOException { + bytesOut.addAndGet(bytes.length); + } + + @Override + public void flush() throws IOException { + } + + @Override + public void onActivity() { + latestActivity = System.currentTimeMillis(); + } + + public long latestActivityTime() { + return latestActivity; + } + + @Override + public boolean isClosed() { + return isClosed; + } + + public class StreamStats { + private long previousPacketArrivalTime; + private int maxPacketGap; + private final AtomicInteger totalBytesArrived = new AtomicInteger(); + + public long getPreviousPacketArrivalTime() { + return previousPacketArrivalTime; + } + + /** + * @return maximum time in MS between full valid packets received via this stream + */ + public int getMaxPacketGap() { + return maxPacketGap; + } + + public void onPacketArrived() { + long now = System.currentTimeMillis(); + if (previousPacketArrivalTime != 0) { + maxPacketGap = (int) Math.max(maxPacketGap, now - previousPacketArrivalTime); + } + previousPacketArrivalTime = now; + AbstractIoStream.this.onActivity(); + } + + public void onArrived(int length) { + totalBytesArrived.addAndGet(length); + } + } + + @Override + public int getBytesIn() { + return streamStats.totalBytesArrived.get(); + } + + @Override + public int getBytesOut() { + return bytesOut.get(); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/serial/RateCounter.java b/fome-can-bridge/src/main/java/com/rusefi/io/serial/RateCounter.java new file mode 100644 index 0000000000..530b6ede32 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/serial/RateCounter.java @@ -0,0 +1,23 @@ +package com.rusefi.io.serial; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Simple counter for rate tracking — slimmed down from the original. + */ +public class RateCounter { + private final AtomicInteger count = new AtomicInteger(); + + public void add() { + count.incrementAndGet(); + } + + public int getCurrentRate() { + return count.get(); + } + + @Override + public String toString() { + return String.valueOf(count.get()); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/serial/StreamStatistics.java b/fome-can-bridge/src/main/java/com/rusefi/io/serial/StreamStatistics.java new file mode 100644 index 0000000000..74a55bafa7 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/serial/StreamStatistics.java @@ -0,0 +1,7 @@ +package com.rusefi.io.serial; + +public interface StreamStatistics { + int getBytesIn(); + + int getBytesOut(); +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/tcp/BinaryProtocolProxy.java b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/BinaryProtocolProxy.java new file mode 100644 index 0000000000..6c85712422 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/BinaryProtocolProxy.java @@ -0,0 +1,98 @@ +package com.rusefi.io.tcp; + +import com.devexperts.logging.Logging; +import com.rusefi.CompatibleFunction; +import com.rusefi.Listener; +import com.rusefi.Timeouts; +import com.rusefi.binaryprotocol.BinaryProtocol; +import com.rusefi.binaryprotocol.IncomingDataBuffer; +import com.rusefi.binaryprotocol.IoHelper; +import com.rusefi.config.generated.Integration; +import com.rusefi.io.IoStream; +import com.rusefi.ui.StatusConsumer; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.net.Socket; + +import static com.devexperts.logging.Logging.getLogging; +import static com.rusefi.config.generated.Integration.TS_PROTOCOL; + +/** + * Takes any IoStream and exposes it as local TCP/IP server socket. + * Slimmed down — removed cloud proxy and NetworkConnector references. + */ +public class BinaryProtocolProxy { + private static final Logging log = getLogging(BinaryProtocolProxy.class); + public static final int USER_IO_TIMEOUT = 10 * Timeouts.MINUTE; + + /** + * @return starts a thread and returns a reference to ServerSocketReference + */ + public static ServerSocketReference createProxy(IoStream targetEcuSocket, int serverProxyPort, ClientApplicationActivityListener clientApplicationActivityListener, StatusConsumer statusConsumer) throws IOException { + CompatibleFunction clientSocketRunnableFactory = clientSocket -> () -> { + TcpIoStream clientStream = null; + try { + clientStream = new TcpIoStream("[[proxy]] ", clientSocket); + runProxy(targetEcuSocket, clientStream, clientApplicationActivityListener, USER_IO_TIMEOUT); + } catch (IOException e) { + statusConsumer.logLine("ERROR BinaryProtocolProxy::run " + e); + if (clientStream != null) { + try { clientStream.close(); } catch (Exception ignored) {} + } + } + }; + return BinaryProtocolServer.tcpServerSocket(serverProxyPort, "proxy", clientSocketRunnableFactory, Listener.empty(), statusConsumer); + } + + public interface ClientApplicationActivityListener { + ClientApplicationActivityListener VOID = (BinaryProtocolServer.Packet clientRequest) -> { + }; + + void onActivity(BinaryProtocolServer.Packet clientRequest); + } + + public static void runProxy(IoStream targetEcu, IoStream clientStream, ClientApplicationActivityListener listener, int timeoutMs) throws IOException { + while (!targetEcu.isClosed()) { + byte firstByte = clientStream.getDataBuffer().readByte(timeoutMs); + if (firstByte == Integration.TS_GET_PROTOCOL_VERSION_COMMAND_F) { + log.info("Responding to GET_PROTOCOL_VERSION with " + TS_PROTOCOL); + clientStream.write(TS_PROTOCOL.getBytes()); + clientStream.flush(); + continue; + } + BinaryProtocolServer.Packet clientRequest = readClientRequest(clientStream.getDataBuffer(), firstByte); + byte[] packet = clientRequest.getPacket(); + listener.onActivity(clientRequest); + + BinaryProtocolServer.Packet controllerResponse; + synchronized (targetEcu) { + sendToTarget(targetEcu, clientRequest); + controllerResponse = targetEcu.readPacket(); + } + + if (log.debugEnabled()) + log.debug("Relaying controller response length=" + controllerResponse.getPacket().length); + clientStream.sendPacket(controllerResponse); + } + } + + @NotNull + private static BinaryProtocolServer.Packet readClientRequest(IncomingDataBuffer in, byte firstByte) throws IOException { + byte secondByte = in.readByte(); + int length = IoHelper.getInt(firstByte, secondByte); + + return BinaryProtocolServer.readPromisedBytes(in, length); + } + + private static void sendToTarget(IoStream targetOutputStream, BinaryProtocolServer.Packet packet) throws IOException { + DataInputStream dis = new DataInputStream(new ByteArrayInputStream(packet.getPacket())); + byte command = (byte) dis.read(); + + if (log.debugEnabled()) + log.debug("Relaying client command " + BinaryProtocol.findCommand(command)); + targetOutputStream.sendPacket(packet); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/tcp/BinaryProtocolServer.java b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/BinaryProtocolServer.java new file mode 100644 index 0000000000..a46beb3ab9 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/BinaryProtocolServer.java @@ -0,0 +1,153 @@ +package com.rusefi.io.tcp; + +import com.devexperts.logging.Logging; +import com.rusefi.CompatibleFunction; +import com.rusefi.Listener; +import com.rusefi.NamedThreadFactory; +import com.rusefi.Timeouts; +import com.rusefi.binaryprotocol.IncomingDataBuffer; +import com.rusefi.binaryprotocol.IoHelper; +import com.rusefi.config.generated.Integration; +import com.rusefi.util.HexBinary; +import com.rusefi.ui.StatusConsumer; +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.devexperts.logging.Logging.getLogging; +import static com.rusefi.config.generated.VariableRegistryValues.*; + +/** + * TCP server that accepts TunerStudio connections and relays them. + * Slimmed down to only the proxy-essential functionality. + */ +public class BinaryProtocolServer { + private static final Logging log = getLogging(BinaryProtocolServer.class); + public static final String TS_OK = "\0"; + + static { + log.configureDebugEnabled(false); + } + + private final static ConcurrentHashMap THREAD_FACTORIES_BY_NAME = new ConcurrentHashMap<>(); + + /** + * Starts a TCP server socket that accepts client connections and dispatches them. + */ + public static ServerSocketReference tcpServerSocket(int port, String threadName, CompatibleFunction socketRunnableFactory, Listener serverSocketCreationCallback, StatusConsumer statusConsumer) throws IOException { + return tcpServerSocket(socketRunnableFactory, port, threadName, serverSocketCreationCallback, p -> { + ServerSocket serverSocket = new ServerSocket(p); + statusConsumer.logLine("ServerSocket " + p + " created. Feel free to point TS at IP Address 'localhost' port " + p); + return serverSocket; + }); + } + + public static ServerSocketReference tcpServerSocket(CompatibleFunction clientSocketRunnableFactory, int port, String threadName, Listener serverSocketCreationCallback, ServerSocketFunction nonSecureSocketFunction) throws IOException { + ThreadFactory threadFactory = getThreadFactory(threadName); + + Objects.requireNonNull(serverSocketCreationCallback, "serverSocketCreationCallback"); + ServerSocket serverSocket = nonSecureSocketFunction.apply(port); + + ServerSocketReference holder = new ServerSocketReference(serverSocket); + + serverSocketCreationCallback.onResult(null); + Runnable runnable = () -> { + while (!holder.isClosed()) { + final Socket clientSocket; + try { + clientSocket = serverSocket.accept(); + } catch (IOException e) { + log.info("Client socket closed right away " + e); + continue; + } + log.info("Accepting binary protocol proxy port connection on " + port); + Runnable clientRunnable = clientSocketRunnableFactory.apply(clientSocket); + Objects.requireNonNull(clientRunnable, "Runnable for " + clientSocket); + threadFactory.newThread(clientRunnable).start(); + } + }; + threadFactory.newThread(runnable).start(); + return holder; + } + + @NotNull + public static ThreadFactory getThreadFactory(String threadName) { + synchronized (THREAD_FACTORIES_BY_NAME) { + ThreadFactory threadFactory = THREAD_FACTORIES_BY_NAME.get(threadName); + if (threadFactory == null) { + threadFactory = new NamedThreadFactory(threadName); + THREAD_FACTORIES_BY_NAME.put(threadName, threadFactory); + } + return threadFactory; + } + } + + public static int getPacketLength(IncomingDataBuffer in, Handler protocolCommandHandler) throws IOException { + return getPacketLength(in, protocolCommandHandler, Timeouts.BINARY_IO_TIMEOUT); + } + + public static int getPacketLength(IncomingDataBuffer in, Handler protocolCommandHandler, int ioTimeout) throws IOException { + byte first = in.readByte(ioTimeout); + if (first == Integration.TS_GET_PROTOCOL_VERSION_COMMAND_F) { + protocolCommandHandler.handle(); + return 0; + } + byte secondByte = in.readByte(ioTimeout); + return IoHelper.getInt(first, secondByte); + } + + public static Packet readPromisedBytes(IncomingDataBuffer in, int length) throws IOException { + if (length <= 0) + throw new IOException("Unexpected packed length " + length); + byte[] packet = new byte[length]; + in.read(packet); + int crc = in.readInt(); + int fromPacket = IoHelper.getCrc32(packet); + if (crc != fromPacket) + throw new IOException("CRC mismatch crc=" + Integer.toString(crc, 16) + " vs packet=" + Integer.toString(fromPacket, 16) + " len=" + packet.length + " data: " + HexBinary.printHexBinary(packet)); + in.onPacketArrived(); + return new Packet(packet, crc); + } + + public interface Handler { + void handle() throws IOException; + } + + public static void handleProtocolCommand(Socket clientSocket) throws IOException { + if (log.debugEnabled()) + log.debug("Got plain GetProtocol F command"); + OutputStream outputStream = clientSocket.getOutputStream(); + outputStream.write(TS_PROTOCOL.getBytes()); + outputStream.flush(); + } + + public static class Packet { + private final byte[] packet; + private final int crc; + + public Packet(byte[] packet, int crc) { + this.packet = packet; + this.crc = crc; + } + + public byte[] getPacket() { + return packet; + } + + public int getCrc() { + return crc; + } + } + + public static class Context { + public int getTimeout() { + return Timeouts.BINARY_IO_TIMEOUT; + } + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/tcp/ServerSocketFunction.java b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/ServerSocketFunction.java new file mode 100644 index 0000000000..9297af2e75 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/ServerSocketFunction.java @@ -0,0 +1,8 @@ +package com.rusefi.io.tcp; + +import java.io.IOException; +import java.net.ServerSocket; + +public interface ServerSocketFunction { + ServerSocket apply(int port) throws IOException; +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/tcp/ServerSocketReference.java b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/ServerSocketReference.java new file mode 100644 index 0000000000..16bb9ce5b3 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/ServerSocketReference.java @@ -0,0 +1,25 @@ +package com.rusefi.io.tcp; + + + +import java.io.Closeable; +import java.net.ServerSocket; + +public class ServerSocketReference implements Closeable { + private final ServerSocket serverSocket; + private boolean isClosed; + + public ServerSocketReference(ServerSocket serverSocket) { + this.serverSocket = serverSocket; + } + + @Override + public void close() { + isClosed = true; + try { serverSocket.close(); } catch (Exception ignored) {} + } + + public boolean isClosed() { + return isClosed; + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/tcp/TcpConnector.java b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/TcpConnector.java new file mode 100644 index 0000000000..35fe040be5 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/TcpConnector.java @@ -0,0 +1,85 @@ +package com.rusefi.io.tcp; + +import com.devexperts.logging.Logging; + +import java.io.IOException; +import java.net.Socket; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; + +import static com.devexperts.logging.Logging.getLogging; + +/** + * @author Andrey Belomutskiy + * 3/3/14 + */ +public class TcpConnector { + private static final Logging log = getLogging(TcpConnector.class); + + public final static int DEFAULT_PORT = 29001; + public static final String LOCALHOST = "localhost"; + + public static boolean isTcpPort(String port) { + try { + getTcpPort(port); + return true; + } catch (InvalidTcpPort e) { + return false; + } + } + + /* + public static String doUnpackConfirmation(String message) { + String confirmation = message.substring(CommandQueue.CONFIRMATION_PREFIX.length()); + int index = confirmation.indexOf(":"); + if (index < 0) { + return null; + } + String number = confirmation.substring(index + 1); + int length; + try { + length = Integer.parseInt(number); + } catch (NumberFormatException e) { + return null; + } + if (length != index) { + return null; + } + return confirmation.substring(0, length); + } + */ + public static class InvalidTcpPort extends IOException { + } + + public static int getTcpPort(String port) throws InvalidTcpPort { + try { + String[] portParts = port.split(":"); + int index = (portParts.length == 1 ? 0 : 1); + return Integer.parseInt(portParts[index]); + } catch (NumberFormatException e) { + throw new InvalidTcpPort(); + } + } + + public static String getHostname(String port) { + String[] portParts = port.split(":"); + return (portParts.length == 1 ? LOCALHOST : portParts[0].length() > 0 ? portParts[0] : LOCALHOST); + } + + public static Collection getAvailablePorts() { + return isTcpPortOpened() ? Collections.singletonList("" + DEFAULT_PORT) : Collections.emptyList(); + } + + public static boolean isTcpPortOpened() { + long now = System.currentTimeMillis(); + try { + Socket s = new Socket(LOCALHOST, DEFAULT_PORT); + s.close(); + return true; + } catch (IOException e) { + log.info("Connection refused in getAvailablePorts(): simulator not running in " + (System.currentTimeMillis() - now) + "ms"); + return false; + } + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/io/tcp/TcpIoStream.java b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/TcpIoStream.java new file mode 100644 index 0000000000..e07884702c --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/io/tcp/TcpIoStream.java @@ -0,0 +1,95 @@ +package com.rusefi.io.tcp; + +import com.opensr5.io.DataListener; +import com.rusefi.binaryprotocol.IncomingDataBuffer; +import com.rusefi.io.ByteReader; +import com.rusefi.io.serial.AbstractIoStream; + +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.net.Socket; + +/** + * Andrey Belomutskiy, (c) 2013-2020 + * 5/11/2015. + */ +public class TcpIoStream extends AbstractIoStream { + private final InputStream input; + private final OutputStream output; + private final String loggingPrefix; + private final DisconnectListener disconnectListener; + @NotNull + private final Socket socket; + private final IncomingDataBuffer dataBuffer; + + public TcpIoStream(String loggingPrefix, Socket socket) throws IOException { + this(loggingPrefix, socket, DisconnectListener.VOID); + } + + public TcpIoStream(String loggingPrefix, Socket socket, DisconnectListener disconnectListener) throws IOException { + this.loggingPrefix = loggingPrefix; + this.disconnectListener = disconnectListener; + if (socket == null) + throw new NullPointerException("socket"); + this.socket = socket; + input = new BufferedInputStream(socket.getInputStream()); + output = new BufferedOutputStream(socket.getOutputStream()); + dataBuffer = createDataBuffer(); + } + + @NotNull + public static TcpIoStream open(String port) throws IOException { + int portPart = TcpConnector.getTcpPort(port); + String hostname = TcpConnector.getHostname(port); + Socket socket = new Socket(hostname, portPart); + + return new TcpIoStream("[start] " + port + " ", socket); + } + + @Override + public void close() { + // we need to guarantee only one onDisconnect invocation for retry logic to be healthy + synchronized (this) { + if (!isClosed()) { + super.close(); + disconnectListener.onDisconnect("on close"); + } + } + try { socket.close(); } catch (Exception ignored) {} + } + + @Override + public IncomingDataBuffer getDataBuffer() { + return dataBuffer; + } + + @Override + public void write(byte[] bytes) throws IOException { + super.write(bytes); + output.write(bytes); + } + + @Override + public String toString() { + return "TcpIoStream{" + loggingPrefix + '}'; + } + + @Override + public void flush() throws IOException { + super.flush(); + output.flush(); + } + + @Override + public void setInputListener(final DataListener listener) { + ByteReader.runReaderLoop(loggingPrefix, listener, input::read, this); + } + + public interface DisconnectListener { + DisconnectListener VOID = (String message) -> { + + }; + void onDisconnect(String message); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/uds/CanConnector.java b/fome-can-bridge/src/main/java/com/rusefi/uds/CanConnector.java new file mode 100644 index 0000000000..321efa41fa --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/uds/CanConnector.java @@ -0,0 +1,11 @@ +package com.rusefi.uds; + +/** + * Minimal CAN packet abstraction used by SocketCAN. + */ +public interface CanConnector { + interface CanPacket { + int id(); + byte[] payload(); + } +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/ui/StatusConsumer.java b/fome-can-bridge/src/main/java/com/rusefi/ui/StatusConsumer.java new file mode 100644 index 0000000000..d5611f362c --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/ui/StatusConsumer.java @@ -0,0 +1,17 @@ +package com.rusefi.ui; + +import com.devexperts.logging.Logging; + +import static com.devexperts.logging.Logging.getLogging; + +/** + * @see StatusWindow + */ +public interface StatusConsumer { + Logging log = getLogging(StatusConsumer.class); + + StatusConsumer ANONYMOUS = (status) -> log.info(status); + StatusConsumer VOID = (status) -> {}; + + void logLine(String status); +} diff --git a/fome-can-bridge/src/main/java/com/rusefi/util/HexBinary.java b/fome-can-bridge/src/main/java/com/rusefi/util/HexBinary.java new file mode 100644 index 0000000000..c06803c822 --- /dev/null +++ b/fome-can-bridge/src/main/java/com/rusefi/util/HexBinary.java @@ -0,0 +1,47 @@ +package com.rusefi.util; + +import com.rusefi.io.can.HexUtil; + +import java.util.List; + +public class HexBinary { + public static String printHexBinary(byte[] data) { + if (data == null) + return "(null)"; + char[] hexCode = HexUtil.HEX_CHAR_ARRAY; + + StringBuilder r = new StringBuilder(data.length * 2); + for (byte b : data) { + r.append(hexCode[(b >> 4) & 0xF]); + r.append(hexCode[(b & 0xF)]); + r.append(' '); + } + return r.toString(); + } + + public static String printHexBinary(List data) { + if (data == null) + return "(null)"; + char[] hexCode = HexUtil.HEX_CHAR_ARRAY; + + StringBuilder r = new StringBuilder(data.size() * 2); + for (byte b : data) { + r.append(hexCode[(b >> 4) & 0xF]); + r.append(hexCode[(b & 0xF)]); + r.append(' '); + } + return r.toString(); + } + + public static String printByteArray(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + if (Character.isJavaIdentifierPart(b)) { + sb.append((char) b); + } else { + sb.append(' '); + } + } + return printHexBinary(data) + sb; + } +} diff --git a/fome-can-bridge/src/main/java/etch/util/ByteBuffer.java b/fome-can-bridge/src/main/java/etch/util/ByteBuffer.java new file mode 100644 index 0000000000..5207051482 --- /dev/null +++ b/fome-can-bridge/src/main/java/etch/util/ByteBuffer.java @@ -0,0 +1,185 @@ +/* $Id$ + * + * Copyright 2007-2008 Cisco Systems Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package etch.util; + +import java.io.EOFException; +import java.nio.BufferOverflowException; + +/** + * Byte buffer with operation to support getting and putting + * bytes. + */ +abstract public class ByteBuffer { + /** + * @return true if the buffer is empty. + */ + public boolean isEmpty() { + return length() == 0; + } + + /** + * @return true if the buffer is full. + */ + public boolean isFull() { + return length() == size(); + } + + /** + * @return the amount of data the buffer can hold. + */ + abstract public int size(); + + /** + * @return the amount of data in the buffer. + */ + abstract public int length(); + + /** + * @return the next byte from the buffer. + * @throws EOFException if the buffer is empty. + */ + abstract public byte get() throws EOFException; + + /** + * Gets bytes from the buffer to fill buf. + * + * @param buf + * @return the length of data put into buf. + */ + public int get(byte[] buf) { + return get(buf, 0, buf.length); + } + + /** + * Gets bytes from the buffer to fill buf, starting at the specified + * position for the specified length. + * + * @param buf + * @param off + * @param len + * @return the length of data put into buf. + */ + public int get(byte[] buf, int off, int len) { + if (off < 0) + throw new IllegalArgumentException("off < 0"); + + if (len < 0) + throw new IllegalArgumentException("len < 0"); + + if (off + len > buf.length) + throw new IllegalArgumentException("off+len > buf.length"); + + if (len == 0) + return 0; + + int count = 0; + try { + while (len > 0) { + buf[off] = get(); + count++; + off++; + len--; + } + } catch (EOFException e) { + // nothing to do. + } + return count; + } + + /** + * Puts a byte into the buffer. + * + * @param b + * @throws BufferOverflowException if the buffer is full + */ + abstract public void put(byte b) throws BufferOverflowException; + + /** + * Puts the bytes from buf into the buffer. + * + * @param buf + * @return the amount of data copied into the buffer. + */ + public int put(byte[] buf) { + return put(buf, 0, buf.length); + } + + /** + * Puts the bytes from buf into the buffer, starting at the specified + * position for the specified length; + * + * @param buf + * @param off + * @param len + * @return the amount of data copied into the buffer. + */ + public int put(byte[] buf, int off, int len) { + if (off < 0) + throw new IllegalArgumentException("off < 0"); + + if (len < 0) + throw new IllegalArgumentException("len < 0"); + + if (off + len > buf.length) + throw new IllegalArgumentException("off+len > buf.length"); + + if (len == 0) + return 0; + + int count = 0; + try { + while (len > 0) { + put(buf[off]); + count++; + off++; + len--; + } + } catch (BufferOverflowException e) { + // nothing to do. + } + return count; + } + + /** + * Clears the buffer. + */ + abstract public void clear(); + + /** + * @return a little-endian 32-bit integer from the buffer. + * @throws EOFException + */ + public int getInt() throws EOFException { + int b0 = get() & 255; + int b1 = get() & 255; + int b2 = get() & 255; + int b3 = get() & 255; + return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); + } + + public int getShort() throws EOFException { + int b0 = get() & 255; + int b1 = get() & 255; + return (b0 | (b1 << 8)); + } + + public int getByte() throws EOFException { + int b0 = get() & 255; + return b0; + } +} \ No newline at end of file diff --git a/fome-can-bridge/src/main/java/etch/util/CircularByteBuffer.java b/fome-can-bridge/src/main/java/etch/util/CircularByteBuffer.java new file mode 100644 index 0000000000..1105b5eef5 --- /dev/null +++ b/fome-can-bridge/src/main/java/etch/util/CircularByteBuffer.java @@ -0,0 +1,97 @@ +/* $Id$ + * + * Copyright 2007-2008 Cisco Systems Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package etch.util; + +import net.jcip.annotations.NotThreadSafe; + +import java.io.EOFException; +import java.nio.BufferOverflowException; + +/** + * Description of CircularByteBuffer. + */ +@NotThreadSafe +public class CircularByteBuffer extends ByteBuffer { + private final int size; + + private final byte[] buf; + + private int length; + + private int nextGet; + + private int nextPut; + + /** + * Constructs the CircularByteBuffer. + * + * @param size + */ + public CircularByteBuffer(int size) { + this.size = size; + buf = new byte[size]; + } + + /** + * creates buffer FILLED with specified content + */ + public CircularByteBuffer(byte[] buf) { + length = size = buf.length; + this.buf = buf; + } + + @Override + public void clear() { + length = 0; + nextGet = 0; + nextPut = 0; + } + + @Override + public int size() { + return size; + } + + @Override + public int length() { + return length; + } + + @Override + public byte get() throws EOFException { + if (isEmpty()) + throw new EOFException(); + + length--; + byte b = buf[nextGet++]; + if (nextGet >= size) + nextGet = 0; + return b; + } + + @Override + public void put(byte b) throws BufferOverflowException { + if (isFull()) + throw new BufferOverflowException(); + + length++; + buf[nextPut++] = b; + if (nextPut >= size) + nextPut = 0; + } +} \ No newline at end of file diff --git a/fome_bridge.sh b/fome_bridge.sh new file mode 100644 index 0000000000..c55a07f144 --- /dev/null +++ b/fome_bridge.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Run the FOME CAN Bridge on Linux (SocketCAN) +# Usage: ./fome_bridge.sh [device_name] [rx_id] [tx_id] + +DEVICE=${1:-can0} +RX_ID=${2:-0x102} +TX_ID=${3:-0x100} + +java -Dcan.type=socketcan -Dcan.device=$DEVICE -Dcan.rx.id=$RX_ID -Dcan.tx.id=$TX_ID -jar fome-can-bridge.jar diff --git a/java_console/generated/bin/main/com/rusefi/config/generated/.gitignore b/java_console/generated/bin/main/com/rusefi/config/generated/.gitignore new file mode 100644 index 0000000000..5a5c169b23 --- /dev/null +++ b/java_console/generated/bin/main/com/rusefi/config/generated/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore +!readme.md diff --git a/java_console/generated/bin/main/com/rusefi/config/generated/readme.md b/java_console/generated/bin/main/com/rusefi/config/generated/readme.md new file mode 100644 index 0000000000..422aa6458a --- /dev/null +++ b/java_console/generated/bin/main/com/rusefi/config/generated/readme.md @@ -0,0 +1,3 @@ +All classes in this folder are generated using ConfigGenerator + +See ```gen_config.bat``` \ No newline at end of file diff --git a/java_console/io/src/main/java/com/rusefi/enums/auto_generated_enums.h b/java_console/io/src/main/java/com/rusefi/enums/auto_generated_enums.h deleted file mode 100644 index fdbbfcc3da..0000000000 --- a/java_console/io/src/main/java/com/rusefi/enums/auto_generated_enums.h +++ /dev/null @@ -1,55 +0,0 @@ -constexpr inline const char* getLive_data_e(live_data_e value) { - switch (value) { - case LDS_ac_control: - return "LDS_ac_control"; - case LDS_antilag_system_state: - return "LDS_antilag_system_state"; - case LDS_boost_control: - return "LDS_boost_control"; - case LDS_electronic_throttle: - return "LDS_electronic_throttle"; - case LDS_engine_state: - return "LDS_engine_state"; - case LDS_fan_control: - return "LDS_fan_control"; - case LDS_fuel_computer: - return "LDS_fuel_computer"; - case LDS_fuel_pump_control: - return "LDS_fuel_pump_control"; - case LDS_high_pressure_fuel_pump: - return "LDS_high_pressure_fuel_pump"; - case LDS_idle_state: - return "LDS_idle_state"; - case LDS_ignition_state: - return "LDS_ignition_state"; - case LDS_injector_model: - return "LDS_injector_model"; - case LDS_knock_controller: - return "LDS_knock_controller"; - case LDS_lambda_monitor: - return "LDS_lambda_monitor"; - case LDS_launch_control_state: - return "LDS_launch_control_state"; - case LDS_main_relay: - return "LDS_main_relay"; - case LDS_output_channels: - return "LDS_output_channels"; - case LDS_throttle_model: - return "LDS_throttle_model"; - case LDS_tps_accel_state: - return "LDS_tps_accel_state"; - case LDS_trigger_central: - return "LDS_trigger_central"; - case LDS_trigger_state: - return "LDS_trigger_state"; - case LDS_trigger_state_primary: - return "LDS_trigger_state_primary"; - case LDS_vvt: - return "LDS_vvt"; - case LDS_wall_fuel_state: - return "LDS_wall_fuel_state"; - case LDS_wideband_state: - return "LDS_wideband_state"; - } - return "unknown"; -} diff --git a/java_console/io/src/main/java/com/rusefi/io/can/IsoTpCanDecoder.java b/java_console/io/src/main/java/com/rusefi/io/can/IsoTpCanDecoder.java index d58eb0fa30..9c47d76bb7 100644 --- a/java_console/io/src/main/java/com/rusefi/io/can/IsoTpCanDecoder.java +++ b/java_console/io/src/main/java/com/rusefi/io/can/IsoTpCanDecoder.java @@ -7,9 +7,10 @@ /** * ISO 15765-2 or ISO-TP (Transport Layer) CAN multi-frame decoder state + * * @see IsoTpConnector */ -public class IsoTpCanDecoder { +public abstract class IsoTpCanDecoder { public static final byte[] FLOW_CONTROL = {0x30, 0, 0, 0, 0, 0, 0, 0}; private static final Logging log = Logging.getLogging(IsoTpCanDecoder.class); @@ -17,54 +18,71 @@ public class IsoTpCanDecoder { log.configureDebugEnabled(false); } - private final static int ISO_TP_FRAME_FLOW_CONTROL = 3; - final static int ISO_TP_FRAME_SINGLE = 0; - final static int ISO_TP_FRAME_FIRST = 1; - final static int ISO_TP_FRAME_CONSECUTIVE = 2; + public final static int ISO_TP_FRAME_FLOW_CONTROL = 3; + public final static int ISO_TP_FRAME_SINGLE = 0; + public final static int ISO_TP_FRAME_FIRST = 1; + public final static int ISO_TP_FRAME_CONSECUTIVE = 2; private final static int FC_ContinueToSend = 0; + private final int isoHeaderByteIndex; private int waitingForNumBytes = 0; private int waitingForFrameIndex = 0; + private boolean isComplete; + + public IsoTpCanDecoder() { + this(0); + } + + public IsoTpCanDecoder(int isoHeaderByteIndex) { + this.isoHeaderByteIndex = isoHeaderByteIndex; + } public byte[] decodePacket(byte[] data) { - int frameType = (data[0] >> 4) & 0xf; + return decodePacket(data, data.length); + } + + public byte[] decodePacket(byte[] data, int dataSize) { + int frameType = (data[isoHeaderByteIndex] >> 4) & 0xf; int numBytesAvailable; int frameIdx; int dataOffset; switch (frameType) { case ISO_TP_FRAME_SINGLE: - numBytesAvailable = data[0] & 0xf; - dataOffset = 1; + numBytesAvailable = data[isoHeaderByteIndex] & 0xf; + dataOffset = isoHeaderByteIndex + 1; this.waitingForNumBytes = 0; if (log.debugEnabled()) log.debug("ISO_TP_FRAME_SINGLE " + numBytesAvailable); + setComplete(true); break; case ISO_TP_FRAME_FIRST: - this.waitingForNumBytes = ((data[0] & 0xf) << 8) | data[1]; + this.waitingForNumBytes = ((data[isoHeaderByteIndex] & 0xf) << 8) | data[isoHeaderByteIndex + 1]; if (log.debugEnabled()) log.debug("Total expected: " + waitingForNumBytes); this.waitingForFrameIndex = 1; - numBytesAvailable = Math.min(this.waitingForNumBytes, 6); + numBytesAvailable = Math.min(this.waitingForNumBytes, dataSize - 2 - isoHeaderByteIndex); waitingForNumBytes -= numBytesAvailable; - dataOffset = 2; + dataOffset = isoHeaderByteIndex + 2; + onTpFirstFrame(); break; case ISO_TP_FRAME_CONSECUTIVE: - frameIdx = data[0] & 0xf; + frameIdx = data[isoHeaderByteIndex] & 0xf; if (this.waitingForNumBytes < 0 || this.waitingForFrameIndex != frameIdx) { throw new IllegalStateException("ISO_TP_FRAME_CONSECUTIVE: That's an abnormal situation, and we probably should react? waitingForNumBytes=" + waitingForNumBytes + " waitingForFrameIndex=" + waitingForFrameIndex + " frameIdx=" + frameIdx); } this.waitingForFrameIndex = (this.waitingForFrameIndex + 1) & 0xf; - numBytesAvailable = Math.min(this.waitingForNumBytes, 7); - dataOffset = 1; + numBytesAvailable = Math.min(this.waitingForNumBytes, dataSize - 1 - isoHeaderByteIndex); + dataOffset = isoHeaderByteIndex + 1; waitingForNumBytes -= numBytesAvailable; if (log.debugEnabled()) log.debug("ISO_TP_FRAME_CONSECUTIVE Got " + numBytesAvailable + " byte(s), still expecting: " + waitingForNumBytes + " byte(s)"); + setComplete(waitingForNumBytes == 0); break; case ISO_TP_FRAME_FLOW_CONTROL: - int flowStatus = data[0] & 0xf; - int blockSize = data[1]; - int separationTime = data[2]; + int flowStatus = data[isoHeaderByteIndex] & 0xf; + int blockSize = data[isoHeaderByteIndex + 1]; + int separationTime = data[isoHeaderByteIndex + 2]; if (flowStatus == FC_ContinueToSend && blockSize == 0 && separationTime == 0) return new byte[0]; throw new IllegalStateException("ISO_TP_FRAME_FLOW_CONTROL: should we just ignore the FC frame? " + flowStatus + " " + blockSize + " " + separationTime); @@ -76,4 +94,14 @@ public byte[] decodePacket(byte[] data) { log.debug(numBytesAvailable + " bytes(s) arrived in this packet: " + IoStream.printByteArray(bytes)); return bytes; } + + protected abstract void onTpFirstFrame(); + + public void setComplete(boolean isComplete) { + this.isComplete = isComplete; + } + + public boolean isComplete() { + return isComplete; + } } diff --git a/java_console/io/src/main/java/com/rusefi/io/can/IsoTpConnector.java b/java_console/io/src/main/java/com/rusefi/io/can/IsoTpConnector.java index 0148ef0303..af3e6a5fc9 100644 --- a/java_console/io/src/main/java/com/rusefi/io/can/IsoTpConnector.java +++ b/java_console/io/src/main/java/com/rusefi/io/can/IsoTpConnector.java @@ -8,8 +8,20 @@ * @see IsoTpCanDecoder */ public abstract class IsoTpConnector { + public abstract void receiveData(); + private final static Logging log = Logging.getLogging(IsoTpConnector.class); + private final int canId; + + protected IsoTpConnector(int canId) { + this.canId = canId; + } + + public int canId() { + return canId; + } + public static void sendStrategy(byte[] bytes, IsoTpConnector connector) { log.info("-------sendBytesToCan " + bytes.length + " byte(s):"); @@ -49,14 +61,12 @@ public static byte[] combineArrays(byte[] hdr, byte[] data, int dataOffset, int } public void sendCanFrame(int hdr0, byte[] data, int offset, int dataLength) { - sendCanData(new byte[]{(byte) hdr0}, data, offset, dataLength); + sendCanData(combineArrays(new byte[]{(byte) hdr0}, data, offset, dataLength)); } public void sendCanFrame(int hdr0, int hdr1, byte[] data, int dataOffset, int dataLength) { - sendCanData(new byte[]{(byte) hdr0, (byte) hdr1}, data, dataOffset, dataLength); + sendCanData(combineArrays(new byte[]{(byte) hdr0, (byte) hdr1}, data, dataOffset, dataLength)); } - public abstract void sendCanData(byte[] hdr, byte[] data, int dataOffset, int dataLength); - - public abstract void receiveData(); + public abstract void sendCanData(byte[] total); } diff --git a/java_console/io/src/main/java/com/rusefi/io/can/SocketCANHelper.java b/java_console/io/src/main/java/com/rusefi/io/can/SocketCANHelper.java new file mode 100644 index 0000000000..a1b0d1fe1b --- /dev/null +++ b/java_console/io/src/main/java/com/rusefi/io/can/SocketCANHelper.java @@ -0,0 +1,93 @@ +package com.rusefi.io.can; + +import com.devexperts.logging.Logging; +import org.jetbrains.annotations.NotNull; +import tel.schich.javacan.CanChannels; +import tel.schich.javacan.CanFrame; +import tel.schich.javacan.NetworkDevice; +import tel.schich.javacan.RawCanChannel; + +import java.io.IOException; + +import static com.devexperts.logging.Logging.getLogging; +import static tel.schich.javacan.CanFrame.FD_NO_FLAGS; +import static tel.schich.javacan.CanSocketOptions.RECV_OWN_MSGS; + +/** + * Low-level SocketCAN send/receive helpers. + * Uses the javacan library (tel.schich:javacan-core) which wraps Linux SocketCAN. + * + * The CAN interface name defaults to "can0" and can be overridden with the + * system property {@code CAN_DEVICE_NAME}, e.g. {@code -DCAN_DEVICE_NAME=can1}. + */ +public class SocketCANHelper { + private static final Logging log = getLogging(SocketCANHelper.class); + + public static final String CAN_DEVICE_PROPERTY = "CAN_DEVICE_NAME"; + public static final String DEFAULT_CAN_DEVICE = "can0"; + + public static class CanPacket { + public final int id; + public final byte[] payload; + + public CanPacket(int id, byte[] payload) { + this.id = id; + this.payload = payload; + } + } + + @NotNull + public static RawCanChannel createSocket(String deviceName) { + final RawCanChannel socket; + try { + NetworkDevice canInterface = NetworkDevice.lookup(deviceName); + socket = CanChannels.newRawChannel(); + socket.bind(canInterface); + socket.configureBlocking(true); + socket.setOption(RECV_OWN_MSGS, false); + log.info("SocketCAN: opened " + deviceName); + } catch (IOException e) { + throw new IllegalStateException("Failed to open SocketCAN device '" + deviceName + "': " + e.getMessage(), e); + } + return socket; + } + + @NotNull + public static RawCanChannel createSocket() { + return createSocket(System.getProperty(CAN_DEVICE_PROPERTY, DEFAULT_CAN_DEVICE)); + } + + public static void send(int id, byte[] payload, RawCanChannel channel) { + CanFrame packet = CanFrame.create(id, FD_NO_FLAGS, payload); + try { + channel.write(packet); + } catch (IOException e) { + throw new IllegalStateException("SocketCAN send failed", e); + } + } + + @NotNull + public static CanPacket read(RawCanChannel socket) throws IOException { + CanFrame rx = socket.read(); + byte[] raw = new byte[rx.getDataLength()]; + rx.getData(raw, 0, raw.length); + if (log.debugEnabled()) + log.debug("SocketCAN RX id=" + String.format("%X", rx.getId()) + " len=" + raw.length); + return new CanPacket(rx.getId(), raw); + } + + /** + * Returns true if SocketCAN is available on this platform (Linux with javacan on classpath). + */ + public static boolean isAvailable() { + if (!System.getProperty("os.name", "").toLowerCase().contains("linux")) { + return false; + } + try { + Class.forName("tel.schich.javacan.RawCanChannel"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/java_console/io/src/main/java/com/rusefi/io/can/SocketCANIoStream.java b/java_console/io/src/main/java/com/rusefi/io/can/SocketCANIoStream.java new file mode 100644 index 0000000000..552384aca7 --- /dev/null +++ b/java_console/io/src/main/java/com/rusefi/io/can/SocketCANIoStream.java @@ -0,0 +1,156 @@ +package com.rusefi.io.can; + +import com.devexperts.logging.Logging; +import com.opensr5.io.DataListener; +import com.rusefi.binaryprotocol.IncomingDataBuffer; +import com.rusefi.io.IoStream; +import com.rusefi.io.serial.AbstractIoStream; +import com.rusefi.io.tcp.BinaryProtocolServer; +import org.jetbrains.annotations.Nullable; +import tel.schich.javacan.RawCanChannel; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import static com.devexperts.logging.Logging.getLogging; + +/** + * IoStream implementation that speaks the FOME binary protocol over Linux SocketCAN, + * using ISO-TP (ISO 15765-2) framing. + * + * The CAN device name defaults to "can0" and can be overridden with the system property + * {@code CAN_DEVICE_NAME}, e.g. {@code -DCAN_DEVICE_NAME=can1}. + * + * CAN IDs used: + * TX (PC → ECU): CAN_ECU_SERIAL_RX_ID (default 0x100) + * RX (ECU → PC): CAN_ECU_SERIAL_TX_ID (default 0x102) + */ +public class SocketCANIoStream extends AbstractIoStream { + private static final int CAN_ECU_SERIAL_RX_ID = 0x100; + private static final int CAN_ECU_SERIAL_TX_ID = 0x102; + + private static final Logging log = getLogging(SocketCANIoStream.class); + + private final IncomingDataBuffer dataBuffer; + private final RawCanChannel socket; + + private final IsoTpCanDecoder canDecoder = new IsoTpCanDecoder() { + @Override + protected void onTpFirstFrame() { + sendCanPacket(IsoTpCanDecoder.FLOW_CONTROL); + } + }; + + private final IsoTpConnector isoTpConnector = new IsoTpConnector(CAN_ECU_SERIAL_RX_ID) { + @Override + public void sendCanData(byte[] total) { + sendCanPacket(total); + } + + @Override + public void receiveData() { + readOnePacket(CAN_ECU_SERIAL_TX_ID); + } + }; + + private void sendCanPacket(byte[] total) { + if (log.debugEnabled()) + log.debug("SocketCAN TX " + total.length + " byte(s): " + IoStream.printHexBinary(total)); + SocketCANHelper.send(isoTpConnector.canId(), total, socket); + } + + private SocketCANIoStream(String deviceName) { + socket = SocketCANHelper.createSocket(deviceName); + dataBuffer = createDataBuffer("SocketCAN"); + } + + /** + * Create a stream connected to the default CAN device (can0 or CAN_DEVICE_NAME property). + * Returns null if SocketCAN is not available on this platform. + */ + @Nullable + public static SocketCANIoStream create() { + return create(System.getProperty(SocketCANHelper.CAN_DEVICE_PROPERTY, SocketCANHelper.DEFAULT_CAN_DEVICE)); + } + + /** + * Create a stream connected to the named CAN device. + * Returns null if SocketCAN is not available on this platform. + */ + @Nullable + public static SocketCANIoStream create(String deviceName) { + if (!SocketCANHelper.isAvailable()) { + log.info("SocketCAN not available on this platform"); + return null; + } + try { + return new SocketCANIoStream(deviceName); + } catch (Exception e) { + log.error("Failed to create SocketCAN stream on " + deviceName + ": " + e.getMessage()); + return null; + } + } + + @Override + public void write(byte[] bytes) throws IOException { + IsoTpConnector.sendStrategy(bytes, isoTpConnector); + } + + @Override + public void setInputListener(DataListener listener) { + Executor threadExecutor = Executors.newSingleThreadExecutor( + BinaryProtocolServer.getThreadFactory("SocketCAN reader")); + threadExecutor.execute(() -> { + while (!isClosed()) { + readOnePacket(listener); + } + }); + } + + private void readOnePacket(DataListener listener) { + readHardwarePacket((rx) -> { + byte[] decoded = canDecoder.decodePacket(rx.payload); + if (decoded != null && decoded.length > 0) + listener.onDataArrived(decoded); + }); + } + + private interface SocketListener { + void onPacket(SocketCANHelper.CanPacket msg); + } + + private void readOnePacket(int expectedId) { + readHardwarePacket((rx) -> { + if (rx.id != expectedId) { + return; + } + canDecoder.decodePacket(rx.payload); + }); + } + + private void readHardwarePacket(SocketListener listener) { + try { + SocketCANHelper.CanPacket rx = SocketCANHelper.read(socket); + listener.onPacket(rx); + } catch (IOException e) { + if (!isClosed()) + throw new IllegalStateException("SocketCAN read error", e); + } + } + + + @Override + public IncomingDataBuffer getDataBuffer() { + return dataBuffer; + } + + @Override + public void close() { + super.close(); + try { + socket.close(); + } catch (IOException ignored) { + } + } +} diff --git a/java_console/ui/bin/main/com/camick/RXTextUtilities.class b/java_console/ui/bin/main/com/camick/RXTextUtilities.class new file mode 100644 index 0000000000..de8c66bd0c Binary files /dev/null and b/java_console/ui/bin/main/com/camick/RXTextUtilities.class differ diff --git a/java_console/ui/bin/main/com/rusefi/BenchTestPane.class b/java_console/ui/bin/main/com/rusefi/BenchTestPane.class new file mode 100644 index 0000000000..f2a1ad6fa7 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/BenchTestPane.class differ diff --git a/java_console/ui/bin/main/com/rusefi/CommandControl.class b/java_console/ui/bin/main/com/rusefi/CommandControl.class new file mode 100644 index 0000000000..590835ef7b Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/CommandControl.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ConsoleUI.class b/java_console/ui/bin/main/com/rusefi/ConsoleUI.class new file mode 100644 index 0000000000..29dfbbd991 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ConsoleUI.class differ diff --git a/java_console/ui/bin/main/com/rusefi/DeviceManager.png b/java_console/ui/bin/main/com/rusefi/DeviceManager.png new file mode 100644 index 0000000000..9883f6ffe7 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/DeviceManager.png differ diff --git a/java_console/ui/bin/main/com/rusefi/FixedCommandControl.class b/java_console/ui/bin/main/com/rusefi/FixedCommandControl.class new file mode 100644 index 0000000000..adeefacaf3 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/FixedCommandControl.class differ diff --git a/java_console/ui/bin/main/com/rusefi/KeyStrokeShortcut.class b/java_console/ui/bin/main/com/rusefi/KeyStrokeShortcut.class new file mode 100644 index 0000000000..fa046e75c4 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/KeyStrokeShortcut.class differ diff --git a/java_console/ui/bin/main/com/rusefi/Launcher.class b/java_console/ui/bin/main/com/rusefi/Launcher.class new file mode 100644 index 0000000000..c7a17ad8c9 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/Launcher.class differ diff --git a/java_console/ui/bin/main/com/rusefi/PerformanceTraceHelper.class b/java_console/ui/bin/main/com/rusefi/PerformanceTraceHelper.class new file mode 100644 index 0000000000..4ca2df2373 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/PerformanceTraceHelper.class differ diff --git a/java_console/ui/bin/main/com/rusefi/SerialPortScanner$AvailableHardware.class b/java_console/ui/bin/main/com/rusefi/SerialPortScanner$AvailableHardware.class new file mode 100644 index 0000000000..aee24e5c24 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/SerialPortScanner$AvailableHardware.class differ diff --git a/java_console/ui/bin/main/com/rusefi/SerialPortScanner$Listener.class b/java_console/ui/bin/main/com/rusefi/SerialPortScanner$Listener.class new file mode 100644 index 0000000000..b86712a18e Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/SerialPortScanner$Listener.class differ diff --git a/java_console/ui/bin/main/com/rusefi/SerialPortScanner$PortResult.class b/java_console/ui/bin/main/com/rusefi/SerialPortScanner$PortResult.class new file mode 100644 index 0000000000..0bbcfd1a6b Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/SerialPortScanner$PortResult.class differ diff --git a/java_console/ui/bin/main/com/rusefi/SerialPortScanner$SerialPortType.class b/java_console/ui/bin/main/com/rusefi/SerialPortScanner$SerialPortType.class new file mode 100644 index 0000000000..12a89885f3 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/SerialPortScanner$SerialPortType.class differ diff --git a/java_console/ui/bin/main/com/rusefi/SerialPortScanner.class b/java_console/ui/bin/main/com/rusefi/SerialPortScanner.class new file mode 100644 index 0000000000..2d49979f3e Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/SerialPortScanner.class differ diff --git a/java_console/ui/bin/main/com/rusefi/SimulatorHelper.class b/java_console/ui/bin/main/com/rusefi/SimulatorHelper.class new file mode 100644 index 0000000000..461a5c5e24 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/SimulatorHelper.class differ diff --git a/java_console/ui/bin/main/com/rusefi/StartupFrame.class b/java_console/ui/bin/main/com/rusefi/StartupFrame.class new file mode 100644 index 0000000000..d1daa2b094 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/StartupFrame.class differ diff --git a/java_console/ui/bin/main/com/rusefi/appicon.png b/java_console/ui/bin/main/com/rusefi/appicon.png new file mode 100644 index 0000000000..2fbd012ff2 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/appicon.png differ diff --git a/java_console/ui/bin/main/com/rusefi/check_engine.jpg b/java_console/ui/bin/main/com/rusefi/check_engine.jpg new file mode 100644 index 0000000000..8ff78bd089 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/check_engine.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/connect48.png b/java_console/ui/bin/main/com/rusefi/connect48.png new file mode 100644 index 0000000000..e873e64566 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/connect48.png differ diff --git a/java_console/ui/bin/main/com/rusefi/download48.jpg b/java_console/ui/bin/main/com/rusefi/download48.jpg new file mode 100644 index 0000000000..3db6f58c99 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/download48.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/erase.png b/java_console/ui/bin/main/com/rusefi/erase.png new file mode 100644 index 0000000000..1a75cab17d Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/erase.png differ diff --git a/java_console/ui/bin/main/com/rusefi/fuel_pump.jpg b/java_console/ui/bin/main/com/rusefi/fuel_pump.jpg new file mode 100644 index 0000000000..bd2b9303f7 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/fuel_pump.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/help.jpg b/java_console/ui/bin/main/com/rusefi/help.jpg new file mode 100644 index 0000000000..7858ab96ec Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/help.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/idle_valve.png b/java_console/ui/bin/main/com/rusefi/idle_valve.png new file mode 100644 index 0000000000..a28c0e52be Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/idle_valve.png differ diff --git a/java_console/ui/bin/main/com/rusefi/info.png b/java_console/ui/bin/main/com/rusefi/info.png new file mode 100644 index 0000000000..68a44dde65 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/info.png differ diff --git a/java_console/ui/bin/main/com/rusefi/injector.png b/java_console/ui/bin/main/com/rusefi/injector.png new file mode 100644 index 0000000000..71d8161ee4 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/injector.png differ diff --git a/java_console/ui/bin/main/com/rusefi/livedocs/gauge.png b/java_console/ui/bin/main/com/rusefi/livedocs/gauge.png new file mode 100644 index 0000000000..60d63bf70b Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/livedocs/gauge.png differ diff --git a/java_console/ui/bin/main/com/rusefi/livedocs/setting.png b/java_console/ui/bin/main/com/rusefi/livedocs/setting.png new file mode 100644 index 0000000000..03787fce2d Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/livedocs/setting.png differ diff --git a/java_console/ui/bin/main/com/rusefi/livedocs/variable.png b/java_console/ui/bin/main/com/rusefi/livedocs/variable.png new file mode 100644 index 0000000000..08b6183480 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/livedocs/variable.png differ diff --git a/java_console/ui/bin/main/com/rusefi/logo.png b/java_console/ui/bin/main/com/rusefi/logo.png new file mode 100644 index 0000000000..575ad3eab2 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/logo.png differ diff --git a/java_console/ui/bin/main/com/rusefi/logo_alphax.png b/java_console/ui/bin/main/com/rusefi/logo_alphax.png new file mode 100644 index 0000000000..5e773a2fee Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/logo_alphax.png differ diff --git a/java_console/ui/bin/main/com/rusefi/logo_proteus.png b/java_console/ui/bin/main/com/rusefi/logo_proteus.png new file mode 100644 index 0000000000..8c1f8106bf Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/logo_proteus.png differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/DfuFlasher.class b/java_console/ui/bin/main/com/rusefi/maintenance/DfuFlasher.class new file mode 100644 index 0000000000..bdb646c03f Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/DfuFlasher.class differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/ExecHelper.class b/java_console/ui/bin/main/com/rusefi/maintenance/ExecHelper.class new file mode 100644 index 0000000000..63816be392 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/ExecHelper.class differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$Chunk.class b/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$Chunk.class new file mode 100644 index 0000000000..cdf09b39da Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$Chunk.class differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$ChunkHandler.class b/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$ChunkHandler.class new file mode 100644 index 0000000000..edcfb39ace Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$ChunkHandler.class differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$ProgressUpdater.class b/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$ProgressUpdater.class new file mode 100644 index 0000000000..f200752e90 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher$ProgressUpdater.class differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher.class b/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher.class new file mode 100644 index 0000000000..4c621bf3b7 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/OpenBltFlasher.class differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/OpenbltCallbacks.class b/java_console/ui/bin/main/com/rusefi/maintenance/OpenbltCallbacks.class new file mode 100644 index 0000000000..ab815e38d8 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/OpenbltCallbacks.class differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/ProgramSelector.class b/java_console/ui/bin/main/com/rusefi/maintenance/ProgramSelector.class new file mode 100644 index 0000000000..a7ef072f40 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/ProgramSelector.class differ diff --git a/java_console/ui/bin/main/com/rusefi/maintenance/UpdateStatusWindow.class b/java_console/ui/bin/main/com/rusefi/maintenance/UpdateStatusWindow.class new file mode 100644 index 0000000000..97fa451be3 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/maintenance/UpdateStatusWindow.class differ diff --git a/java_console/ui/bin/main/com/rusefi/radiator_fan.jpg b/java_console/ui/bin/main/com/rusefi/radiator_fan.jpg new file mode 100644 index 0000000000..289af6e6bb Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/radiator_fan.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/sdinfo.jpg b/java_console/ui/bin/main/com/rusefi/sdinfo.jpg new file mode 100644 index 0000000000..250c6864c6 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/sdinfo.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/solenoid.jpg b/java_console/ui/bin/main/com/rusefi/solenoid.jpg new file mode 100644 index 0000000000..beaffa5b15 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/solenoid.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/spark.jpg b/java_console/ui/bin/main/com/rusefi/spark.jpg new file mode 100644 index 0000000000..ba049b128d Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/spark.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/stop.jpg b/java_console/ui/bin/main/com/rusefi/stop.jpg new file mode 100644 index 0000000000..8965600815 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/stop.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/tools/ConsoleTools$ConsoleTool.class b/java_console/ui/bin/main/com/rusefi/tools/ConsoleTools$ConsoleTool.class new file mode 100644 index 0000000000..7250675695 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/tools/ConsoleTools$ConsoleTool.class differ diff --git a/java_console/ui/bin/main/com/rusefi/tools/ConsoleTools.class b/java_console/ui/bin/main/com/rusefi/tools/ConsoleTools.class new file mode 100644 index 0000000000..8f42b024a2 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/tools/ConsoleTools.class differ diff --git a/java_console/ui/bin/main/com/rusefi/trigger.jpg b/java_console/ui/bin/main/com/rusefi/trigger.jpg new file mode 100644 index 0000000000..fcacfd36e3 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/trigger.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/trigger/TriggerImage$TriggerPanel.class b/java_console/ui/bin/main/com/rusefi/trigger/TriggerImage$TriggerPanel.class new file mode 100644 index 0000000000..b46e596a93 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/trigger/TriggerImage$TriggerPanel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/trigger/TriggerImage.class b/java_console/ui/bin/main/com/rusefi/trigger/TriggerImage.class new file mode 100644 index 0000000000..8dcf372674 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/trigger/TriggerImage.class differ diff --git a/java_console/ui/bin/main/com/rusefi/trigger/WaveState$trigger_value_e.class b/java_console/ui/bin/main/com/rusefi/trigger/WaveState$trigger_value_e.class new file mode 100644 index 0000000000..feba98b83f Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/trigger/WaveState$trigger_value_e.class differ diff --git a/java_console/ui/bin/main/com/rusefi/trigger/WaveState.class b/java_console/ui/bin/main/com/rusefi/trigger/WaveState.class new file mode 100644 index 0000000000..be9105420f Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/trigger/WaveState.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/FrameHelper$1.class b/java_console/ui/bin/main/com/rusefi/ui/FrameHelper$1.class new file mode 100644 index 0000000000..331c81ab26 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/FrameHelper$1.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/FrameHelper.class b/java_console/ui/bin/main/com/rusefi/ui/FrameHelper.class new file mode 100644 index 0000000000..2220d4eb01 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/FrameHelper.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/LogSizeControl.class b/java_console/ui/bin/main/com/rusefi/ui/LogSizeControl.class new file mode 100644 index 0000000000..354aa3eeb0 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/LogSizeControl.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/MessagesPane.class b/java_console/ui/bin/main/com/rusefi/ui/MessagesPane.class new file mode 100644 index 0000000000..473231e77e Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/MessagesPane.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/MessagesPanel.class b/java_console/ui/bin/main/com/rusefi/ui/MessagesPanel.class new file mode 100644 index 0000000000..39d7a79dd1 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/MessagesPanel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/MessagesView$Listener.class b/java_console/ui/bin/main/com/rusefi/ui/MessagesView$Listener.class new file mode 100644 index 0000000000..93ced52243 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/MessagesView$Listener.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/MessagesView.class b/java_console/ui/bin/main/com/rusefi/ui/MessagesView.class new file mode 100644 index 0000000000..41b8f4ee2f Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/MessagesView.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/RecentCommands$Entry.class b/java_console/ui/bin/main/com/rusefi/ui/RecentCommands$Entry.class new file mode 100644 index 0000000000..f0049bbe8f Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/RecentCommands$Entry.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/RecentCommands.class b/java_console/ui/bin/main/com/rusefi/ui/RecentCommands.class new file mode 100644 index 0000000000..43d4b8c30c Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/RecentCommands.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/RpmLabel.class b/java_console/ui/bin/main/com/rusefi/ui/RpmLabel.class new file mode 100644 index 0000000000..163a9a712b Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/RpmLabel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/RpmModel$RpmListener.class b/java_console/ui/bin/main/com/rusefi/ui/RpmModel$RpmListener.class new file mode 100644 index 0000000000..34cef0173a Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/RpmModel$RpmListener.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/RpmModel.class b/java_console/ui/bin/main/com/rusefi/ui/RpmModel.class new file mode 100644 index 0000000000..da2ed13e00 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/RpmModel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/StatusWindow.class b/java_console/ui/bin/main/com/rusefi/ui/StatusWindow.class new file mode 100644 index 0000000000..8b5140b331 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/StatusWindow.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/UIContext.class b/java_console/ui/bin/main/com/rusefi/ui/UIContext.class new file mode 100644 index 0000000000..041048d4a5 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/UIContext.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/WarningPanel.class b/java_console/ui/bin/main/com/rusefi/ui/WarningPanel.class new file mode 100644 index 0000000000..1163b7df59 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/WarningPanel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/config/BaseConfigField.class b/java_console/ui/bin/main/com/rusefi/ui/config/BaseConfigField.class new file mode 100644 index 0000000000..f1c25bba6a Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/config/BaseConfigField.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/config/ConfigUiField.class b/java_console/ui/bin/main/com/rusefi/ui/config/ConfigUiField.class new file mode 100644 index 0000000000..2041fe4be7 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/config/ConfigUiField.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/console/MainFrame.class b/java_console/ui/bin/main/com/rusefi/ui/console/MainFrame.class new file mode 100644 index 0000000000..c2494be66a Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/console/MainFrame.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/console/TabbedPanel.class b/java_console/ui/bin/main/com/rusefi/ui/console/TabbedPanel.class new file mode 100644 index 0000000000..9bcb36505a Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/console/TabbedPanel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/engine/ChannelNaming.class b/java_console/ui/bin/main/com/rusefi/ui/engine/ChannelNaming.class new file mode 100644 index 0000000000..64dc1fd93d Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/engine/ChannelNaming.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferPanel$ImageOrderComparator.class b/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferPanel$ImageOrderComparator.class new file mode 100644 index 0000000000..63f510798d Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferPanel$ImageOrderComparator.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferPanel.class b/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferPanel.class new file mode 100644 index 0000000000..e0d78b8654 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferPanel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferStatusPanel.class b/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferStatusPanel.class new file mode 100644 index 0000000000..ba9397617e Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/engine/EngineSnifferStatusPanel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/engine/NameUtil.class b/java_console/ui/bin/main/com/rusefi/ui/engine/NameUtil.class new file mode 100644 index 0000000000..01c14ae070 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/engine/NameUtil.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/engine/UpDownImage.class b/java_console/ui/bin/main/com/rusefi/ui/engine/UpDownImage.class new file mode 100644 index 0000000000..f03e6a0c95 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/engine/UpDownImage.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/engine/ZoomControl$ZoomControlListener.class b/java_console/ui/bin/main/com/rusefi/ui/engine/ZoomControl$ZoomControlListener.class new file mode 100644 index 0000000000..011127cacb Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/engine/ZoomControl$ZoomControlListener.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/engine/ZoomControl.class b/java_console/ui/bin/main/com/rusefi/ui/engine/ZoomControl.class new file mode 100644 index 0000000000..21697567a9 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/engine/ZoomControl.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/lua/DocumentSizeFilter.class b/java_console/ui/bin/main/com/rusefi/ui/lua/DocumentSizeFilter.class new file mode 100644 index 0000000000..dcc4768f44 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/lua/DocumentSizeFilter.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/lua/LuaScriptPanel.class b/java_console/ui/bin/main/com/rusefi/ui/lua/LuaScriptPanel.class new file mode 100644 index 0000000000..591c5d3902 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/lua/LuaScriptPanel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor$1.class b/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor$1.class new file mode 100644 index 0000000000..12bc393fb6 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor$1.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor$2.class b/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor$2.class new file mode 100644 index 0000000000..8b5ae4c203 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor$2.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor.class b/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor.class new file mode 100644 index 0000000000..a7486e5cf4 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/lua/TextEditor.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/DefaultExceptionHandler.class b/java_console/ui/bin/main/com/rusefi/ui/util/DefaultExceptionHandler.class new file mode 100644 index 0000000000..b901f27eb6 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/DefaultExceptionHandler.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/HorizontalLine.class b/java_console/ui/bin/main/com/rusefi/ui/util/HorizontalLine.class new file mode 100644 index 0000000000..2bb6966b1b Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/HorizontalLine.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/JTextFieldWithWidth.class b/java_console/ui/bin/main/com/rusefi/ui/util/JTextFieldWithWidth.class new file mode 100644 index 0000000000..ebd368b706 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/JTextFieldWithWidth.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/JustOneInstance$1.class b/java_console/ui/bin/main/com/rusefi/ui/util/JustOneInstance$1.class new file mode 100644 index 0000000000..15dfb5ac86 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/JustOneInstance$1.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/JustOneInstance.class b/java_console/ui/bin/main/com/rusefi/ui/util/JustOneInstance.class new file mode 100644 index 0000000000..591e7d49f1 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/JustOneInstance.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/LocalizedMessages.class b/java_console/ui/bin/main/com/rusefi/ui/util/LocalizedMessages.class new file mode 100644 index 0000000000..0c729e8b69 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/LocalizedMessages.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel$1.class b/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel$1.class new file mode 100644 index 0000000000..612c0b29bf Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel$1.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel$2.class b/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel$2.class new file mode 100644 index 0000000000..7bdb3ba2b4 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel$2.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel.class b/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel.class new file mode 100644 index 0000000000..2ca5c64b52 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/URLLabel.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$1.class b/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$1.class new file mode 100644 index 0000000000..611320f7bf Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$1.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$2.class b/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$2.class new file mode 100644 index 0000000000..946057f440 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$2.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$DynamicForResourcesURLClassLoader.class b/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$DynamicForResourcesURLClassLoader.class new file mode 100644 index 0000000000..7e30978cd0 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils$DynamicForResourcesURLClassLoader.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils.class b/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils.class new file mode 100644 index 0000000000..32a13a007b Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/util/UiUtils.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand$1.class b/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand$1.class new file mode 100644 index 0000000000..1f699deb8b Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand$1.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand$Listener.class b/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand$Listener.class new file mode 100644 index 0000000000..2c6f05c468 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand$Listener.class differ diff --git a/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand.class b/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand.class new file mode 100644 index 0000000000..9e576482fa Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/ui/widgets/AnyCommand.class differ diff --git a/java_console/ui/bin/main/com/rusefi/undo.jpg b/java_console/ui/bin/main/com/rusefi/undo.jpg new file mode 100644 index 0000000000..7b3cc60657 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/undo.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/upload48.png b/java_console/ui/bin/main/com/rusefi/upload48.png new file mode 100644 index 0000000000..991f91b0de Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/upload48.png differ diff --git a/java_console/ui/bin/main/com/rusefi/writeconfig.jpg b/java_console/ui/bin/main/com/rusefi/writeconfig.jpg new file mode 100644 index 0000000000..80ee05b479 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/writeconfig.jpg differ diff --git a/java_console/ui/bin/main/com/rusefi/writeconfig48.jpg b/java_console/ui/bin/main/com/rusefi/writeconfig48.jpg new file mode 100644 index 0000000000..b3f12e8b04 Binary files /dev/null and b/java_console/ui/bin/main/com/rusefi/writeconfig48.jpg differ diff --git a/java_console/ui/bin/main/org/putgemin/VerticalFlowLayout.class b/java_console/ui/bin/main/org/putgemin/VerticalFlowLayout.class new file mode 100644 index 0000000000..618180c8a0 Binary files /dev/null and b/java_console/ui/bin/main/org/putgemin/VerticalFlowLayout.class differ diff --git a/java_console/ui/bin/test/com/rusefi/ui/LuaFormatterTest.class b/java_console/ui/bin/test/com/rusefi/ui/LuaFormatterTest.class new file mode 100644 index 0000000000..22076c5644 Binary files /dev/null and b/java_console/ui/bin/test/com/rusefi/ui/LuaFormatterTest.class differ diff --git a/java_console/ui/bin/test/com/rusefi/ui/engine/test/UpDownSandbox.class b/java_console/ui/bin/test/com/rusefi/ui/engine/test/UpDownSandbox.class new file mode 100644 index 0000000000..a41840444b Binary files /dev/null and b/java_console/ui/bin/test/com/rusefi/ui/engine/test/UpDownSandbox.class differ diff --git a/java_console/ui/bin/test/com/rusefi/ui/test/EngineSnifferPanelTest.class b/java_console/ui/bin/test/com/rusefi/ui/test/EngineSnifferPanelTest.class new file mode 100644 index 0000000000..f243dec66c Binary files /dev/null and b/java_console/ui/bin/test/com/rusefi/ui/test/EngineSnifferPanelTest.class differ diff --git a/java_console/ui/bin/test/com/rusefi/ui/test/WavePanelSandbox.class b/java_console/ui/bin/test/com/rusefi/ui/test/WavePanelSandbox.class new file mode 100644 index 0000000000..f904466c3f Binary files /dev/null and b/java_console/ui/bin/test/com/rusefi/ui/test/WavePanelSandbox.class differ diff --git a/java_console/ui/src/main/java/com/rusefi/SerialPortScanner.java b/java_console/ui/src/main/java/com/rusefi/SerialPortScanner.java index b26a08d175..70cfae299c 100644 --- a/java_console/ui/src/main/java/com/rusefi/SerialPortScanner.java +++ b/java_console/ui/src/main/java/com/rusefi/SerialPortScanner.java @@ -10,6 +10,7 @@ import com.rusefi.io.IoStream; import com.rusefi.io.LinkManager; import com.rusefi.io.UpdateOperationCallbacks; +import com.rusefi.io.can.SocketCANHelper; import com.rusefi.io.tcp.TcpConnector; import com.rusefi.maintenance.DfuFlasher; import org.jetbrains.annotations.NotNull; @@ -31,6 +32,7 @@ public enum SerialPortType { FomeEcu("FOME ECU", 20), FomeEcuWithOpenblt("FOME ECU w/ BL", 20), OpenBlt("OpenBLT Bootloader", 10), + SocketCanAdapter("SocketCAN (Linux)", 15), Unknown("Unknown", 100), ; @@ -86,7 +88,9 @@ public boolean equals(Object o) { } public boolean isEcu() { - return type == SerialPortType.FomeEcu || type == SerialPortType.FomeEcuWithOpenblt; + return type == SerialPortType.FomeEcu + || type == SerialPortType.FomeEcuWithOpenblt + || type == SerialPortType.SocketCanAdapter; } } @@ -108,6 +112,11 @@ public void addListener(Listener listener) { private static PortResult inspectPort(String serialPort) { log.info("Determining type of port: " + serialPort); + // CAN adapters are pre-classified when added to candidatePorts + if (serialPort.startsWith(LinkManager.SOCKETCAN_PREFIX)) { + return new PortResult(serialPort, SerialPortType.SocketCanAdapter); + } + boolean isOpenblt = isPortOpenblt(serialPort); log.info("Port " + serialPort + (isOpenblt ? " looks like" : " does not look like") + " an OpenBLT bootloader"); if (isOpenblt) { @@ -128,6 +137,34 @@ private static PortResult inspectPort(String serialPort) { } } + /** + * Enumerate SocketCAN interfaces visible to the OS and add them to candidatePorts + * using the "socketcan:" prefix format. Linux only. + */ + private static void addSocketCanPorts(List candidatePorts) { + if (!SocketCANHelper.isAvailable()) { + return; + } + // Read /sys/class/net to find CAN interfaces (canX, slcanX, vcanX, etc.) + java.io.File netDir = new java.io.File("/sys/class/net"); + if (!netDir.isDirectory()) { + return; + } + String[] ifaces = netDir.list((dir, name) -> + name.startsWith("can") || name.startsWith("slcan") || name.startsWith("vcan")); + if (ifaces == null) { + return; + } + for (String iface : ifaces) { + String portName = LinkManager.SOCKETCAN_PREFIX + iface; + if (!candidatePorts.contains(portName)) { + log.info("Detected SocketCAN interface: " + iface); + candidatePorts.add(portName); + } + } + } + + private static List inspectPorts(final List ports) { if (ports.isEmpty()) { return new ArrayList<>(); @@ -195,7 +232,8 @@ private static List inspectPorts(final List ports) { private final static Map portCache = new HashMap<>(); /** - * Find all available serial ports and checks if simulator local TCP port is available + * Find all available serial ports and checks if simulator local TCP port is available. + * Also adds CAN adapter entries (SocketCAN on Linux) when detected. */ private void findAllAvailablePorts(boolean includeSlowLookup) { List ports = new ArrayList<>(); @@ -206,6 +244,8 @@ private void findAllAvailablePorts(boolean includeSlowLookup) { if (includeSlowLookup) { candidatePorts.addAll(TcpConnector.getAvailablePorts()); + // Add SocketCAN interfaces visible to the OS (Linux only) + addSocketCanPorts(candidatePorts); } for (String candidate : candidatePorts) { diff --git a/java_console/ui/src/main/java/com/rusefi/maintenance/UpdateStatusWindow.java b/java_console/ui/src/main/java/com/rusefi/maintenance/UpdateStatusWindow.java index 74eed8938e..a954f93557 100644 --- a/java_console/ui/src/main/java/com/rusefi/maintenance/UpdateStatusWindow.java +++ b/java_console/ui/src/main/java/com/rusefi/maintenance/UpdateStatusWindow.java @@ -10,7 +10,7 @@ public UpdateStatusWindow(String title) { @Override public void log(String message) { - append(message); + logLine(message); } @Override diff --git a/java_console/ui/src/main/java/com/rusefi/ui/StatusWindow.java b/java_console/ui/src/main/java/com/rusefi/ui/StatusWindow.java index 6a0aa9e096..c4c0f186e2 100644 --- a/java_console/ui/src/main/java/com/rusefi/ui/StatusWindow.java +++ b/java_console/ui/src/main/java/com/rusefi/ui/StatusWindow.java @@ -38,7 +38,7 @@ public Dimension getPreferredSize() { }; content.add(messagesScroll, BorderLayout.CENTER); - append("Bundle " + BundleUtil.readBundleFullNameNotNull()); + logLine("Bundle " + BundleUtil.readBundleFullNameNotNull()); } public void setErrorState() { @@ -58,7 +58,7 @@ public void showFrame(String title) { } @Override - public void append(final String string) { + public void logLine(final String string) { // todo: check if AWT thread and do not invokeLater if already on AWT thread SwingUtilities.invokeLater(() -> { String s = string.replaceAll(Character.toString((char) 219), ""); @@ -74,6 +74,6 @@ public void copyContentToClipboard() { SwingUtilities.invokeLater(() -> Toolkit.getDefaultToolkit().getSystemClipboard() .setContents(new StringSelection(logTextArea.getText()), null)); - append("hint: error state is already in your clipboard, please use PASTE or Ctrl-V while reporting issues"); + logLine("hint: error state is already in your clipboard, please use PASTE or Ctrl-V while reporting issues"); } } diff --git a/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerSignal.class b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerSignal.class new file mode 100644 index 0000000000..492a8941b6 Binary files /dev/null and b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerSignal.class differ diff --git a/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerSignalReader.class b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerSignalReader.class new file mode 100644 index 0000000000..2ac65c0521 Binary files /dev/null and b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerSignalReader.class differ diff --git a/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo$TriggerGaps.class b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo$TriggerGaps.class new file mode 100644 index 0000000000..e21c8cb547 Binary files /dev/null and b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo$TriggerGaps.class differ diff --git a/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo$TriggerWheelInfoConsumer.class b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo$TriggerWheelInfoConsumer.class new file mode 100644 index 0000000000..703987f7b0 Binary files /dev/null and b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo$TriggerWheelInfoConsumer.class differ diff --git a/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo.class b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo.class new file mode 100644 index 0000000000..f2fc259703 Binary files /dev/null and b/java_tools/trigger/bin/main/com/rusefi/trigger/TriggerWheelInfo.class differ