1 // 2 // Copyright (c) 2010-2024 Antmicro 3 // 4 // This file is licensed under the MIT License. 5 // Full license text is available in 'licenses/MIT.txt'. 6 // 7 using System; 8 using System.Collections.Generic; 9 using System.Linq; 10 using System.Reflection; 11 using System.Text; 12 using Antmicro.Migrant; 13 using Antmicro.Migrant.Hooks; 14 using Antmicro.Renode.Core; 15 using Antmicro.Renode.Logging; 16 using Antmicro.Renode.Peripherals.UART; 17 using Antmicro.Renode.Time; 18 using Antmicro.Renode.Utilities; 19 20 namespace Antmicro.Renode.Peripherals.Network 21 { 22 public abstract partial class AtCommandModem : IUART 23 { AtCommandModem(IMachine machine)24 public AtCommandModem(IMachine machine) 25 { 26 this.machine = machine; 27 commandOverrides = new Dictionary<string, CommandOverride>(); 28 Init(); 29 Reset(); 30 } 31 Reset()32 public virtual void Reset() 33 { 34 Enabled = true; 35 PassthroughMode = false; 36 echoEnabled = EchoEnabledAtReset; 37 lineBuffer = new StringBuilder(); 38 } 39 WriteChar(byte value)40 public virtual void WriteChar(byte value) 41 { 42 if(!Enabled) 43 { 44 this.Log(LogLevel.Warning, "Modem is not enabled, ignoring incoming byte 0x{0:x2}", value); 45 return; 46 } 47 48 if(PassthroughMode) 49 { 50 PassthroughWriteChar(value); 51 return; 52 } 53 54 // The Zephyr bg9x driver sends a ^Z after sending fixed-length binary data. 55 // Ignore it and similar things done by other drivers. 56 if(value == ControlZ || value == Escape) 57 { 58 this.Log(LogLevel.Debug, "Ignoring byte 0x{0:x2} in AT mode", value); 59 return; 60 } 61 62 var charValue = (char)value; 63 64 // Echo is only active in AT command mode 65 if(echoEnabled) 66 { 67 SendChar(charValue); 68 } 69 70 // Ignore newline characters 71 // Commands are supposed to end with \r, some software sends \r\n 72 if(charValue == '\n') 73 { 74 // do nothing 75 } 76 // \r indicates that the command is complete 77 else if(charValue == '\r') 78 { 79 var command = lineBuffer.ToString(); 80 lineBuffer.Clear(); 81 if(command == "") 82 { 83 this.Log(LogLevel.Debug, "Ignoring empty command"); 84 return; 85 } 86 this.Log(LogLevel.Debug, "Command received: '{0}'", command); 87 var response = HandleCommand(command); 88 if(response != null) 89 { 90 if(CommandResponseDelayMilliseconds.HasValue) 91 { 92 machine.ScheduleAction(TimeInterval.FromMilliseconds(CommandResponseDelayMilliseconds.Value), _ => SendResponse(response)); 93 } 94 else 95 { 96 SendResponse(response); 97 } 98 } 99 } 100 else 101 { 102 lineBuffer.Append(charValue); 103 } 104 } 105 OverrideResponseForCommand(string command, string status, string parameters = R, bool oneShot = false)106 public void OverrideResponseForCommand(string command, string status, string parameters = "", bool oneShot = false) 107 { 108 var splitParams = string.IsNullOrEmpty(parameters) ? new string[] { } : parameters.Split('\n'); 109 commandOverrides[command] = new CommandOverride(new Response(status, splitParams), oneShot); 110 } 111 ClearOverrides()112 public void ClearOverrides() 113 { 114 commandOverrides.Clear(); 115 } 116 PassthroughWriteChar(byte value)117 public abstract void PassthroughWriteChar(byte value); 118 119 public bool EchoEnabledAtReset { get; set; } = true; 120 121 public bool EchoEnabled => echoEnabled; 122 123 public virtual uint BaudRate { get; protected set; } = 115200; 124 125 public Bits StopBits => Bits.One; 126 127 public Parity ParityBit => Parity.None; 128 129 public ulong? CommandResponseDelayMilliseconds { get; set; } 130 131 public uint? TransferBandwidth { get; set; } 132 133 [field: Transient] 134 public event Action<byte> CharReceived; 135 136 protected static readonly Encoding StringEncoding = Encoding.UTF8; 137 protected static readonly byte[] CrLfBytes = StringEncoding.GetBytes(CrLf); 138 protected static readonly Response Ok = new Response(OkMessage); 139 protected static readonly Response Error = new Response(ErrorMessage); 140 SendChar(char ch)141 protected void SendChar(char ch) 142 { 143 var charReceived = CharReceived; 144 if(charReceived == null) 145 { 146 this.Log(LogLevel.Warning, "Wanted to send char '{0}' but nothing is connected to {1}", 147 ch, nameof(CharReceived)); 148 return; 149 } 150 151 lock(uartWriteLock) 152 { 153 CharReceived?.Invoke((byte)ch); 154 } 155 } 156 SendBytes(byte[] bytes)157 protected void SendBytes(byte[] bytes) 158 { 159 var charReceived = CharReceived; 160 if(charReceived == null) 161 { 162 this.Log(LogLevel.Warning, "Wanted to send bytes '{0}' but nothing is connected to {1}", 163 Misc.PrettyPrintCollectionHex(bytes), nameof(CharReceived)); 164 return; 165 } 166 167 if(TransferBandwidth == null) 168 { 169 lock(uartWriteLock) 170 { 171 foreach(var b in bytes) 172 { 173 charReceived(b); 174 } 175 } 176 } 177 else 178 { 179 var currentByte = 0; 180 IManagedThread thread = null; 181 thread = machine.ObtainManagedThread(() => 182 { 183 lock(uartWriteLock) 184 { 185 var b = bytes[currentByte]; 186 currentByte++; 187 charReceived(b); 188 } 189 190 if(currentByte == bytes.Length) 191 { 192 thread.Stop(); 193 } 194 }, TransferBandwidth.Value); 195 thread.Start(); 196 } 197 } 198 SendString(string str)199 protected void SendString(string str) 200 { 201 var strWithNewlines = str.SurroundWith(CrLf); 202 var stringForDisplay = str.SurroundWith(CrLfSymbol); 203 this.Log(LogLevel.Debug, "Sending string: '{0}'", stringForDisplay); 204 SendBytes(StringEncoding.GetBytes(strWithNewlines)); 205 } 206 SendResponse(Response response)207 protected void SendResponse(Response response) 208 { 209 this.Log(LogLevel.Debug, "Sending response: {0}", response); 210 SendBytes(response.GetBytes()); 211 } 212 213 // This method is intended for cases where a modem driver sends a command 214 // and expects a URC (Unsolicited Result Code) after some time, and a URC 215 // sent immediately after the normal (solicited) command response and status 216 // string (like OK) is not accepted. In this case we can use something like 217 // `ExecuteWithDelay(() => SendResponse(...))` in the command callback. ExecuteWithDelay(Action action, ulong milliseconds = 50)218 protected void ExecuteWithDelay(Action action, ulong milliseconds = 50) 219 { 220 machine.ScheduleAction(TimeInterval.FromMilliseconds(milliseconds), _ => action()); 221 } 222 223 // AT - Do Nothing, Successfully (used to detect modem presence) 224 [AtCommand("AT")] At()225 protected virtual Response At() 226 { 227 return Ok; 228 } 229 230 // ATE - Enable/Disable Echo 231 [AtCommand("ATE")] Ate(int value = 0)232 protected virtual Response Ate(int value = 0) 233 { 234 echoEnabled = value != 0; 235 return Ok; 236 } 237 238 // ATH - Hook Status 239 // This is a stub implementation - models should override it if they need to do 240 // something special on hangup/pickup 241 [AtCommand("ATH")] Ath(int offHook = 0)242 protected virtual Response Ath(int offHook = 0) 243 { 244 return Ok; 245 } 246 247 // AT&W - Save Current Parameters to NVRAM 248 [AtCommand("AT&W")] Atw()249 protected virtual Response Atw() 250 { 251 EchoEnabledAtReset = echoEnabled; 252 return Ok; 253 } 254 255 protected bool Enabled { get; set; } 256 protected bool PassthroughMode { get; set; } 257 258 protected const string OkMessage = "OK"; 259 protected const string ErrorMessage = "ERROR"; 260 protected const string CrLf = "\r\n"; 261 protected const string CrLfSymbol = "⏎"; 262 protected const byte ControlZ = 26; 263 protected const byte Escape = 27; 264 265 protected readonly IMachine machine; 266 DefaultTestCommand()267 private Response DefaultTestCommand() 268 { 269 return Ok; 270 } 271 TryFindCommandMethod(ParsedCommand command, out MethodInfo method)272 private bool TryFindCommandMethod(ParsedCommand command, out MethodInfo method) 273 { 274 if(!commandMethods.TryGetValue(command.Command, out var types)) 275 { 276 method = null; 277 return false; 278 } 279 280 // Look for an implementation method for this combination of command and type 281 if(!types.TryGetValue(command.Type, out method)) 282 { 283 // If we are looking for a test command and there is no implementation, return the default one 284 // We know the command itself exists because it has an entry in commandMethods (it just has no 285 // explicitly-implemented Test behavior) 286 if(command.Type == CommandType.Test) 287 { 288 method = defaultTestCommandMethodInfo; 289 } 290 } 291 292 return method != null; 293 } 294 HandleCommand(string command)295 protected virtual Response HandleCommand(string command) 296 { 297 if(commandOverrides.TryGetValue(command, out var overrideResp)) 298 { 299 this.Log(LogLevel.Debug, "Using overridden response for '{0}'{1}", 300 command, overrideResp.oneShot ? " once" : ""); 301 if(overrideResp.oneShot) 302 { 303 commandOverrides.Remove(command); 304 } 305 return overrideResp.response; 306 } 307 308 if(!ParsedCommand.TryParse(command, out var parsed)) 309 { 310 this.Log(LogLevel.Warning, "Failed to parse command '{0}'", command); 311 return Error; 312 } 313 314 if(!TryFindCommandMethod(parsed, out var handler)) 315 { 316 this.Log(LogLevel.Warning, "Unhandled command '{0}'", command); 317 return Error; 318 } 319 320 var parameters = handler.GetParameters(); 321 var argumentsString = parsed.Arguments; 322 object[] arguments; 323 try 324 { 325 arguments = ParseArguments(argumentsString, parameters); 326 } 327 catch(ArgumentException e) 328 { 329 this.Log(LogLevel.Warning, "Failed to parse arguments: {0}", e.Message); 330 // An incorrectly-formatted argument always leads to a plain ERROR 331 return Error; 332 } 333 // Pad arguments to the number of parameters with Type.Missing to use defaults 334 if(arguments.Length < parameters.Length) 335 { 336 arguments = arguments 337 .Concat(Enumerable.Repeat(Type.Missing, parameters.Length - arguments.Length)) 338 .ToArray(); 339 } 340 341 try 342 { 343 return (Response)handler.Invoke(this, arguments); 344 } 345 catch(ArgumentException) 346 { 347 var parameterTypesString = string.Join(", ", parameters.Select(t => t.ParameterType.FullName)); 348 var argumentTypesString = string.Join(", ", arguments.Select(a => a?.GetType()?.FullName ?? "(null)")); 349 this.Log(LogLevel.Error, "Argument type mismatch in command '{0}'. Got types [{1}], expected [{2}]", 350 command, argumentTypesString, parameterTypesString); 351 return Error; 352 } 353 } 354 GetCommandMethods()355 private Dictionary<string, Dictionary<CommandType, MethodInfo>> GetCommandMethods() 356 { 357 // We want to get a hierarchy like 358 // AT+IPR 359 // | - Read -> IprRead 360 // | - Write -> IprWrite 361 // but with the possibility of having for example 362 // AT+ABC 363 // | - Read -> AbcReadWrite 364 // | - Write -> AbcReadWrite 365 // which would come from AbcReadWrite being annotated with [AtCommand("AT+ABC", Read|Write)] 366 // so we first flatten the types and then group by the command name. 367 // Also, if unrelated (i.e. not in an override hierarchy) methods in a base class 368 // and a subclass are both annotated with [AtCommand("AT+ABC", Read)], we want to use the 369 // implementation from the most derived class. This is done using DistinctBy(type), in order to turn 370 // AT+ABC 371 // | - Read -> AbcReadDerivedDerived (in subclass C <: B) 372 // | - Read -> AbcReadDerived (in subclass B <: A) 373 // | - Read -> AbcRead (in base class A) 374 // into 375 // AT+ABC 376 // | - Read -> AbcReadDerivedDerived (in subclass C <: B) 377 // This relies on the fact that GetMethodsWithAttribute returns methods sorted by 378 // the depth of their declaring class in the inheritance hierarchy, deepest first. 379 380 // We don't inherit the [AtCommand] attribute in order to allow "hiding" commands 381 // in subclasses by overriding them and not marking them with [AtCommand] 382 var commandMethods = this.GetType().GetMethodsWithAttribute<AtCommandAttribute>(inheritAttribute: false) 383 .SelectMany(ma => ma.Attribute.Types, (ma, type) => new { ma.Attribute.Command, type, ma.Method }) 384 .GroupBy(ma => ma.Command) 385 .ToDictionary(g => g.Key, g => g.DistinctBy(h => h.type).ToDictionary(ma => ma.type, ma => ma.Method)); 386 387 // Verify that all command methods return Response 388 foreach(var typeMethod in commandMethods.Values.SelectMany(m => m)) 389 { 390 var type = typeMethod.Key; 391 var method = typeMethod.Value; 392 if(method.ReturnType != typeof(Response)) 393 { 394 throw new InvalidOperationException($"Command method {method.Name} ({type}) does not return {nameof(Response)}"); 395 } 396 } 397 398 return commandMethods; 399 } 400 401 [PostDeserialization] Init()402 private void Init() 403 { 404 commandMethods = GetCommandMethods(); 405 argumentParsers = GetArgumentParsers(); 406 defaultTestCommandMethodInfo = typeof(AtCommandModem).GetMethod(nameof(DefaultTestCommand), BindingFlags.Instance | BindingFlags.NonPublic); 407 } 408 409 private bool echoEnabled; 410 private StringBuilder lineBuffer; 411 412 [Transient] 413 private Dictionary<string, Dictionary<CommandType, MethodInfo>> commandMethods; 414 [Transient] 415 private Dictionary<Type, Func<string, object>> argumentParsers; 416 [Transient] 417 private MethodInfo defaultTestCommandMethodInfo; 418 419 private readonly object uartWriteLock = new object(); 420 private readonly Dictionary<string, CommandOverride> commandOverrides; 421 422 [AttributeUsage(AttributeTargets.Method)] 423 protected class AtCommandAttribute : Attribute 424 { AtCommandAttribute(string command, CommandType type = CommandType.Execution)425 public AtCommandAttribute(string command, CommandType type = CommandType.Execution) 426 { 427 Command = command; 428 Type = type; 429 } 430 431 public string Command { get; } 432 433 public CommandType Type { get; } 434 435 public IEnumerable<CommandType> Types 436 { 437 get => Enum.GetValues(typeof(CommandType)).Cast<CommandType>().Where(t => (Type & t) != 0); 438 } 439 } 440 441 protected class Response 442 { Response(string status, params string[] parameters)443 public Response(string status, params string[] parameters) : this(status, "", parameters, null) 444 { 445 } 446 WithParameters(params string[] parameters)447 public Response WithParameters(params string[] parameters) 448 { 449 return new Response(Status, Trailer, parameters, null); 450 } 451 WithParameters(byte[] parameters)452 public Response WithParameters(byte[] parameters) 453 { 454 return new Response(Status, Trailer, null, parameters); 455 } 456 WithTrailer(string trailer)457 public Response WithTrailer(string trailer) 458 { 459 return new Response(Status, trailer, Parameters, BinaryBody); 460 } 461 ToString()462 public override string ToString() 463 { 464 string bodyRepresentation; 465 if(Parameters != null) 466 { 467 bodyRepresentation = string.Join(", ", Parameters.Select(p => p.SurroundWith("'"))); 468 bodyRepresentation = $"[{bodyRepresentation}]"; 469 } 470 else 471 { 472 bodyRepresentation = Misc.PrettyPrintCollectionHex(BinaryBody); 473 } 474 475 var result = $"Body: {bodyRepresentation}; Status: {Status}"; 476 if(Trailer.Length > 0) 477 { 478 result += $"; Trailer: {Trailer}"; 479 } 480 return result; 481 } 482 483 // Get the binary representation formatted as a modem would send it GetBytes()484 public byte[] GetBytes() 485 { 486 return bytes; 487 } 488 489 // The status line is usually "OK" or "ERROR" 490 public string Status { get; } 491 // The parameters are the actual useful data returned by a command, for example 492 // the current value of a parameter in the case of a Read command 493 public string[] Parameters { get; } 494 // Alternatively to parameters, a binary body can be provided. It is placed where 495 // the parameters would be and surrounded with CrLf 496 public byte[] BinaryBody { get; } 497 // The trailer can be thought of as an immediately-sent URC: it is sent after 498 // the status line. 499 public string Trailer { get; } 500 Response(string status, string trailer, string[] parameters, byte[] binaryBody)501 private Response(string status, string trailer, string[] parameters, byte[] binaryBody) 502 { 503 // We want exactly one of Parameters or BinaryBody. If neither is provided or both 504 // are, throw an exception. 505 if((parameters != null) == (binaryBody != null)) 506 { 507 throw new InvalidOperationException("Either parameters xor a binary body must be provided"); 508 } 509 510 Status = status; 511 Trailer = trailer; 512 Parameters = parameters; 513 BinaryBody = binaryBody; 514 515 byte[] bodyContent; 516 if(Parameters != null) 517 { 518 var parametersContent = string.Join(CrLf, Parameters); 519 bodyContent = StringEncoding.GetBytes(parametersContent); 520 } 521 else 522 { 523 bodyContent = BinaryBody; 524 } 525 526 var bodyBytes = CrLfBytes.Concat(bodyContent).Concat(CrLfBytes); 527 var statusPart = Status.SurroundWith(CrLf); 528 var statusBytes = StringEncoding.GetBytes(statusPart); 529 var trailerBytes = StringEncoding.GetBytes(Trailer.SurroundWith(CrLf)); 530 bytes = bodyBytes.Concat(statusBytes).Concat(trailerBytes).ToArray(); 531 } 532 533 private readonly byte[] bytes; 534 } 535 536 [Flags] 537 protected enum CommandType 538 { 539 Test = 1 << 0, 540 Read = 1 << 1, 541 Write = 1 << 2, 542 Execution = 1 << 3, 543 } 544 545 private struct CommandOverride 546 { CommandOverrideAntmicro.Renode.Peripherals.Network.AtCommandModem.CommandOverride547 public CommandOverride(Response response, bool oneShot) 548 { 549 this.response = response; 550 this.oneShot = oneShot; 551 } 552 553 public readonly Response response; 554 public readonly bool oneShot; 555 } 556 } 557 } 558