using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Iot.Device.Arduino;
using Iot.Device.Common;
using Microsoft.Extensions.Logging;
using UnitsNet;
namespace ArduinoCsCompiler
{
internal class CompilerCommandHandler : ExtendedCommandHandler
{
private static readonly TimeSpan ProgrammingTimeout = TimeSpan.FromMinutes(2);
public const int SchedulerData = 0x7B;
private readonly MicroCompiler _compiler;
private readonly ILogger _logger;
private IlCapabilities? _ilCapabilities;
private int _maxBytesPerMessage = 64;
public CompilerCommandHandler(MicroCompiler compiler)
{
_compiler = compiler;
_logger = this.GetCurrentClassLogger();
}
public IlCapabilities? IlCapabilities
{
get
{
return _ilCapabilities;
}
set
{
_ilCapabilities = value;
}
}
protected override void OnErrorMessage(string message, Exception? exception)
{
_compiler.OnCompilerCallback(0, message, MethodState.ConnectionError, exception);
base.OnErrorMessage(message, exception);
}
protected override void OnSysexData(ReplyType type, byte[] data)
{
if (type == ReplyType.AsciiData)
{
string rawMessage = Encoding.Unicode.GetString(data);
Logger.LogInformation(rawMessage);
}
if (type != ReplyType.SysexCommand)
{
return;
}
CommandError error = CommandError.None;
ParseReply(data, ExecutorCommand.None, ref error);
}
private void WaitAndHandleIlCommand(FirmataIlCommandSequence commandSequence)
{
WaitAndHandleIlCommand(commandSequence, ProgrammingTimeout);
}
private void WaitAndHandleIlCommand(FirmataIlCommandSequence commandSequence, TimeSpan timeout)
{
CommandError error = CommandError.None;
try
{
while (timeout >= TimeSpan.Zero)
{
var data = SendCommandAndWait(commandSequence, timeout, out error);
if (ExpectAck(data, commandSequence.Command, commandSequence.SequenceNumber, ref error))
{
break;
}
timeout -= TimeSpan.FromMilliseconds(20);
}
if (timeout <= TimeSpan.Zero)
{
error = CommandError.Timeout;
}
}
catch (TimeoutException tx)
{
throw new TimeoutException($"Arduino failed to accept IL command {commandSequence.Command}.", tx);
}
if (error != 0)
{
throw new TaskSchedulerException($"Task scheduler method returned state {error}.");
}
}
protected override bool IsMatchingAck(FirmataCommandSequence sequence, byte[] reply)
{
if (sequence is FirmataIlCommandSequence ilCommand)
{
if (reply.Length != 5 || reply[0] != SchedulerData || reply[4] != ilCommand.SequenceNumber)
{
return false;
}
}
return base.IsMatchingAck(sequence, reply);
}
protected override CommandError HasCommandError(FirmataCommandSequence sequence, byte[] reply)
{
if (sequence is FirmataIlCommandSequence ilCommand)
{
CommandError error = CommandError.None;
ExpectAck(reply, ilCommand.Command, ilCommand.SequenceNumber, ref error);
return error;
}
return base.HasCommandError(sequence, reply);
}
private bool ExpectAck(byte[] data, ExecutorCommand expectedCommand, int expectedSequenceNo, ref CommandError error)
{
if (data.Length >= 5 && data[0] == SchedulerData && data[2] == (byte)expectedCommand)
{
if (data.Length == 5 && data[1] == (byte)ExecutorCommand.Ack && data[4] == expectedSequenceNo)
{
return true;
}
if (data.Length == 5 && data[1] == (byte)ExecutorCommand.Nack && data[4] == expectedSequenceNo)
{
error = (CommandError)data[3];
_logger.LogWarning($"Received NoACK for command {expectedCommand}");
return true;
}
}
return false;
}
private bool ParseReply(byte[] data, ExecutorCommand expectedCommand, ref CommandError error)
{
if (data.Length > 0 && data[0] == SchedulerData)
{
if (data.Length == 4 && data[1] == (byte)ExecutorCommand.Ack)
{
if (data[2] == (byte)expectedCommand)
{
return true;
}
return false;
}
if (data.Length == 4 && data[1] == (byte)ExecutorCommand.Nack)
{
error = (CommandError)data[3];
}
else if (data.Length < 7)
{
error = CommandError.InvalidArguments;
}
else if (data[1] == (byte)ExecutorCommand.Reply && data[2] == (byte)RuntimeState.TaskTermination)
{
ParseTaskTerminationResult(data, out error);
}
else if (data[1] == (byte)ExecutorCommand.Reply)
{
if (data[2] == (byte)ExecutorCommand.QueryHardware)
{
var ilCapabilities = new IlCapabilities()
{
FlashSize = Information.FromBytes(FirmataIlCommandSequence.DecodeInt32(data, 8)),
FlashUsed = Information.FromBytes(FirmataIlCommandSequence.DecodeInt32(data, 8 + 5)),
IntSize = Information.FromBytes(data[6]),
PointerSize = Information.FromBytes(data[7]),
RamSize = Information.FromBytes(FirmataIlCommandSequence.DecodeInt32(data, 8 + 10)),
ProtocolVersion = FirmataCommandSequence.DecodeInt14(data, 4),
};
_ilCapabilities = ilCapabilities;
_maxBytesPerMessage = Math.Min(FirmataCommandSequence.DecodeInt32(data, 8 + 15), 64);
_logger.LogInformation(_ilCapabilities.ToString());
}
else if (data[2] == (byte)ExecutorCommand.ConditionalBreakpointHit || data[2] == (byte)ExecutorCommand.Variables)
{
_compiler.OnCompilerCallback(data[4] | (data[5] << 7), string.Empty, MethodState.Debugging, data);
}
}
else
{
return false;
}
return true;
}
return false;
}
private void ParseTaskTerminationResult(byte[] data, out CommandError error)
{
int startIndex = 2;
MethodState state = (MethodState)data[startIndex + 3];
int numArgs = data[startIndex + 4];
if (state == MethodState.Aborted)
{
int[] results = new int[numArgs];
for (int i = 0; i < numArgs; i++)
{
results[i] = FirmataCommandSequence.DecodeInt32(data, i * 5 + startIndex + 5);
}
error = CommandError.Aborted;
_compiler.OnCompilerCallback(data[startIndex + 1] | (data[startIndex + 2] << 7), string.Empty, state, results);
}
else
{
error = CommandError.None;
var result = FirmataIlCommandSequence.Decode7BitBytes(data.Skip(startIndex + 5).ToArray(), numArgs);
_compiler.OnCompilerCallback(data[startIndex + 1] | (data[startIndex + 2] << 7), string.Empty, state, result);
}
}
public void AddMethodIlCode(List<FirmataCommandSequence> sequences, int methodToken, byte[] byteCode)
{
int bytesPerPacket = (_maxBytesPerMessage - 15) * 7 / 8;
int codeIndex = 0;
while (codeIndex < byteCode.Length)
{
FirmataIlCommandSequence sequence = new(ExecutorCommand.LoadIl);
sequence.SendInt32(methodToken);
ushort len = (ushort)byteCode.Length;
sequence.WriteByte((byte)(len & 0x7f));
sequence.WriteByte((byte)(len >> 7));
sequence.WriteByte((byte)(codeIndex & 0x7f));
sequence.WriteByte((byte)(codeIndex >> 7));
int bytesThisPacket = Math.Min(bytesPerPacket, byteCode.Length - codeIndex);
var bytesToSend = Encoder7Bit.Encode(byteCode, codeIndex, bytesThisPacket);
sequence.Write(bytesToSend);
codeIndex += bytesThisPacket;
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
Debug.Assert(sequence.Length <= _maxBytesPerMessage, "Message is to long");
sequences.Add(sequence);
}
}
public void ExecuteIlCode(int methodToken, short taskId, object[] parameters)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.StartTask);
sequence.SendInt32(methodToken);
sequence.SendInt14(taskId);
for (int i = 0; i < parameters.Length; i++)
{
byte[] param;
Type t = parameters[i].GetType();
if (t == typeof(Int32) || t == typeof(Int16) || t == typeof(sbyte) || t == typeof(bool))
{
param = BitConverter.GetBytes(Convert.ToInt32(parameters[i]));
}
else if (t == typeof(UInt32) || t == typeof(UInt16) || t == typeof(byte))
{
param = BitConverter.GetBytes(Convert.ToUInt32(parameters[i]));
}
else if (t == typeof(float))
{
param = BitConverter.GetBytes(Convert.ToSingle(parameters[i]));
}
else if (t == typeof(double))
{
param = BitConverter.GetBytes(Convert.ToDouble(parameters[i]));
}
else if (t == typeof(ulong))
{
param = BitConverter.GetBytes(Convert.ToUInt64(parameters[i]));
}
else if (t == typeof(long))
{
param = BitConverter.GetBytes(Convert.ToInt64(parameters[i]));
}
else
{
param = BitConverter.GetBytes(Convert.ToUInt32(0));
}
sequence.WriteBytesAsTwo7bitBytes(param);
}
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
public void SendMethod(ArduinoMethodDeclaration decl, ClassMember[] localTypes, ClassMember[] argTypes)
{
List<FirmataCommandSequence> sequences = new();
AddMethodDeclarations(sequences, decl.Token, decl.Flags, (byte)decl.MaxStack,
(byte)decl.ArgumentCount, decl.NativeMethod, localTypes, argTypes);
if (decl.HasBody && decl.NativeMethod == 0)
{
AddMethodIlCode(sequences, decl.Token, decl.Code.IlBytes!);
if (decl.Code.ExceptionClauses != null && decl.Code.ExceptionClauses.Any())
{
AddMethodExceptionClauses(sequences, decl.Token, decl.Code.ExceptionClauses);
}
}
SendCommandsAndWait(sequences, ProgrammingTimeout, out var error);
if (error != CommandError.None)
{
throw new TaskSchedulerException($"Command sequence returned error {error}");
}
}
public void AddMethodDeclarations(IList<FirmataCommandSequence> sequences, int declarationToken, MethodFlags methodFlags, byte maxStack, byte argCount,
int nativeMethod, ClassMember[] localTypes, ClassMember[] argTypes)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.DeclareMethod);
sequence.SendInt32(declarationToken);
sequence.SendUInt14((ushort)methodFlags);
sequence.WriteByte(maxStack);
sequence.WriteByte(argCount);
sequence.SendInt32((int)nativeMethod);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
sequences.Add(sequence);
int startIndex = 0;
int totalLocals = localTypes.Length;
int localsToSend = Math.Min(localTypes.Length, 16);
while (localsToSend > 0)
{
sequence = new FirmataIlCommandSequence(ExecutorCommand.MethodSignature);
sequence.SendInt32(declarationToken);
sequence.WriteByte(1);
sequence.WriteByte((byte)localsToSend);
for (int i = startIndex; i < startIndex + localsToSend; i++)
{
sequence.WriteByte((byte)localTypes[i].VariableType);
int sizeOfField = localTypes[i].SizeOfField;
if (sizeOfField > 0x3FFF)
{
throw new InvalidOperationException("Variables with size > 2^14 are not supported as locals");
}
sequence.SendInt14(sizeOfField);
}
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
sequences.Add(sequence);
totalLocals -= 16;
startIndex += 16;
localsToSend = Math.Min(totalLocals, 16);
}
startIndex = 0;
totalLocals = argTypes.Length;
localsToSend = Math.Min(argTypes.Length, 16);
while (localsToSend > 0)
{
sequence = new FirmataIlCommandSequence(ExecutorCommand.MethodSignature);
sequence.SendInt32(declarationToken);
sequence.WriteByte(0);
sequence.WriteByte((byte)localsToSend);
for (int i = startIndex; i < startIndex + localsToSend; i++)
{
sequence.WriteByte((byte)argTypes[i].VariableType);
int sizeOfField = argTypes[i].SizeOfField;
if (sizeOfField > 0x3FFF)
{
throw new InvalidOperationException("Variables with size > 2^14 are not supported as arguments");
}
sequence.SendInt14(sizeOfField);
}
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
sequences.Add(sequence);
totalLocals -= 16;
startIndex += 16;
localsToSend = Math.Min(totalLocals, 16);
}
}
public void AddMethodExceptionClauses(IList<FirmataCommandSequence> sequences, int token, List<ExceptionClause> exceptionClauses)
{
foreach (var c in exceptionClauses)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.ExceptionClauses);
sequence.SendInt32(token);
sequence.SendInt32((int)c.Clause);
sequence.SendInt32(c.TryOffset);
sequence.SendInt32(c.TryLength);
sequence.SendInt32(c.HandlerOffset);
sequence.SendInt32(c.HandlerLength);
sequence.SendInt32(c.ExceptionFilterToken);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
sequences.Add(sequence);
}
}
public void SendClassDeclaration(Int32 classToken, Int32 parentToken, (int Dynamic, int Statics) sizeOfClass, short classFlags, IList<ClassMember> members, int[] interfaceImplementationData)
{
List<FirmataCommandSequence> sequences = new();
if (members.Count == 0)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.ClassDeclarationEnd);
sequence.SendInt32(classToken);
sequence.SendInt32(parentToken);
if ((classFlags & 1) == 1)
{
sequence.SendInt14(sizeOfClass.Dynamic);
}
else
{
sequence.SendInt14(sizeOfClass.Dynamic >> 2);
}
sequence.SendInt14(sizeOfClass.Statics >> 2);
sequence.SendInt14(classFlags);
sequence.SendInt14(0);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
sequences.Add(sequence);
}
else
{
for (short member = 0; member < members.Count; member++)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(member == members.Count - 1 ? ExecutorCommand.ClassDeclarationEnd : ExecutorCommand.ClassDeclaration);
sequence.SendInt32(classToken);
sequence.SendInt32(parentToken);
if ((classFlags & 1) == 1)
{
sequence.SendInt14(sizeOfClass.Dynamic);
}
else
{
sequence.SendInt14(sizeOfClass.Dynamic >> 2);
}
sequence.SendInt14(sizeOfClass.Statics >> 2);
sequence.SendInt14(classFlags);
sequence.SendInt14(member);
sequence.WriteByte((byte)members[member].VariableType);
sequence.SendInt32(members[member].Token);
if (members[member].VariableType != VariableKind.Method)
{
sequence.SendInt14(members[member].SizeOfField);
}
else
{
var tokenList = members[member].BaseTokens;
if (tokenList != null)
{
foreach (int bdt in tokenList)
{
sequence.SendInt32(bdt);
}
}
}
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
sequences.Add(sequence);
}
}
for (int idx = 0; idx < interfaceImplementationData.Length;)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.Interfaces);
sequence.SendInt32(classToken);
int remaining = interfaceImplementationData.Length - idx;
if (remaining > 8)
{
remaining = 8;
}
for (int i = idx; i < idx + remaining; i++)
{
sequence.SendInt32(interfaceImplementationData[i]);
}
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
idx = idx + remaining;
sequences.Add(sequence);
}
SendCommandsAndWait(sequences, ProgrammingTimeout, out var error);
if (error != CommandError.None)
{
throw new TaskSchedulerException($"Command sequence returned error {error}");
}
}
public void PrepareStringLoad(int constantSize, int stringSize)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.SetConstantMemorySize);
sequence.SendInt32(constantSize);
sequence.SendInt32(stringSize);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
public void SendConstant(Int32 constantToken, byte[] data)
{
const int packetSize = 28;
for (int offset = 0; offset < data.Length; offset += packetSize)
{
int remaining = Math.Min(packetSize, data.Length - offset);
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.ConstantData);
sequence.SendInt32(constantToken);
sequence.SendInt32(data.Length);
sequence.SendInt32(offset);
var encoded = Encoder7Bit.Encode(data, offset, remaining);
sequence.Write(encoded, 0, encoded.Length);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
}
public void SendSpecialTypeList(List<int> tokens)
{
SendSpecialTokenList(tokens, ExecutorCommand.SpecialTokenList);
}
public void SendSpecialTokenList(List<int> tokens, ExecutorCommand command)
{
const int packetSize = 5;
for (int offset = 0; offset < tokens.Count; offset += packetSize)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(command);
int remaining = Math.Min(packetSize, tokens.Count - offset);
sequence.SendInt32(tokens.Count);
sequence.SendInt32(offset);
for (int i = 0; i < remaining; i++)
{
sequence.SendInt32(tokens[i + offset]);
}
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
}
public void SendGlobalMetadata(UInt32 staticRootVectorSize)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.GlobalMetadata);
sequence.SendInt32(4);
sequence.SendUInt32(staticRootVectorSize);
sequence.WriteByte(FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
public void ClearFlash()
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.EraseFlash);
sequence.WriteByte((byte)(1));
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
public void SendIlResetCommand(bool force)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.ResetExecutor);
sequence.WriteByte((byte)(force ? 1 : 0));
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
try
{
WaitAndHandleIlCommand(sequence);
}
catch (TaskSchedulerException x)
{
Logger.LogWarning(x, "Terminated running task (ignored)");
}
}
public void SendKillTask(int methodToken)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.KillTask);
sequence.SendInt32(methodToken);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
public void CopyToFlash()
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.CopyToFlash);
sequence.WriteByte(0);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
public void WriteFlashHeader(int dataVersion, int hashCode, int startupToken, CodeStartupFlags startupFlags)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.WriteFlashHeader);
sequence.SendInt32(dataVersion);
sequence.SendInt32(hashCode);
sequence.SendInt32(startupToken);
sequence.SendInt32((int)startupFlags);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
WaitAndHandleIlCommand(sequence);
}
public bool IsMatchingFirmwareLoaded(int dataVersion, int hashCode)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.CheckFlashVersion);
sequence.SendInt32(dataVersion);
sequence.SendInt32(hashCode);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
var data = SendCommandAndWait(sequence, ProgrammingTimeout, out CommandError _);
if (data.Length > 0 && data[0] == SchedulerData)
{
if (data.Length == 5 && data[1] == (byte)ExecutorCommand.Ack)
{
return true;
}
if (data.Length == 5 && data[1] == (byte)ExecutorCommand.Nack)
{
return false;
}
}
throw new InvalidOperationException("Unexpected command reply");
}
public void QueryCapabilities()
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.QueryHardware);
sequence.SendUInt32(0);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
SendCommand(sequence);
}
public void SendDebuggerCommand(DebuggerCommand command)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.DebuggerCommand);
sequence.SendInt32((int)command);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
SendCommandAndWait(sequence, TimeSpan.FromMinutes(1));
}
public void SendDebuggerCommand(DebuggerCommand command, Int32 arg1, Int32 arg2 = 0)
{
FirmataIlCommandSequence sequence = new FirmataIlCommandSequence(ExecutorCommand.DebuggerCommand);
sequence.SendInt32((int)command);
sequence.SendInt32(arg1);
sequence.SendInt32(arg2);
sequence.WriteByte((byte)FirmataCommandSequence.EndSysex);
SendCommandAndWait(sequence, TimeSpan.FromMinutes(1));
}
}
}