package uk.org.smithfamily.mslogger.ecuDef; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.text.DecimalFormat; import java.util.*; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import uk.org.smithfamily.mslogger.*; import uk.org.smithfamily.mslogger.activity.MSLoggerActivity; import uk.org.smithfamily.mslogger.comms.*; import uk.org.smithfamily.mslogger.ecuDef.gen.ECURegistry; import uk.org.smithfamily.mslogger.log.*; import android.app.*; import android.content.*; import android.os.*; import android.util.Log; import android.widget.Toast; /** * Abstract base class for all ECU implementations * * @author dgs * */ public class Megasquirt extends Service implements MSControllerInterface { private static final int MAX_QUEUE_SIZE = 10; BlockingQueue<InjectedCommand> injectionQueue = new ArrayBlockingQueue<InjectedCommand>(MAX_QUEUE_SIZE); private enum State { DISCONNECTED, CONNECTING, CONNECTED, LOGGING }; private volatile State currentState = State.DISCONNECTED; private NotificationManager notifications; private MSECUInterface ecuImplementation; private final boolean simulated = false; public static final String CONNECTED = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.CONNECTED"; public static final String DISCONNECTED = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.DISCONNECTED"; public static final String NEW_DATA = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.NEW_DATA"; public static final String UNKNOWN_ECU = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.UNKNOWN_ECU"; public static final String UNKNOWN_ECU_BT = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.UNKNOWN_ECU_BT"; public static final String PROBE_ECU = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.ECU_PROBED"; public static final String INJECTED_COMMAND_RESULTS = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.INJECTED_COMMAND_RESULTS"; public static final String INJECTED_COMMAND_RESULT_ID = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.INJECTED_COMMAND_RESULTS_ID"; public static final String INJECTED_COMMAND_RESULT_DATA = "uk.org.smithfamily.mslogger.ecuDef.Megasquirt.INJECTED_COMMAND_RESULTS_DATA"; private static final String UNKNOWN = "UNKNOWN"; private static final String LAST_SIG = "LAST_SIG"; private static final String LAST_PROBE = "LAST_PROBE"; private static final int NOTIFICATION_ID = 0; final DecimalFormat decimalFormat3dp = new DecimalFormat("#.000"); final DecimalFormat decimalFormat1dp = new DecimalFormat("#.0"); private BroadcastReceiver yourReceiver; // Used by broadcast receiver public static final int CONTROLLER_COMMAND = 1; public static final int BURN_DATA = 10; public static final int MS3_SD_CARD_STATUS_WRITE = 50; public static final int MS3_SD_CARD_STATUS_READ = 51; public static final int MS3_SD_CARD_RESET_AND_GO = 52; public static final int MS3_SD_CARD_RESET_AND_WAIT = 53; public static final int MS3_SD_CARD_STOP_LOGGING = 54; public static final int MS3_SD_CARD_START_LOGGING = 55; public static final int MS3_SD_CARD_REINITIALISE_CARD = 56; public static final int MS3_SD_CARD_READ_DIRECTORY_WRITE = 57; public static final int MS3_SD_CARD_READ_DIRECTORY_READ = 58; public static final int MS3_SD_CARD_READ_STREAM = 59; public static final int MS3_SD_CARD_READ_RTC_WRITE = 60; public static final int MS3_SD_CARD_READ_RTC_READ = 61; private boolean constantsLoaded; private String trueSignature = "Unknown"; private volatile ECUThread ecuThread; private static volatile ECUThread watch; private long logStart = 0; public class LocalBinder extends Binder { public Megasquirt getService() { return Megasquirt.this; } } @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { DebugLogManager.INSTANCE.log("Megasquirt Received start id " + startId + ": " + intent, Log.VERBOSE); // We want this service to continue running until it is explicitly // stopped, so return sticky. return START_STICKY; } @Override public IBinder onBind(final Intent intent) { return mBinder; } // This is the object that receives interactions from clients. See // RemoteService for a more complete example. private final IBinder mBinder = new LocalBinder(); @Override public void onCreate() { super.onCreate(); notifications = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); final IntentFilter btChangedFilter = new IntentFilter(); btChangedFilter.addAction(ApplicationSettings.BT_CHANGED); final IntentFilter injectCommandResultsFilter = new IntentFilter(); injectCommandResultsFilter.addAction(Megasquirt.INJECTED_COMMAND_RESULTS); this.yourReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (action.equals(ApplicationSettings.BT_CHANGED)) { DebugLogManager.INSTANCE.log("BT_CHANGED received", Log.VERBOSE); stop(); start(); } else if (action.equals(Megasquirt.INJECTED_COMMAND_RESULTS)) { final int resultId = intent.getIntExtra(Megasquirt.INJECTED_COMMAND_RESULT_ID, 0); switch (resultId) { case Megasquirt.BURN_DATA: // Wait til we get some data and flush it try { Thread.sleep(200); } catch (final InterruptedException e) { } break; default: break; } } } }; // Registers the receiver so that your service will listen for broadcasts this.registerReceiver(this.yourReceiver, btChangedFilter); this.registerReceiver(this.yourReceiver, injectCommandResultsFilter); final String lastSig = ApplicationSettings.INSTANCE.getPref(LAST_SIG); if (lastSig != null) { setImplementation(lastSig); } ApplicationSettings.INSTANCE.setEcu(this); start(); startForeground(NOTIFICATION_ID, null); } private void setState(final State s) { currentState = s; int msgId = R.string.disconnected_from_ms; boolean removeNotification = false; switch (currentState) { case DISCONNECTED: removeNotification = true; break; case CONNECTING: msgId = R.string.connecting_to_ms; break; case CONNECTED: msgId = R.string.connected_to_ms; break; case LOGGING: msgId = R.string.logging; break; default: msgId = R.string.unknown; break; } if (removeNotification) { notifications.cancelAll(); } else { final CharSequence text = getText(R.string.app_name); final Notification notification = new Notification(R.drawable.icon, text, System.currentTimeMillis()); final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, MSLoggerActivity.class), 0); notification.setLatestEventInfo(this, getText(msgId), text, contentIntent); notifications.notify(NOTIFICATION_ID, notification); } } @Override public void onDestroy() { super.onDestroy(); notifications.cancelAll(); // Do not forget to unregister the receiver!!! this.unregisterReceiver(this.yourReceiver); } /** * Shortcut function to access data tables. Makes the INI->Java translation a little simpler * * @param i1 index into table * @param name table name * @return value from table */ protected int table(final int i1, final String name) { return TableManager.INSTANCE.table(i1, name); } @Override public int table(final double d1, final String name) { return table((int) d1, name); } /** * Add a command for the ECUThread to process when it can * * @param command */ public void injectCommand(final InjectedCommand command) { injectionQueue.add(command); } /** * @return true if we're connected to an ECU, false otherwise */ public boolean isConnected() { return (currentState == State.CONNECTED) || (currentState == State.LOGGING); } /** * @return true if we're data logging the ECU realtime stream, false otherwise */ public boolean isLogging() { return currentState == State.LOGGING; } /** * Temperature unit conversion function * * @param t temp in F * @return temp in C if CELSIUS is set, in F otherwise */ @Override public double tempCvt(final double t) { if (isSet("CELSIUS")) { return ((t - 32.0) * 5.0) / 9.0; } else { return t; } } /** * Launch the ECU thread */ public synchronized void start() { DebugLogManager.INSTANCE.log("Megasquirt.start()", Log.INFO); if (ApplicationSettings.INSTANCE.getECUBluetoothMac().equals(ApplicationSettings.MISSING_VALUE)) { broadcast(UNKNOWN_ECU_BT); } else { setState(State.DISCONNECTED); ecuThread = new ECUThread(); ecuThread.setPriority(Thread.MAX_PRIORITY); ecuThread.start(); } } /** * Shut down the ECU thread */ public synchronized void stop() { DebugLogManager.INSTANCE.log("Megasquirt.stop()", Log.INFO); ecuThread = null; setState(State.DISCONNECTED); broadcast(DISCONNECTED); } /** * Revert to initial state */ public void reset() { ecuImplementation.refreshFlags(); constantsLoaded = false; notifications.cancelAll(); } /** * Output the current values to be logged */ private void logValues(final byte[] buffer) { if (!isLogging()) { return; } try { FRDLogManager.INSTANCE.write(buffer); DatalogManager.INSTANCE.write(ecuImplementation.getLogRow()); } catch (final IOException e) { DebugLogManager.INSTANCE.logException(e); } } /** * Shutdown the data connection to the MS */ private void disconnect() { if (simulated) { return; } DebugLogManager.INSTANCE.log("Disconnect", Log.INFO); ECUConnectionManager.getInstance().disconnect(); DatalogManager.INSTANCE.mark("Disconnected"); FRDLogManager.INSTANCE.close(); DatalogManager.INSTANCE.close(); broadcast(DISCONNECTED); } /** * Send a message to the user * * @param msg Message to be sent */ protected void sendMessage(final String msg) { broadcast(ApplicationSettings.GENERAL_MESSAGE, msg); } /** * Send a toast message to the user * * @param message to be sent */ protected void sendToastMessage(final String msg) { final Intent broadcast = new Intent(); broadcast.setAction(ApplicationSettings.TOAST); broadcast.putExtra(ApplicationSettings.TOAST_MESSAGE, msg); sendBroadcast(broadcast); } /** * Send the reads per second to be displayed on the screen * * @param RPS the current reads per second value */ private void sendRPS(final double RPS) { final Intent broadcast = new Intent(); broadcast.setAction(ApplicationSettings.RPS_MESSAGE); broadcast.putExtra(ApplicationSettings.RPS, decimalFormat1dp.format(RPS)); sendBroadcast(broadcast); } /** * Send a status update to the rest of the application * * @param action */ private void broadcast(final String action) { final Intent broadcast = new Intent(); broadcast.setAction(action); sendBroadcast(broadcast); } private void broadcast(final String action, final String data) { DebugLogManager.INSTANCE.log("Megasquirt.broadcast(" + action + "," + data + ")", Log.VERBOSE); final Intent broadcast = new Intent(); broadcast.setAction(action); broadcast.putExtra(ApplicationSettings.MESSAGE, data); sendBroadcast(broadcast); } private void broadcast() { final Intent broadcast = new Intent(); broadcast.setAction(NEW_DATA); sendBroadcast(broadcast); } /** * How long have we been running? * * @return */ @Override public double timeNow() { if (logStart == 0) { logStart = DatalogManager.INSTANCE.getLogStart(); } final long runTime = System.currentTimeMillis() - logStart; final double timeNow = runTime / 1000.0; return timeNow; } /** * Flag the logging process to happen */ public void startLogging() { if (currentState == State.CONNECTED) { currentState = State.LOGGING; DebugLogManager.INSTANCE.log("startLogging()", Log.INFO); } } /** * Stop the logging process */ public void stopLogging() { if (currentState == State.LOGGING) { DebugLogManager.INSTANCE.log("stopLogging()", Log.INFO); currentState = State.CONNECTED; FRDLogManager.INSTANCE.close(); DatalogManager.INSTANCE.close(); } } /** * Take a wild stab at what this does. * * @param v * @return */ @Override public double round(final double v) { return Math.floor((v * 100) + .5) / 100; } /** * Returns if a flag has been set in the application * * @param name * @return */ @Override public boolean isSet(final String name) { return ApplicationSettings.INSTANCE.isSet(name); } /** * The thread that handles all communications with the ECU. This must be done in it's own thread as Android gets very picky about unresponsive UI * threads */ private class ECUThread extends Thread { int interWriteDelay = 0; private class CalculationThread extends Thread { private volatile boolean running = true; public void halt() { DebugLogManager.INSTANCE.log("CalculationThread.halt()", Log.INFO); running = false; } @Override public void run() { this.setName("CalculationThread"); try { while (running) { final byte[] buffer = handshake.get(); if (ecuImplementation != null) { ecuImplementation.calculate(buffer); logValues(buffer); broadcast(); } } } catch (final InterruptedException e) { // Swallow, we're on our way out. } } } class Handshake { private byte[] buffer; public void put(final byte[] buf) { buffer = buf; synchronized (this) { notify(); } } public byte[] get() throws InterruptedException { synchronized (this) { wait(); } return buffer; } } Handshake handshake = new Handshake(); CalculationThread calculationThread = new CalculationThread(); /** * */ public ECUThread() { if (watch != null) { DebugLogManager.INSTANCE.log("Attempting to create second connection!", Log.ASSERT); } watch = this; final String name = "ECUThread:" + System.currentTimeMillis(); setName(name); DebugLogManager.INSTANCE.log("Creating ECUThread named " + name, Log.VERBOSE); interWriteDelay = Integer.parseInt(ApplicationSettings.INSTANCE.getOrSetPref("iwd", "0")); calculationThread.start(); } /** * Kick the connection off */ public void initialiseConnection() { // sendMessage("Launching connection"); // Connection conn = ConnectionFactory.INSTANCE.getConnection(); final String btAddress = ApplicationSettings.INSTANCE.getECUBluetoothMac(); ECUConnectionManager.getInstance().init(null, btAddress); } /** * The main loop of the connection to the ECU */ @Override public void run() { try { setState(Megasquirt.State.CONNECTING); sendMessage("Starting connection"); DebugLogManager.INSTANCE.log("BEGIN connectedThread", Log.INFO); initialiseConnection(); delay(500); try { ECUConnectionManager.getInstance().flushAll(); initialiseImplementation(); /* * Make sure we have calculated runtime vars at least once before refreshing flags. The reason is that the refreshFlags() function * also trigger the creation of menus/dialogs/tables/curves/etc that use variables such as {clthighlim} in curves that need to * have their value assigned before being used. */ try { final byte[] bufferRV = getRuntimeVars(); ecuImplementation.calculate(bufferRV); } catch (final CRC32Exception e) { DebugLogManager.INSTANCE.logException(e); } catch (final BTTimeoutException e) { DebugLogManager.INSTANCE.logException(e); } // Make sure everyone agrees on what flags are set ApplicationSettings.INSTANCE.refreshFlags(); ecuImplementation.refreshFlags(); if (!constantsLoaded) { // Only do this once so reconnects are quicker ecuImplementation.loadConstants(simulated); constantsLoaded = true; } sendMessage("Connected to " + getTrueSignature()); setState(Megasquirt.State.CONNECTED); long lastRpsTime = System.currentTimeMillis(); double readCounter = 0; // This is the actual work. Outside influences will toggle 'running' when we want this to stop while ((currentState == Megasquirt.State.CONNECTED) || (currentState == Megasquirt.State.LOGGING)) { try { if (injectionQueue.peek() != null) { for (final InjectedCommand i : injectionQueue) { processCommand(i); } injectionQueue.clear(); } final byte[] buffer = getRuntimeVars(); handshake.put(buffer); } catch (final CRC32Exception e) { DatalogManager.INSTANCE.mark(e.getLocalizedMessage()); DebugLogManager.INSTANCE.logException(e); } catch (final BTTimeoutException e) { DatalogManager.INSTANCE.mark(e.getLocalizedMessage()); DebugLogManager.INSTANCE.logException(e); // Get out of the loop, we're going to disconnect break; } catch (final IOException e) { DatalogManager.INSTANCE.mark(e.getLocalizedMessage()); DebugLogManager.INSTANCE.logException(e); initialiseConnection(); ECUConnectionManager.getInstance().connect(); } readCounter++; final long delay = System.currentTimeMillis() - lastRpsTime; if (delay > 1000) { final double RPS = (readCounter / delay) * 1000; readCounter = 0; lastRpsTime = System.currentTimeMillis(); if (RPS > 0) { sendRPS(RPS); } } } } catch (final IOException e) { DebugLogManager.INSTANCE.logException(e); } catch (final CRC32Exception e) { DebugLogManager.INSTANCE.logException(e); } catch (final ArithmeticException e) { // If we get a maths error, we probably have loaded duff constants and hit a divide by zero // force the constants to reload in case it was just a bad data read DebugLogManager.INSTANCE.logException(e); constantsLoaded = false; } catch (final RuntimeException t) { DebugLogManager.INSTANCE.logException(t); throw (t); } // We're on our way out, so drop the connection disconnect(); } finally { calculationThread.halt(); calculationThread.interrupt(); watch = null; } } private void processCommand(final InjectedCommand i) throws IOException { ECUConnectionManager.getInstance().writeCommand(i.getCommand(), i.getDelay(), ecuImplementation.isCRC32Protocol()); // If we want to get the result back if (i.isReturnResult()) { final Intent broadcast = new Intent(); broadcast.setAction(INJECTED_COMMAND_RESULTS); final byte[] result = ECUConnectionManager.getInstance().readBytes(); broadcast.putExtra(INJECTED_COMMAND_RESULT_ID, i.getResultId()); broadcast.putExtra(INJECTED_COMMAND_RESULT_DATA, result); sendBroadcast(broadcast); } } private void initialiseImplementation() throws IOException, CRC32Exception { sendMessage("Checking your ECU"); final String signature = getSignature(); setImplementation(signature); } private String getSignature() throws IOException, CRC32Exception { final byte[] bootCommand = { 'X' }; final String lastSuccessfulProbeCommand = ApplicationSettings.INSTANCE.getPref(LAST_PROBE); final String lastSig = ApplicationSettings.INSTANCE.getPref(LAST_SIG); if ((lastSuccessfulProbeCommand != null) && (lastSig != null)) { final byte[] probe = lastSuccessfulProbeCommand.getBytes(); // We need to loop as a BT adapter can pump crap into the MS at the start which confuses the poor thing. for (int i = 0; i < 3; i++) { byte[] response = ECUConnectionManager.getInstance().writeAndRead(probe, 50, false); try { final String sig = processResponse(response); if (lastSig.equals(sig)) { return sig; } } catch (final BootException e) { response = ECUConnectionManager.getInstance().writeAndRead(bootCommand, 500, false); } } } final String probeCommand1 = "Q"; final String probeCommand2 = "S"; String probeUsed; int i = 0; String sig = UNKNOWN; // IF we don't get it in 20 goes, we're not talking to a Megasquirt while (i++ < 20) { probeUsed = probeCommand1; byte[] response = ECUConnectionManager.getInstance().writeAndRead(probeUsed.getBytes(), 500, false); try { if ((response != null) && (response.length > 1)) { sig = processResponse(response); } else { probeUsed = probeCommand2; response = ECUConnectionManager.getInstance().writeAndRead(probeUsed.getBytes(), 500, false); if ((response != null) && (response.length > 1)) { sig = processResponse(response); } } if (!UNKNOWN.equals(sig)) { ApplicationSettings.INSTANCE.setPref(LAST_PROBE, probeUsed); ApplicationSettings.INSTANCE.setPref(LAST_SIG, sig); ECUConnectionManager.getInstance().flushAll(); break; } } catch (final BootException e) { /* * My ECU also occasionally goes to a Boot> prompt on start up (dodgy electrics) so if we see that, force the ECU to start. */ response = ECUConnectionManager.getInstance().writeAndRead(bootCommand, 500, false); } } return sig; } /** * Attempt to figure out the data we got back from the device * * @param response * @return * @throws BootException */ private String processResponse(final byte[] response) throws BootException { final String result = new String(response); trueSignature = result; if (result.contains("Boot>")) { throw new BootException(); } if (response == null) { return UNKNOWN; } // Early ECUs only respond with one byte if ((response.length == 1) && (response[0] != 20)) { return UNKNOWN; } if (response.length <= 1) { return UNKNOWN; } // Examine the first few bytes and see if it smells of one of the things an MS may say to us. if (((response[0] != 'M') && (response[0] != 'J')) || ((response[1] != 'S') && (response[1] != 'o') && (response[1] != 'i'))) { return UNKNOWN; } // Looks like we have a Megasquirt return result; } /** * Get the current variables from the ECU * * @throws IOException * @throws CRC32Exception * @throws BTTimeoutException */ private byte[] getRuntimeVars() throws IOException, CRC32Exception, BTTimeoutException { if (interWriteDelay > 0) { delay(interWriteDelay); } final byte[] buffer = new byte[ecuImplementation.getBlockSize()]; if (simulated) { MSSimulator.INSTANCE.getNextRTV(buffer); return buffer; } final int delay = interWriteDelay == 0 ? ecuImplementation.getInterWriteDelay() : interWriteDelay; ECUConnectionManager.getInstance().writeAndRead(ecuImplementation.getOchCommand(), buffer, delay, ecuImplementation.isCRC32Protocol()); return buffer; } private void delay(final int delay) { try { Thread.sleep(delay); } catch (final InterruptedException e) { DebugLogManager.INSTANCE.logException(e); } } /** * Read a page of constants from the ECU into a byte buffer. MS1 uses a select/read combo, MS2 just does a read * * @param pageBuffer * @param pageSelectCommand * @param pageReadCommand * @throws IOException */ protected void getPage(final byte[] pageBuffer, final byte[] pageSelectCommand, final byte[] pageReadCommand) throws IOException, CRC32Exception { ECUConnectionManager.getInstance().flushAll(); for (int i = 1; i <= 5; i++) { final int delay = ecuImplementation.getPageActivationDelay() * i; if (pageSelectCommand != null) { ECUConnectionManager.getInstance().writeCommand(pageSelectCommand, delay, ecuImplementation.isCRC32Protocol()); } if (pageReadCommand != null) { ECUConnectionManager.getInstance().writeCommand(pageReadCommand, delay, ecuImplementation.isCRC32Protocol()); } try { ECUConnectionManager.getInstance().readBytes(pageBuffer, ecuImplementation.isCRC32Protocol()); return; } catch (final BTTimeoutException e) { DebugLogManager.INSTANCE.logException(e); } } } } /** * * @return */ public String getTrueSignature() { return trueSignature; } /** * helper method for subclasses * * @param pageNo * @param pageOffset * @param pageSize * @param select * @param read * @return */ @Override public byte[] loadPage(final int pageNo, final int pageOffset, final int pageSize, final byte[] select, final byte[] read) { final byte[] buffer = new byte[pageSize]; try { sendMessage("Loading constants from page " + pageNo); getPage(buffer, select, read); savePage(pageNo, buffer); sendMessage("Constants loaded from page " + pageNo); } catch (final IOException e) { e.printStackTrace(); DebugLogManager.INSTANCE.logException(e); sendMessage("Error loading constants from page " + pageNo); } catch (final CRC32Exception e) { e.printStackTrace(); DebugLogManager.INSTANCE.logException(e); sendMessage("Error loading constants from page " + pageNo); } return buffer; } /** * * @param buffer * @param select * @param read * @throws IOException */ private void getPage(final byte[] buffer, final byte[] select, final byte[] read) throws IOException, CRC32Exception { ecuThread.getPage(buffer, select, read); } /** * Dumps a loaded page to SD card for analysis * * @param pageNo * @param buffer */ private void savePage(final int pageNo, final byte[] buffer) { try { final File dir = new File(Environment.getExternalStorageDirectory(), "MSLogger"); if (!dir.exists()) { final boolean mkDirs = dir.mkdirs(); if (!mkDirs) { DebugLogManager.INSTANCE.log("Unable to create directory MSLogger at " + Environment.getExternalStorageDirectory(), Log.ERROR); } } final String fileName = ecuImplementation.getClass().getName() + ".firmware"; final File outputFile = new File(dir, fileName); BufferedOutputStream out = null; try { final boolean append = !(pageNo == 1); out = new BufferedOutputStream(new FileOutputStream(outputFile, append)); DebugLogManager.INSTANCE.log("Saving page " + pageNo + " append=" + append, Log.INFO); out.write(buffer); } finally { if (out != null) { out.flush(); out.close(); } } } catch (final IOException e) { DebugLogManager.INSTANCE.logException(e); } } /** * Write a constant back to the ECU * * @param constant The constant to write */ public void writeConstant(final Constant constant) { final List<String> pageIdentifiers = ecuImplementation.getPageIdentifiers(); final List<String> pageValueWrites = ecuImplementation.getPageValueWrites(); // Ex: U08, S16 final String type = constant.getType(); // 8 bits = 1 byte by default int size = 1; if (type.contains("16")) { size = 2; // 16 bits = 2 bytes } final int pageNo = constant.getPage(); final int offset = constant.getOffset(); int[] msValue = null; // Constant to write is of type scalar or bits if (constant.getClassType().equals("scalar") || constant.getClassType().equals("bits")) { msValue = new int[1]; msValue[0] = (int) getField(constant.getName()); } // Constant to write to ECU is of type array else if (constant.getClassType().equals("array")) { final int shape[] = MSUtilsShared.getArraySize(constant.getShape()); final int width = shape[0]; final int height = shape[1]; // Vector if (height == -1) { size *= width; msValue = getVector(constant.getName()); } // Array else { // Flatten array into msValue final int[][] array = getArray(constant.getName()); int i = 0; size *= width * height; msValue = new int[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { msValue[i++] = array[x][y]; } } } } // Make sure we have something to send to the MS if ((msValue != null) && (msValue.length > 0)) { final String writeCommand = pageValueWrites.get(pageNo - 1); final String command = MSUtilsShared.HexStringToBytes(pageIdentifiers, writeCommand, offset, size, msValue, pageNo); final byte[] byteCommand = MSUtils.INSTANCE.commandStringtoByteArray(command); DebugLogManager.INSTANCE.log("Writing to MS: command: " + command + " constant: " + constant.getName() + " msValue: " + Arrays.toString(msValue) + " pageValueWrite: " + writeCommand + " offset: " + offset + " count: " + size + " pageNo: " + pageNo, Log.DEBUG); final List<byte[]> pageActivates = ecuImplementation.getPageActivates(); try { final int delay = ecuImplementation.getPageActivationDelay(); // MS1 use page select command if (pageActivates.size() >= pageNo) { final byte[] pageSelectCommand = pageActivates.get(pageNo - 1); ECUConnectionManager.getInstance().writeCommand(pageSelectCommand, delay, ecuImplementation.isCRC32Protocol()); } final InjectedCommand writeToRAM = new InjectedCommand(byteCommand, 300, false, 0); injectCommand(writeToRAM); Toast.makeText(this, "Writing constant " + constant.getName() + " to MegaSquirt", Toast.LENGTH_SHORT).show(); } catch (final IOException e) { DebugLogManager.INSTANCE.logException(e); } burnPage(pageNo); } // Nothing to send to the MS, maybe unsupported constant type ? else { DebugLogManager.INSTANCE.log("Couldn't find any value to write, maybe unsupported constant type " + constant.getType(), Log.DEBUG); } } /** * Burn a page from MS RAM to Flash * * @param pageNo The page number to burn */ private void burnPage(final int pageNo) { // Convert from page to table index that the ECU understand final List<String> pageIdentifiers = ecuImplementation.getPageIdentifiers(); final String pageIdentifier = pageIdentifiers.get(pageNo - 1).replace("\\$tsCanId\\", ""); final byte tblIdx = (byte) MSUtilsShared.HexByteToDec(pageIdentifier); DebugLogManager.INSTANCE.log("Burning page " + pageNo + " (Page identifier: " + pageIdentifier + " - Table index: " + tblIdx + ")", Log.DEBUG); // Send "b" command for the tblIdx final InjectedCommand burnToFlash = new InjectedCommand(new byte[] { 98, 0, tblIdx }, 300, true, Megasquirt.BURN_DATA); injectCommand(burnToFlash); Toast.makeText(this, "Burning page " + pageNo + " to MegaSquirt", Toast.LENGTH_SHORT).show(); } /** * Get an array from the ECU * * @param channelName The variable name to modify * @return */ public int[][] getArray(final String channelName) { int[][] value = { { 0 }, { 0 } }; final Class<?> c = ecuImplementation.getClass(); try { final Field f = c.getDeclaredField(channelName); value = (int[][]) f.get(ecuImplementation); } catch (final Exception e) { DebugLogManager.INSTANCE.log("Failed to get array value for " + channelName, Log.ERROR); } return value; } /** * Get a vector from the ECU * * @param channelName The variable name to modify * @return */ public int[] getVector(final String channelName) { int[] value = { 0 }; final Class<?> c = ecuImplementation.getClass(); try { final Field f = c.getDeclaredField(channelName); value = (int[]) f.get(ecuImplementation); } catch (final Exception e) { DebugLogManager.INSTANCE.log("Failed to get vector value for " + channelName, Log.ERROR); } return value; } /** * * @param channelName * @param value */ public void setField(final String channelName, final int value) { final Class<?> c = ecuImplementation.getClass(); try { final Field f = c.getDeclaredField(channelName); f.setInt(ecuImplementation, value); } catch (final NoSuchFieldException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + value + " for " + channelName + ", no such field", Log.ERROR); } catch (final IllegalArgumentException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + value + " for " + channelName + ", illegal argument", Log.ERROR); } catch (final IllegalAccessException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + value + " for " + channelName + ", illegal access", Log.ERROR); } } /** * Set a vector in the ECU class * * @param channelName The variable name to modify * @param double[] * @return */ public void setVector(final String channelName, final int[] xBins) { final Class<?> c = ecuImplementation.getClass(); try { final Field f = c.getDeclaredField(channelName); f.set(ecuImplementation, xBins); } catch (final NoSuchFieldException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + xBins + " for " + channelName + ", no such field", Log.ERROR); } catch (final IllegalArgumentException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + xBins + " for " + channelName + ", illegal argument", Log.ERROR); } catch (final IllegalAccessException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + xBins + " for " + channelName + ", illegal access", Log.ERROR); } } /** * Set an array in the ECU class * * @param channelName The variable name to modify * @param double[][] * @return * */ public void setArray(final String channelName, final int[][] zBins) { final Class<?> c = ecuImplementation.getClass(); try { final Field f = c.getDeclaredField(channelName); f.set(ecuImplementation, zBins); } catch (final NoSuchFieldException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + zBins + " for " + channelName + ", no such field", Log.ERROR); } catch (final IllegalArgumentException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + zBins + " for " + channelName + ", illegal argument", Log.ERROR); } catch (final IllegalAccessException e) { DebugLogManager.INSTANCE.log("Failed to set value to " + zBins + " for " + channelName + ", illegal access", Log.ERROR); } } /** * Load a byte array contained in pageBuffer from the specified offset and width * * @param pageBuffer The buffer where the byte array is located * @param offset The offset where the byte array is located * @param width The width of the byte array * @param height The height of the byte array * @param signed Is the data signed ? * * @return */ @Override public int[][] loadByteArray(final byte[] pageBuffer, final int offset, final int width, final int height, final boolean signed) { final int[][] destination = new int[width][height]; int index = offset; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int value = signed ? MSUtils.INSTANCE.getSignedByte(pageBuffer, index) : MSUtils.INSTANCE.getByte(pageBuffer, index); destination[x][y] = value; index = index + 1; } } return destination; } /** * Load a byte vector contained in pageBuffer from the specified offset and width * * @param pageBuffer The buffer where the byte vector is located * @param offset The offset where the byte vector is located * @param width The width of the byte vector * @param signed Is the data signed ? * * @return */ @Override public int[] loadByteVector(final byte[] pageBuffer, final int offset, final int width, final boolean signed) { final int[] destination = new int[width]; int index = offset; for (int x = 0; x < width; x++) { final int value = signed ? MSUtils.INSTANCE.getSignedByte(pageBuffer, index) : MSUtils.INSTANCE.getByte(pageBuffer, index); destination[x] = value; index = index + 1; } return destination; } /** * Load a word array contained in pageBuffer from the specified offset and width * * @param pageBuffer The buffer where the word array is located * @param offset The offset where the word array is located * @param width The width of the word array * @param height The height of the word array * @param signed Is the data signed ? * * @return */ @Override public int[][] loadWordArray(final byte[] pageBuffer, final int offset, final int width, final int height, final boolean signed) { final int[][] destination = new int[width][height]; int index = offset; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final int value = signed ? MSUtils.INSTANCE.getSignedWord(pageBuffer, index) : MSUtils.INSTANCE.getWord(pageBuffer, index); destination[x][y] = value; index = index + 2; } } return destination; } /** * Load a word vector contained in pageBuffer from the specified offset and width * * @param pageBuffer The buffer where the word vector is located * @param offset The offset where the word vector is located * @param width The width of the word vector * @param signed Is the data signed ? * * @return */ @Override public int[] loadWordVector(final byte[] pageBuffer, final int offset, final int width, final boolean signed) { final int[] destination = new int[width]; int index = offset; for (int x = 0; x < width; x++) { final int value = signed ? MSUtils.INSTANCE.getSignedWord(pageBuffer, index) : MSUtils.INSTANCE.getWord(pageBuffer, index); destination[x] = value; index = index + 2; } return destination; } /** * Helper function to know if a constant name exists * * @param name The name of the constant * @return true if the constant exists, false otherwise */ public boolean isConstantExists(final String name) { return MSECUInterface.constants.containsKey(name); } /** * Get a constant from the ECU class * * @param name The name of the constant * @return The constant object */ public Constant getConstantByName(final String name) { return MSECUInterface.constants.get(name); } /** * Get an output channel from the ECU class * * @param name The name of the output channel * @return The output channel object */ public OutputChannel getOutputChannelByName(final String name) { return MSECUInterface.outputChannels.get(name); } /** * Get a table editor from the ECU class * * @param name The name of the table editor object * @return The table editor object */ public TableEditor getTableEditorByName(final String name) { return MSECUInterface.tableEditors.get(name); } /** * Get a curve editor from the ECU class * * @param name The name of the curve editor object * @return The curve editor object */ public CurveEditor getCurveEditorByName(final String name) { return MSECUInterface.curveEditors.get(name); } /** * Get a list of menus from the ECU class * * @param name The name of the menu tree * @return A list of menus object */ public List<Menu> getMenusForDialog(final String name) { return MSECUInterface.menus.get(name); } /** * Get a dialog from the ECU class * * @param name The name of the dialog object * @return The dialog object */ public MSDialog getDialogByName(final String name) { return MSECUInterface.dialogs.get(name); } /** * Get a visibility flag for a user defined (dialog, field, panel, etc) Used for field in dialog, for example * * @param name The name of the user defined flag * @return true if visible, false otherwise */ public boolean getUserDefinedVisibilityFlagsByName(final String name) { if (MSECUInterface.userDefinedVisibilityFlags.containsKey(name)) { return MSECUInterface.userDefinedVisibilityFlags.get(name); } return true; } /** * Get a visibility flag for a menu * * @param name The name of the menu flag * @return true if visible, false otherwise */ public boolean getMenuVisibilityFlagsByName(final String name) { if (MSECUInterface.menuVisibilityFlags.containsKey(name)) { return MSECUInterface.menuVisibilityFlags.get(name); } return true; } /** * Add a dialog to the list of dialogs in the ECU class * * @param dialog The dialog object to add */ public void addDialog(final MSDialog dialog) { MSECUInterface.dialogs.put(dialog.getName(), dialog); } /** * Add a curve to the list of curves in the ECU class * * @param curve The curve object to add */ public void addCurve(final CurveEditor curve) { MSECUInterface.curveEditors.put(curve.getName(), curve); } /** * Add a constant to the list of constants in the ECU class * * @param constant The constant object to add */ public void addConstant(final Constant constant) { MSECUInterface.constants.put(constant.getName(), constant); } /** * Used to get a list of all constants name used in a specific dialog * * @param dialog The dialog to get the list of constants name * @return A list of constants name */ public List<String> getAllConstantsNamesForDialog(final MSDialog dialog) { final List<String> constants = new ArrayList<String>(); return buildListOfConstants(constants, dialog); } /** * Helper function for getAllConstantsNamesForDialog() which builds the array of constants name * * @param constants * @param dialog */ private List<String> buildListOfConstants(final List<String> constants, final MSDialog dialog) { for (final DialogField df : dialog.getFieldsList()) { if (!df.getName().equals("null")) { constants.add(df.getName()); } } for (final DialogPanel dp : dialog.getPanelsList()) { final MSDialog dialogPanel = this.getDialogByName(dp.getName()); if (dialogPanel != null) { buildListOfConstants(constants, dialogPanel); } } return constants; } /** * * @return */ public int getBlockSize() { return ecuImplementation.getBlockSize(); } /** * * @return */ public int getCurrentTPS() { return ecuImplementation.getCurrentTPS(); } /** * * @return */ public String getLogHeader() { return ecuImplementation.getLogHeader(); } /** * */ public void refreshFlags() { ecuImplementation.refreshFlags(); } /** * */ public void setMenuVisibilityFlags() { ecuImplementation.setMenuVisibilityFlags(); } /** * */ public void setUserDefinedVisibilityFlags() { ecuImplementation.setUserDefinedVisibilityFlags(); } /** * * @return */ public String[] getControlFlags() { return ecuImplementation.getControlFlags(); } /** * * @return */ public List<String> getRequiresPowerCycle() { return ecuImplementation.getRequiresPowerCycle(); } public List<SettingGroup> getSettingGroups() { ecuImplementation.createSettingGroups(); return ecuImplementation.getSettingGroups(); } public Map<String, String> getControllerCommands() { ecuImplementation.createControllerCommands(); return ecuImplementation.getControllerCommands(); } /** * Helper functions to get specific value out of ECU Different MS version have different name for the same thing so get the right one depending on * the MS version we're connected to */ /** * @return Return the current ECU cylinders count */ public int getCylindersCount() { return (int) (isConstantExists("nCylinders") ? getField("nCylinders") : getField("nCylinders1")); } /** * @return Return the current ECU injectors count */ public int getInjectorsCount() { return (int) (isConstantExists("nInjectors") ? getField("nInjectors") : getField("nInjectors1")); } /** * @return Return the current ECU divider */ public int getDivider() { return (int) (isConstantExists("divider") ? getField("divider") : getField("divider1")); } /** * Return the current ECU injector staging * * @return 0 = Simultaneous, 1 = Alternating */ public int getInjectorStating() { return (int) (isConstantExists("alternate") ? getField("alternate") : getField("alternate1")); } public double getField(final String channelName) { double value = 0; final Class<?> c = ecuImplementation.getClass(); try { final Field f = c.getDeclaredField(channelName); value = f.getDouble(ecuImplementation); } catch (final Exception e) { DebugLogManager.INSTANCE.log("Failed to get value for " + channelName, Log.ERROR); } return value; } private void setImplementation(final String signature) { final Class<? extends MSECUInterface> ecuClass = ECURegistry.INSTANCE.findEcu(signature); if ((ecuImplementation != null) && ecuImplementation.getClass().equals(ecuClass)) { broadcast(PROBE_ECU); return; } Constructor<? extends MSECUInterface> constructor; try { constructor = ecuClass.getConstructor(MSControllerInterface.class, MSUtilsInterface.class); ecuImplementation = constructor.newInstance(Megasquirt.this, MSUtils.INSTANCE); if (!signature.equals(ecuImplementation.getSignature())) { trueSignature = ecuImplementation.getSignature(); final String msg = "Got unsupported signature from Megasquirt \"" + trueSignature + "\" but found a similar supported signature \"" + signature + "\""; sendToastMessage(msg); DebugLogManager.INSTANCE.log(msg, Log.INFO); } sendMessage("Found " + trueSignature); } catch (final Exception e) { DebugLogManager.INSTANCE.logException(e); broadcast(UNKNOWN_ECU); } broadcast(PROBE_ECU); } @Override public void registerOutputChannel(final OutputChannel o) { DataManager.getInstance().addOutputChannel(o); } }