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