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.IO;
10 using System.Linq;
11 using System.Runtime.CompilerServices;
12 using Antmicro.Renode.Core;
13 using Antmicro.Renode.Logging;
14 using Antmicro.Renode.Peripherals.Timers;
15 using Antmicro.Renode.Time;
16 using Antmicro.Renode.Utilities;
17 
18 namespace Antmicro.Renode.Peripherals.Network
19 {
20     public abstract class QuectelModem : AtCommandModem, IGPIOReceiver, INumberedGPIOOutput
21     {
QuectelModem(IMachine machine, string imeiNumber = DefaultImeiNumber, string softwareVersionNumber = DefaultSoftwareVersionNumber, string serialNumber = DefaultSerialNumber)22         public QuectelModem(IMachine machine, string imeiNumber = DefaultImeiNumber,
23             string softwareVersionNumber = DefaultSoftwareVersionNumber,
24             string serialNumber = DefaultSerialNumber) : base(machine)
25         {
26             this.imeiNumber = imeiNumber;
27             this.softwareVersionNumber = softwareVersionNumber;
28             this.serialNumber = serialNumber;
29             deepsleepTimer = new LimitTimer(machine.ClockSource, 1, this, "T3324", direction: Direction.Ascending, workMode: WorkMode.OneShot, eventEnabled: true);
30             deepsleepTimer.LimitReached += () =>
31             {
32                 this.Log(LogLevel.Noisy, "T3324 timer timeout");
33                 SendSignalingConnectionStatus(false);
34                 EnterDeepsleep();
35             };
36             Connections = new Dictionary<int, IGPIO>
37             {
38                 {0, vddExt},
39                 {1, netLight},
40             };
41 
42             Reset();
43         }
44 
Reset()45         public override void Reset()
46         {
47             base.Reset();
48             mtResultCodeMode = MobileTerminationResultCodeMode.Disabled;
49             dataBuffer = new MemoryStream();
50             dataBytesRemaining = null;
51             dataCallback = null;
52             for(int i = 0; i < sockets.Length; i++)
53             {
54                 sockets[i]?.Dispose();
55                 sockets[i] = null;
56             }
57             inReset = false;
58             signalingConnectionActive = false;
59             powerSavingModeActive = false;
60             echoInDataMode = false;
61             Enabled = false;
62             vddExt.Unset();
63         }
64 
PassthroughWriteChar(byte value)65         public override void PassthroughWriteChar(byte value)
66         {
67             if(echoInDataMode)
68             {
69                 SendChar((char)value);
70             }
71 
72             // Variable-length data mode - ^Z confirms, Esc cancels
73             if(dataBytesRemaining == null)
74             {
75                 if(value == ControlZ)
76                 {
77                     ExitDataMode(true);
78                 }
79                 else if(value == Escape)
80                 {
81                     ExitDataMode(false);
82                     // Send OK manually because we don't call the data callback
83                     SendResponse(Ok);
84                 }
85                 else
86                 {
87                     dataBuffer.WriteByte(value);
88                 }
89             }
90             else // Fixed-length data mode, no special character handling
91             {
92                 dataBuffer.WriteByte(value);
93                 if(--dataBytesRemaining == 0)
94                 {
95                     ExitDataMode(true);
96                 }
97             }
98         }
99 
OnGPIO(int number, bool value)100         public void OnGPIO(int number, bool value)
101         {
102             this.Log(LogLevel.Debug, "GPIO {0} -> {1}", (GPIOInput)number, value);
103             switch((GPIOInput)number)
104             {
105                 case GPIOInput.Power:
106                     // Pulling down the Power Key pin means to turn on the modem.
107                     // The modem cannot be turned on while it is in reset.
108                     if(!value && !inReset)
109                     {
110                         EnableModem();
111                     }
112                     break;
113                 case GPIOInput.Reset:
114                     // We assume the reset completes immediately, and the modem is held in reset
115                     // as long as the reset pin is low.
116                     inReset = !value;
117                     if(inReset)
118                     {
119                         Reset();
120                     }
121                     else
122                     {
123                         EnableModem();
124                     }
125                     break;
126                 case GPIOInput.PsmEint:
127                     // If we are sleeping (and not held in reset) we should wake up on a falling edge.
128                     if(!Enabled && !inReset && !value)
129                     {
130                         EnableModem();
131                     }
132                     break;
133                 default:
134                     this.Log(LogLevel.Error, "Got GPIO state {0} for unknown input {1}", value, number);
135                     break;
136             }
137         }
138 
139         public IReadOnlyDictionary<int, IGPIO> Connections { get; }
140 
141         public string IccidNumber { get; set; } = "00000000000000000000";
142         public string TrackingAreaCode { get; set; } = "0000";
143         public string NetworkLocationArea { get; set; } = "00000";
144         public string CellId { get; set; } = "0000";
145         public int CellPhysicalId { get; set; } = 0;
146         public int CellEarfcn { get; set; } = 0;
147         public int CellEarfcnOffset { get; set; } = 0;
148         public string ActiveTime { get; set; } = "00100100";
149         public string PeriodicTau { get; set; } = "01000111";
150         public string NetworkIp { get; set; } = "0.0.0.0";
151         public int BitErrorRate { get; set; } = 0;
152         public int Rsrp { get; set; } = 0;
153         public decimal Rsrq { get; set; } = 0m;
154         public int Rssi { get; set; } = 0;
155         public int Sinr { get; set; } = 0;
156         public int Rscp { get; set; } = 0;
157         public decimal Ecno { get; set; } = 0m;
158         public int Band { get; set; } = 0;
159         public int EnhancedCoverageLevel { get; set; } = 0;
160         public int TransmitPower { get; set; } = 0;
161         public NetworkRegistrationStates NetworkRegistrationState { get; set; } = NetworkRegistrationStates.NotRegisteredNotSearching;
162         public PublicLandMobileNetworkSearchingState ModemPLMNState { get; set; } = PublicLandMobileNetworkSearchingState.Selected;
163         public int PLMNSearchTime { get; set; } = 0;
164         public bool DeepsleepOnRellock { get; set; } = false;
165         // The delay before sending the +CSCON URC in virtual milliseconds
166         public ulong CsconDelay { get; set; } = 500;
167         // The delay before sending the +CEREG URC in virtual milliseconds
168         public ulong CeregDelay { get; set; } = 500;
169         // The delay before sending the +IP URC in virtual milliseconds
170         public ulong IpDelay { get; set; } = 1000;
171         // These timers should in theory be automatically updated when we enter deep sleep,
172         // receive or transmit something. This is a basic implementation that only supports
173         // setting them manually.
174         public decimal SleepDuration { get; set; } = 0m;
175         public decimal RxTime { get; set; } = 0m;
176         public decimal TxTime { get; set; } = 0m;
177         // These parameters should be set according to expectations of network behavior.
178         public ulong ClosePortInactivityDisconnectDelay { get; set; }
179         public ulong SendDataInactivityDisconnectDelay { get; set; }
180         public ulong SendDataActivityConnectDelay { get; set; } = 550;
181         public bool SendCSCONOnChangeOnly { get; set; } = true;
182 
183         public int SignalStrength => (int?)Misc.RemapNumber(Rssi, -113m, -51m, 0, 31) ?? 0;
184         public int ActiveTimeSeconds => ConvertEncodedStringToSeconds(ActiveTime, ModemTimerType.ActiveTimeT3324);
185         public int PeriodicTauSeconds => ConvertEncodedStringToSeconds(PeriodicTau, ModemTimerType.PeriodicTauT3412);
186 
HandleCommand(string command)187         protected override Response HandleCommand(string command)
188         {
189             if(deepsleepTimer.Enabled)
190             {
191                 // Restart deep sleep timer after receiving any AT command.
192                 deepsleepTimer.Value = 0;
193                 this.Log(LogLevel.Noisy, "'{0}' command received: Defer deepsleep by {1} seconds", command, ActiveTimeSeconds);
194             }
195             return base.HandleCommand(command);
196         }
197 
198         // ATI - Display Product Identification Information
199         [AtCommand("ATI")]
Ati()200         protected virtual Response Ati() => Ok.WithParameters(Vendor, ModelName, Revision);
201 
202         // AT&W - Save Current Parameters to NVRAM
203         [AtCommand("AT&W")]
Atw()204         protected override Response Atw()
205         {
206             var d = 0UL;
207             ExecuteWithDelay(() => SendSignalingConnectionStatus(true), d += CsconDelay);
208             ExecuteWithDelay(() => SendString($"+IP: {NetworkIp}"), d += IpDelay); // IP URC means successfully registered
209             return base.Atw();
210         }
211 
212         // CEDRXS - eDRX Setting
213         [AtCommand("AT+CEDRXS", CommandType.Write)]
Cedrxs(int mode = 1, int accessTechnology = 5, string requestedEdrxValue = R)214         protected virtual Response Cedrxs(int mode = 1, int accessTechnology = 5, string requestedEdrxValue = "0010")
215         {
216             return Ok; // stub
217         }
218 
CeregContent(bool urc = false, string prefix = R)219         protected virtual string CeregContent(bool urc = false, string prefix = "+CEREG")
220         {
221             var fragments = new List<string>();
222             // The URC form of CEREG does not report the type, the command response form does.
223             if(!urc)
224             {
225                 fragments.Add(((int)networkRegistrationUrcType).ToString());
226             }
227 
228             if(networkRegistrationUrcType >= NetworkRegistrationUrcType.StatOnly)
229             {
230                 fragments.Add(((int)NetworkRegistrationState).ToString());
231             }
232             if(networkRegistrationUrcType >= NetworkRegistrationUrcType.StatLocation)
233             {
234                 fragments.Add(TrackingAreaCode.SurroundWith("\""));
235                 fragments.Add(CellId.PadLeft(8, '0').SurroundWith("\""));
236                 fragments.Add("9"); // access technology: E-UTRAN (NB-S1 mode)
237             }
238             if(networkRegistrationUrcType >= NetworkRegistrationUrcType.StatLocationEmmCause)
239             {
240                 if(networkRegistrationUrcType == NetworkRegistrationUrcType.StatLocationPsm)
241                 {
242                     fragments.Add("");
243                     fragments.Add("");
244                 }
245                 else
246                 {
247                     fragments.Add("0"); // reject cause type
248                     fragments.Add("0"); // reject cause
249                 }
250             }
251             if(networkRegistrationUrcType == NetworkRegistrationUrcType.StatLocationPsm ||
252                 networkRegistrationUrcType == NetworkRegistrationUrcType.StatLocationEmmCausePsm)
253             {
254                 fragments.Add(ActiveTime.SurroundWith("\""));
255                 fragments.Add(PeriodicTau.SurroundWith("\""));
256             }
257 
258             return prefix + ": " + string.Join(",", fragments);
259         }
260 
CregContent(bool urc = false)261         protected virtual string CregContent(bool urc = false)
262         {
263             // Signature of +CREG message is the same as of +CEREG.
264             return CeregContent(urc, "+CREG");
265         }
266 
267         // CEREG - EPS Network Registration Status
268         [AtCommand("AT+CEREG", CommandType.Write)]
CeregWrite(NetworkRegistrationUrcType type)269         protected virtual Response CeregWrite(NetworkRegistrationUrcType type)
270         {
271             networkRegistrationUrcType = type;
272             // Queue a CEREG URC in response to the write
273             ExecuteWithDelay(() => SendString(CeregContent(true)), CeregDelay);
274             return Ok; // stub, should disable or enable network registration URC
275         }
276 
277         [AtCommand("AT+CEREG", CommandType.Read)]
CeregRead()278         protected virtual Response CeregRead() => Ok.WithParameters(CeregContent());
279 
280         // CREG - Network Registration
281         [AtCommand("AT+CREG", CommandType.Write)]
CregWrite(NetworkRegistrationUrcType type)282         protected virtual Response CregWrite(NetworkRegistrationUrcType type)
283         {
284             networkRegistrationUrcType = type;
285             // Queue a CREG URC in response to the write, use the same delay as for CEREG URC
286             ExecuteWithDelay(() => SendString(CregContent(true)), CeregDelay);
287             return Ok; // stub, should disable or enable network registration URC
288         }
289 
290         [AtCommand("AT+CREG", CommandType.Read)]
CregRead()291         protected virtual Response CregRead() => Ok.WithParameters(CregContent());
292 
293         // CESQ - Extended Signal Quality
294         [AtCommand("AT+CESQ")]
Cesq()295         protected virtual Response Cesq()
296         {
297             var rscp = (int?)Misc.RemapNumber(Rscp, -120m, -25m, 0, 96) ?? 255;
298             var ecno = (int?)Misc.RemapNumber(Ecno, -24m, 0m, 0, 49) ?? 255;
299             var rsrq = (int?)Misc.RemapNumber(Rsrq, -19.5m, -3m, 0, 34) ?? 255;
300             var rsrp = (int?)Misc.RemapNumber(Rsrp, -140m, -44m, 0, 97) ?? 255;
301             return Ok.WithParameters($"+CESQ: {SignalStrength},{BitErrorRate},{rscp},{ecno},{rsrq},{rsrp}");
302         }
303 
304         // CFUN - Set UE Functionality
305         [AtCommand("AT+CFUN", CommandType.Write)]
Cfun(FunctionalityLevel functionalityLevel = FunctionalityLevel.Full, int reset = 0)306         protected virtual Response Cfun(FunctionalityLevel functionalityLevel = FunctionalityLevel.Full, int reset = 0)
307         {
308             // Reset option isn't taken into account yet, so the new functionality level
309             // is always activated immediately and remains valid after deep sleep wakeup.
310             this.functionalityLevel = functionalityLevel;
311 
312             if(signalingConnectionStatusReportingEnabled && functionalityLevel == FunctionalityLevel.Full)
313             {
314                 var d = 0UL;
315                 // We do both of the sends here after a delay to accomodate software
316                 // which might not expect the "instant" reply which would otherwise happen.
317                 ExecuteWithDelay(() => SendSignalingConnectionStatus(true), d += CsconDelay);
318                 ExecuteWithDelay(() => SendString($"+IP: {NetworkIp}"), d += IpDelay); // IP URC means successfully registered
319             }
320 
321             // Notify the DTE about the registration status to emulate the behavior
322             // of a real modem where it might change in response to the functionality
323             // level being changed.
324             ExecuteWithDelay(() => SendString(CeregContent(true)), CeregDelay);
325             return Ok; // stub
326         }
327 
328         [AtCommand("AT+CFUN", CommandType.Read)]
Cfun()329         protected virtual Response Cfun() => Ok.WithParameters($"+CFUN: {(int)functionalityLevel}");
330 
331         // CGDCONT - Define PDP Context
332         [AtCommand("AT+CGDCONT", CommandType.Read)]
Cgdcont()333         protected virtual Response Cgdcont() => Ok.WithParameters($"+CGDCONT: 1,\"IP\",\"{pdpContextApn}\",\"{NetworkIp}\",0,0,0,,,,0,,0,,0,0"); // stub
334 
335         // CPIN - Enter PIN
336         [AtCommand("AT+CPIN", CommandType.Read)]
Cpin()337         protected virtual Response Cpin()
338         {
339             return Ok.WithParameters("+CPIN: READY"); // stub
340         }
341 
342         // CPSMS - Power Saving Mode Setting
343         [AtCommand("AT+CPSMS", CommandType.Write)]
CpsmsWrite(int mode = 1, int reserved1 = 0, int reserved2 = 0, string requestedPeriodicTau = R, string requestedActiveTime = R)344         protected virtual Response CpsmsWrite(int mode = 1, int reserved1 = 0, int reserved2 = 0,
345             string requestedPeriodicTau = "", string requestedActiveTime = "")
346         {
347             return Ok; // stub
348         }
349 
CsconContent(bool urc = false)350         protected virtual string CsconContent(bool urc = false)
351         {
352             var reportingEnabled = signalingConnectionStatusReportingEnabled ? 1 : 0;
353             var active = signalingConnectionActive ? 1 : 0;
354 
355             if(urc)
356             {
357                 return $"+CSCON: {active}";
358             }
359             return $"+CSCON: {reportingEnabled},{active}";
360         }
361 
362         // CSCON - Signaling Connection Status
363         [AtCommand("AT+CSCON", CommandType.Read)]
Cscon()364         protected virtual Response Cscon()
365         {
366             return Ok.WithParameters(CsconContent());
367         }
368 
369         // CSCON - Signaling Connection Status
370         [AtCommand("AT+CSCON", CommandType.Write)]
Cscon(int enable = 0)371         protected virtual Response Cscon(int enable = 0)
372         {
373             signalingConnectionStatusReportingEnabled = enable == 1;
374             return Ok;
375         }
376 
377         // CSQ - Signal Quality Report
378         [AtCommand("AT+CSQ")]
Csq()379         protected virtual Response Csq() => Ok.WithParameters($"+CSQ: {SignalStrength},{BitErrorRate}");
380 
381         // CGACT - PDP Context Activate/Deactivate
382         [AtCommand("AT+CGACT", CommandType.Read)]
Cgact()383         protected virtual Response Cgact() => Ok.WithParameters("+CGACT: 1,1"); // stub
384 
385         // CGMI - Request Manufacturer Identification
386         [AtCommand("AT+CGMI")]
Cgmi()387         protected virtual Response Cgmi() => Ok.WithParameters(Vendor, ModelName, Revision);
388 
389         // CGMM - Request Model Identification
390         [AtCommand("AT+CGMM")]
Cgmm()391         protected virtual Response Cgmm() => Ok.WithParameters(ModelName);
392 
393         // CGMR - Request Manufacturer Revision
394         [AtCommand("AT+CGMR")]
Cgmr()395         protected virtual Response Cgmr() => Ok.WithParameters($"Revision: {ManufacturerRevision}");
396 
397         // CGPADDR - Show PDP Addresses
398         [AtCommand("AT+CGPADDR", CommandType.Read)]
Cgpaddr()399         protected virtual Response Cgpaddr() => Ok.WithParameters($"+CGPADDR: 1,{NetworkIp}"); // stub
400 
401         // CGSN - Request Product Serial Number
402         [AtCommand("AT+CGSN")]
Cgsn()403         protected virtual Response Cgsn() => CgsnWrite();
404 
405         [AtCommand("AT+CGSN", CommandType.Write)]
CgsnWrite(SerialNumberType serialNumberType = SerialNumberType.Device)406         protected virtual Response CgsnWrite(SerialNumberType serialNumberType = SerialNumberType.Device)
407         {
408             string result;
409             switch(serialNumberType)
410             {
411                 case SerialNumberType.Device:
412                     result = serialNumber;
413                     break;
414                 case SerialNumberType.Imei:
415                     result = imeiNumber;
416                     break;
417                 case SerialNumberType.ImeiSv:
418                     result = imeiNumber.Substring(0, imeiNumber.Length - 1) + softwareVersionNumber;
419                     break;
420                 case SerialNumberType.SoftwareVersionNumber:
421                     result = softwareVersionNumber;
422                     break;
423                 default:
424                     return Error; // unreachable
425             }
426             return Ok.WithParameters($"+CGSN: {result}");
427         }
428 
429         // CMEE - Report Mobile Termination Error
430         [AtCommand("AT+CMEE", CommandType.Write)]
Cmee(MobileTerminationResultCodeMode mode = MobileTerminationResultCodeMode.Disabled)431         protected virtual Response Cmee(MobileTerminationResultCodeMode mode = MobileTerminationResultCodeMode.Disabled)
432         {
433             mtResultCodeMode = mode;
434             this.Log(LogLevel.Debug, "CMEE result mode set to {0}", mode);
435             return Ok;
436         }
437 
438         [AtCommand("AT+CMEE", CommandType.Read)]
CmeeRead()439         protected virtual Response CmeeRead() => Ok.WithParameters($"+CMEE: {((int)mtResultCodeMode)}");
440 
441         // COPS - Operator Selection
442         [AtCommand("AT+COPS", CommandType.Write)]
CopsWrite(int mode = 0, int operFormat = 0, string oper = R, int accessTechnology = 9)443         protected virtual Response CopsWrite(int mode = 0, int operFormat = 0, string oper = "", int accessTechnology = 9)
444         {
445             return Ok; // stub
446         }
447 
448         [AtCommand("AT+COPS", CommandType.Read)]
CopsRead()449         protected virtual Response CopsRead() => Ok.WithParameters($"+COPS: 0,2,\"{NetworkLocationArea}\",9"); // stub
450 
451         // IPR - Set TE-TA Local Rate
452         [AtCommand("AT+IPR", CommandType.Write)]
IprWrite(uint rate = 115200)453         protected virtual Response IprWrite(uint rate = 115200)
454         {
455             BaudRate = rate;
456             return Ok;
457         }
458 
459         [AtCommand("AT+IPR", CommandType.Read)]
IprRead()460         protected virtual Response IprRead() => Ok.WithParameters($"+IPR: {BaudRate}");
461 
462         // QBAND - Get and Set Mobile Operation Band
463         [AtCommand("AT+QBAND", CommandType.Write)]
Qband(int numberOfBands, params int[] bands)464         protected virtual Response Qband(int numberOfBands, params int[] bands)
465         {
466             if(bands.Length != numberOfBands)
467             {
468                 return Error;
469             }
470             return Ok; // stub
471         }
472 
473         // QCCID - USIM Card Identification
474         [AtCommand("AT+QCCID")]
Qccid()475         protected virtual Response Qccid() => Ok.WithParameters($"+QCCID: {IccidNumber}");
476 
477         // CCLK - Set and Get Current Date and Time
478         [AtCommand("AT+CCLK", CommandType.Write)]
CclkWrite(string dateTime)479         protected virtual Response CclkWrite(string dateTime)
480         {
481             this.Log(LogLevel.Warning, "Ignoring attempt to set date/time to '{0}'", dateTime);
482             return Ok; // stub
483         }
484 
485         // QCFG - System Configuration
486         [AtCommand("AT+QCFG", CommandType.Write)]
Qcfg(string function, params int[] args)487         protected virtual Response Qcfg(string function, params int[] args)
488         {
489             this.Log(LogLevel.Warning, "Config value '{0}' set to {1}, not supported by this modem", function, string.Join(", ", args));
490             return Error;
491         }
492 
493         // QENG - Engineering Mode
494         [AtCommand("AT+QENG", CommandType.Write)]
Qeng(int mode)495         protected virtual Response Qeng(int mode)
496         {
497             // Only modes 0 and 2 are implemented.
498             switch(mode)
499             {
500                 case 0:
501                     return Ok.WithParameters($"+QENG: 0,{CellEarfcn},{CellEarfcnOffset},{CellPhysicalId},\"{CellId}\",{Rsrp},{(int)Rsrq},{Rssi},{Sinr},{Band},\"{TrackingAreaCode}\",{EnhancedCoverageLevel},{TransmitPower},2");
502                 case 2:
503                     // The 3 here is not a typo, it matches real modem output and the AT command manual
504                     return Ok.WithParameters($"+QENG: 3,{SleepDuration * 10:0},{RxTime * 10:0},{TxTime * 10:0}");
505                 default:
506                     return Error;
507             }
508         }
509 
510         // QGMR - Request Modem and Application Firmware Versions
511         [AtCommand("AT+QGMR")]
Qgmr()512         protected virtual Response Qgmr() => Ok.WithParameters($"{ManufacturerRevision}_{SoftwareRevision}");
513 
514         // QICFG - Configure Optional TCP/IP Parameters
515         [AtCommand("AT+QICFG", CommandType.Write)]
Qicfg(string parameter, params int[] args)516         protected virtual Response Qicfg(string parameter, params int[] args)
517         {
518             this.Log(LogLevel.Warning, "TCP/IP config value '{0}' set to {1}, not supported by this modem",
519                 parameter, args.Stringify());
520             return Error;
521         }
522 
523         // QNBIOTEVENT - Enable/Disable NB-IoT Related Event Report
524         [AtCommand("AT+QNBIOTEVENT", CommandType.Write)]
Qnbiotevent(int enable = 0, int eventType = 1)525         protected virtual Response Qnbiotevent(int enable = 0, int eventType = 1)
526         {
527             // Only event type 1 (PSM state) is supported
528             if(eventType != 1)
529             {
530                 return Error;
531             }
532             powerSavingModeEventEnabled = enable != 0;
533             return Ok;
534         }
535 
536         [AtCommand("AT+QNBIOTEVENT", CommandType.Read)]
QnbioteventRead()537         protected virtual Response QnbioteventRead()
538         {
539             return Ok.WithParameters($"+QNBIOTEVENT: {(powerSavingModeEventEnabled ? 1 : 0)},1");
540         }
541 
542         // QNBIOTRAI - NB-IoT Release Assistance Indication
543         [AtCommand("AT+QNBIOTRAI", CommandType.Write)]
Qnbiotrai(int raiMode = 0)544         protected virtual Response Qnbiotrai(int raiMode = 0)
545         {
546             if(raiMode < 0 || raiMode > 2)
547             {
548                 return Error;
549             }
550 
551             this.Log(LogLevel.Debug, "NB-IoT Release Assistance Indication set to {0}", raiMode);
552             return Ok; // stub
553         }
554 
555         // QPOWD - Power Off
556         [AtCommand("AT+QPOWD", CommandType.Write)]
QpowdWrite(PowerOffType type = PowerOffType.Normal)557         protected virtual Response QpowdWrite(PowerOffType type = PowerOffType.Normal)
558         {
559             Reset();
560             ExecuteWithDelay(EnableModem);
561             return Ok;
562         }
563 
564         // QRST - Module Reset
565         [AtCommand("AT+QRST", CommandType.Write)]
QrstWrite(int mode = 1)566         protected virtual Response QrstWrite(int mode = 1)
567         {
568             return Ok; // stub
569         }
570 
571         // QSCLK - Configure Sleep Mode
572         [AtCommand("AT+QSCLK", CommandType.Write)]
QsclkWrite(int mode = 1)573         protected virtual Response QsclkWrite(int mode = 1)
574         {
575             switch(mode)
576             {
577                 case 0: // Disable sleep modes.
578                 {
579                     deepsleepTimer.Enabled = false;
580                     break;
581                 }
582                 case 1: // Enable deep sleep mode.
583                 {
584                     if(ActiveTimeSeconds != 0)
585                     {
586                         deepsleepTimer.Value = 0;
587                         deepsleepTimer.Limit = (ulong)ActiveTimeSeconds;
588                         deepsleepTimer.Enabled = true;
589                         // Defer entering deep sleep until timer timeouts.
590                         this.Log(LogLevel.Noisy, "Defer deepsleep by {0} seconds", ActiveTimeSeconds);
591                         break;
592                     }
593                     // The signaling connection goes inactive when sleep mode is enabled.
594                     ExecuteWithDelay(() =>
595                     {
596                         SendSignalingConnectionStatus(false);
597 
598                         // Also, if we are configured to enter deep sleep when the sleep
599                         // lock is released, we also use sleep mode being enabled as our
600                         // cue to enter it.
601                         if(DeepsleepOnRellock)
602                         {
603                             EnterDeepsleep();
604                         }
605                     }, CsconDelay);
606                     break;
607                 }
608                 case 2: // Enable light sleep mode.
609                 {
610                     // Light sleep mode is not implemented.
611                     break;
612                 }
613                 default:
614                 {
615                     return Error;
616                 }
617             }
618             return Ok;
619         }
620 
621         // QIOPEN - Open a Socket Service
622         [AtCommand("AT+QIOPEN", CommandType.Write)]
Qiopen(int contextId, int connectionId, ServiceType serviceType, string host, ushort remotePort, ushort localPort = 0, int accessMode = 1)623         protected virtual Response Qiopen(int contextId, int connectionId, ServiceType serviceType, string host, ushort remotePort, ushort localPort = 0, int accessMode = 1)
624         {
625             if(!IsValidConnectionId(connectionId) || sockets[connectionId] != null)
626             {
627                 return Error;
628             }
629 
630             if(!IsValidContextId(contextId))
631             {
632                 return Error;
633             }
634 
635             this.Log(LogLevel.Debug, "Context {0} connectionId {1} requested {2} connection open to {3}:{4}",
636                 contextId, connectionId, serviceType, host, remotePort);
637 
638             // We can't just send Ok.WithTrailer because the driver won't see the URC.
639             ExecuteWithDelay(() =>
640             {
641                 var service = SocketService.Open(this, connectionId, serviceType, host, remotePort);
642                 if(service == null)
643                 {
644                     this.Log(LogLevel.Warning, "Failed to open connection {0}", connectionId);
645                 }
646                 else
647                 {
648                     this.Log(LogLevel.Debug, "Connection {0} opened successfully", connectionId);
649 
650                     if(serviceType == ServiceType.Tcp)
651                     {
652                         ExecuteWithDelay(() => SendSignalingConnectionStatus(true), CsconDelay + 50);
653                     }
654                 }
655                 sockets[connectionId] = service;
656                 SendString($"+QIOPEN: {connectionId},{(service == null ? 1 : 0)}");
657             });
658             return Ok;
659         }
660 
661         // QICLOSE - Close a Socket Service
662         [AtCommand("AT+QICLOSE", CommandType.Write)]
Qiclose(int connectionId)663         protected virtual Response Qiclose(int connectionId)
664         {
665             if(!IsValidConnectionId(connectionId))
666             {
667                 return Error;
668             }
669 
670             // AT+QICLOSE succeeds even if the socket was already closed.
671             sockets[connectionId]?.Dispose();
672             sockets[connectionId] = null;
673             // If all sockets are closed the signaling connection goes inactive.
674             // After the port is closed, modem disconnects after a period of inactivity.
675             ExecuteWithDelay(() =>
676             {
677                 if(sockets.All(s => s == null))
678                 {
679                     SendSignalingConnectionStatus(false);
680                 }
681             }, ClosePortInactivityDisconnectDelay);
682             return Ok.WithTrailer("CLOSE OK");
683         }
684 
685         // QIRD - Retrieve the Received TCP/IP Data
686         [AtCommand("AT+QIRD", CommandType.Write)]
Qird(int connectionId, int readLength)687         protected virtual Response Qird(int connectionId, int readLength)
688         {
689             if(!IsValidConnectionId(connectionId) || sockets[connectionId] == null)
690             {
691                 return Error;
692             }
693 
694             // Query the length of buffered received data
695             if(readLength == 0)
696             {
697                 return Ok.WithParameters($"+QIRD: {sockets[connectionId].BytesAvailable}");
698             }
699 
700             var readBytes = sockets[connectionId].Receive(readLength);
701             var qirdResponseHeader = $"+QIRD: {readBytes.Length}";
702             if(showLength)
703             {
704                 qirdResponseHeader += $",{sockets[connectionId].BytesAvailable}";
705             }
706             qirdResponseHeader += dataOutputSeparator;
707 
708             switch(receiveDataFormat)
709             {
710                 case DataFormat.Hex:
711                     var hexBytes = BitConverter.ToString(readBytes).Replace("-", "");
712                     return Ok.WithParameters(qirdResponseHeader + hexBytes.SurroundWith(dataOutputSurrounding));
713                 case DataFormat.Text:
714                     var dataOutputSurroundingBytes = StringEncoding.GetBytes(dataOutputSurrounding);
715                     return Ok.WithParameters(StringEncoding.GetBytes(qirdResponseHeader)
716                                                         .Concat(dataOutputSurroundingBytes)
717                                                         .Concat(readBytes)
718                                                         .Concat(dataOutputSurroundingBytes)
719                                                         .ToArray());
720                 default:
721                     throw new InvalidOperationException($"Invalid {nameof(receiveDataFormat)}");
722             }
723         }
724 
725         // QISEND - Send Hex/Text String Data
726         [AtCommand("AT+QISEND", CommandType.Write)]
Qisend(int connectionId, int? sendLength = null, string data = null, int? raiMode = null)727         protected virtual Response Qisend(int connectionId, int? sendLength = null, string data = null, int? raiMode = null)
728         {
729             if(!IsValidConnectionId(connectionId) || sockets[connectionId] == null)
730             {
731                 return Error;
732             }
733 
734             // Check the total lengths of data sent, acknowledged and not acknowledged
735             if(sendLength == 0)
736             {
737                 this.Log(LogLevel.Warning, "Sent data counters not implemented, returning 0");
738                 return Ok.WithParameters("+QISEND: 0,0,0");
739             }
740             else if(data != null) // Send data in non-data mode
741             {
742                 byte[] bytes;
743                 switch(sendDataFormat)
744                 {
745                     case DataFormat.Hex:
746                         bytes = Misc.HexStringToByteArray(data);
747                         break;
748                     case DataFormat.Text:
749                         bytes = StringEncoding.GetBytes(data);
750                         break;
751                     default:
752                         throw new InvalidOperationException($"Invalid {nameof(sendDataFormat)}");
753                 }
754                 // Non-data mode is only supported with a fixed length
755                 if(sendLength == null || bytes.Length != sendLength)
756                 {
757                     return Error;
758                 }
759                 else if(raiMode.HasValue)
760                 {
761                     if(raiMode.Value < 0 || raiMode.Value > 2)
762                     {
763                         return Error;
764                     }
765                     this.Log(LogLevel.Debug, "QISEND: NB-IoT Release Assistance Indication set to {0}", raiMode);
766                 }
767                 this.Log(LogLevel.Debug, "ConnectionId {0} requested send of '{1}' in non-data mode",
768                     connectionId, BitConverter.ToString(bytes));
769 
770                 SendSocketData(bytes, connectionId);
771                 // After data is sent, modem disconnects after a period of inactivity.
772                 ExecuteWithDelay(() =>
773                 {
774                     SendSignalingConnectionStatus(false);
775                 }, SendDataInactivityDisconnectDelay);
776                 return null;
777             }
778             else // Send data (fixed or variable-length) in data mode
779             {
780                 // We need to wait a while before sending the data mode prompt.
781                 ExecuteWithDelay(() =>
782                 {
783                     SendString(DataModePrompt);
784                     EnterDataMode(sendLength, bytes =>
785                     {
786                         this.Log(LogLevel.Debug, "ConnectionId {0} requested send of '{1}' in data mode",
787                             connectionId, BitConverter.ToString(bytes));
788 
789                         SendSocketData(bytes, connectionId);
790                     });
791                 });
792                 return null;
793             }
794         }
795 
796         [AtCommand("AT+QISTATE", CommandType.Read)]
QistateRead()797         protected virtual Response QistateRead() => Ok.WithParameters(
798             sockets.Where(s => s != null).Select(s => s.Qistate).ToArray());
799 
IsValidContextId(int id)800         protected virtual bool IsValidContextId(int id)
801         {
802             return id >= 1 && id <= 16;
803         }
804 
SendSocketData(byte[] bytes, int connectionId)805         protected void SendSocketData(byte[] bytes, int connectionId)
806         {
807             string sendResponse;
808             if(sockets[connectionId].Send(bytes))
809             {
810                 sendResponse = SendOk;
811             }
812             else
813             {
814                 sendResponse = SendFailed;
815                 this.Log(LogLevel.Warning, "Failed to send data to connection {0}", connectionId);
816             }
817             SendResponse(Ok);
818             // We can send the OK (the return value of this command) immediately,
819             // but we have to wait before SEND OK/SEND FAIL if the network is too fast
820             ExecuteWithDelay(() => SendString(sendResponse), 50);
821             // A successful send means the signaling connection became active, but this
822             // happens after the actual send notification hence the additional delay.
823             ExecuteWithDelay(() => SendSignalingConnectionStatus(true), SendDataActivityConnectDelay);
824         }
825 
EnterDeepsleep()826         protected void EnterDeepsleep()
827         {
828             if(!Enabled)
829             {
830                 return;
831             }
832 
833             this.Log(LogLevel.Debug, "Entering deep sleep mode");
834             // Entering deep sleep mode also enables power saving mode
835             SetPowerSavingMode(true);
836             ReportNbiotEvent(NbiotEvent.EnterDeepsleep);
837             // Entering deep sleep is equivalent to a power off, so we do a reset here.
838             // NVRAM values will be preserved.
839             Reset();
840         }
841 
EnableModem()842         protected void EnableModem()
843         {
844             Enabled = true;
845             // Notify the DTE that the modem is ready
846             SendString(ModemReady);
847             vddExt.Set();
848         }
849 
SetPowerSavingMode(bool enable)850         protected void SetPowerSavingMode(bool enable)
851         {
852             if(powerSavingModeActive == enable)
853             {
854                 return;
855             }
856 
857             ReportNbiotEvent(enable ? NbiotEvent.EnterPowerSavingMode : NbiotEvent.ExitPowerSavingMode);
858             powerSavingModeActive = enable;
859         }
860 
SendSignalingConnectionStatus(bool active)861         protected void SendSignalingConnectionStatus(bool active)
862         {
863             if(SendCSCONOnChangeOnly && signalingConnectionActive == active)
864             {
865                 return;
866             }
867 
868             // When the signaling connection becomes active, we leave PSM and vice versa
869             SetPowerSavingMode(!active);
870             signalingConnectionActive = active;
871 
872             if(!signalingConnectionStatusReportingEnabled)
873             {
874                 return;
875             }
876 
877             SendString(CsconContent(true));
878         }
879 
ReportNbiotEvent(NbiotEvent kind)880         protected void ReportNbiotEvent(NbiotEvent kind)
881         {
882             string eventDescription = null;
883             switch(kind)
884             {
885                 case NbiotEvent.EnterDeepsleep:
886                     if(deepSleepEventEnabled)
887                     {
888                         eventDescription = "ENTER DEEPSLEEP";
889                     }
890                     break;
891                 case NbiotEvent.EnterPowerSavingMode:
892                     if(powerSavingModeEventEnabled)
893                     {
894                         eventDescription = "ENTER PSM";
895                     }
896                     break;
897                 case NbiotEvent.ExitPowerSavingMode:
898                     if(powerSavingModeEventEnabled)
899                     {
900                         eventDescription = "EXIT PSM";
901                     }
902                     break;
903             }
904             if(eventDescription != null)
905             {
906                 SendString($"+QNBIOTEVENT: \"{eventDescription}\"");
907             }
908         }
909 
910         // NETLIGHT pin is controlled by AT+QLEDMODE (BC660K, BC66) or AT+QCFG="ledmode" (BG96)
911         // You can implement exact AT commands for particular models using this helper.
SetNetLightMode(int ledMode)912         protected Response SetNetLightMode(int ledMode)
913         {
914             if(ledMode < 0 || ledMode > 1)
915             {
916                 this.Log(LogLevel.Warning, "Invalid mode for NETLIGHT pin: {0}", ledMode);
917                 return Error;
918             }
919             netLightMode = ledMode;
920             return Ok;
921         }
922 
923         protected abstract string Vendor { get; }
924         protected abstract string ModelName { get; }
925         protected abstract string Revision { get; }
926         protected abstract string ManufacturerRevision { get; }
927         protected abstract string SoftwareRevision { get; }
928 
929         protected bool echoInDataMode;
930 
931         // These fields are not affected by resets because they are automatically saved to NVRAM.
932         protected string pdpContextApn = "";
933         protected bool showLength = false;
934         protected DataFormat sendDataFormat = DataFormat.Text;
935         protected DataFormat receiveDataFormat = DataFormat.Text;
936         protected string dataOutputSeparator = CrLf;
937         protected string dataOutputSurrounding = ""; // for specific models it can be overriden in the constructor
938         protected bool deepSleepEventEnabled = false;
939         protected bool powerSavingModeEventEnabled;
940         protected bool signalingConnectionStatusReportingEnabled;
941         protected bool outOfServiceAreaUrcEnabled;
942         protected NetworkRegistrationUrcType networkRegistrationUrcType;
943         protected int netLightMode;
944         protected FunctionalityLevel functionalityLevel;
945 
946         protected readonly string imeiNumber;
947         protected readonly LimitTimer deepsleepTimer;
948 
MobileTerminationError(int errorCode)949         private Response MobileTerminationError(int errorCode)
950         {
951             switch(mtResultCodeMode)
952             {
953                 case MobileTerminationResultCodeMode.Disabled:
954                     return Error;
955                 case MobileTerminationResultCodeMode.Numeric:
956                     return new Response($"+CME ERROR: {errorCode}");
957                 case MobileTerminationResultCodeMode.Verbose:
958                     this.Log(LogLevel.Warning, "Verbose MT error reporting is not implemented");
959                     goto case MobileTerminationResultCodeMode.Numeric;
960                 default:
961                     throw new ArgumentOutOfRangeException();
962             }
963         }
964 
EnterDataMode(int? byteCount, Action<byte[]> callback)965         private void EnterDataMode(int? byteCount, Action<byte[]> callback)
966         {
967             if(byteCount == 0)
968             {
969                 this.Log(LogLevel.Warning, "Tried to enter data mode with a fixed length of 0 bytes, ignoring");
970                 return;
971             }
972 
973             PassthroughMode = true;
974             dataBytesRemaining = byteCount;
975             dataCallback = callback;
976         }
977 
ExitDataMode(bool callCallback)978         private void ExitDataMode(bool callCallback)
979         {
980             if(callCallback)
981             {
982                 dataCallback(dataBuffer.ToArray());
983             }
984             dataBuffer.SetLength(0);
985             PassthroughMode = false;
986         }
987 
BytesReceived(int connectionId, int byteCount)988         private void BytesReceived(int connectionId, int byteCount)
989         {
990             // We do both of the sends here after a delay to accomodate software
991             // which might not expect the "instant" reply which would otherwise happen.
992             // Notify that the signaling connection is active
993             ExecuteWithDelay(() => SendSignalingConnectionStatus(true), CsconDelay);
994             // Send the data received notification. This needs to happen after the signaling
995             // connection active notification, so we add a delay on top of CsconDelay.
996             ExecuteWithDelay(() =>
997             {
998                 var recvUrc = $"+QIURC: \"recv\",{connectionId}";
999                 if(showLength)
1000                 {
1001                     recvUrc += $",{byteCount}";
1002                 }
1003                 SendString(recvUrc);
1004             }, CsconDelay + 500);
1005         }
1006 
IsValidConnectionId(int connectionId, [CallerMemberName] string caller = R)1007         private bool IsValidConnectionId(int connectionId, [CallerMemberName] string caller = "")
1008         {
1009             if(connectionId < 0 || connectionId >= sockets.Length)
1010             {
1011                 this.Log(LogLevel.Warning, "Connection ID {0} is invalid in {1}", connectionId, caller);
1012                 return false;
1013             }
1014             return true;
1015         }
1016 
1017         // Return encoded string that represents the greatest available value
1018         // that is not greater than the requested one.
ConvertSecondsToEncodedString(int t, ModemTimerType timerType)1019         protected string ConvertSecondsToEncodedString(int t, ModemTimerType timerType)
1020         {
1021             IReadOnlyDictionary<byte, int> keyMap;
1022             switch(timerType)
1023             {
1024                 case ModemTimerType.ActiveTimeT3324:
1025                 {
1026                     keyMap = activeTimeUnitToSecondsMultiplier;
1027                     break;
1028                 }
1029                 case ModemTimerType.PeriodicTauT3412:
1030                 {
1031                     keyMap = periodicTauTimeUnitToSecondsMultiplier;
1032                     break;
1033                 }
1034                 default:
1035                 {
1036                     throw new ArgumentException("Invalid timer type");
1037                 }
1038             }
1039 
1040             byte selectedKey = 0b111;
1041             int selectedValue = 0;
1042 
1043             foreach(var entry in keyMap.OrderByDescending(x => x.Value))
1044             {
1045                 if(t >= entry.Value)
1046                 {
1047                     selectedKey = entry.Key;
1048                     selectedValue = entry.Value;
1049                     break;
1050                 }
1051             }
1052 
1053             selectedValue = selectedValue == 0 ? 0 : t / selectedValue;
1054 
1055             const int timerValueWidth = 5;
1056             const int maxTimerValue = (1 << timerValueWidth) - 1; // 5 bits for coding timer value
1057             var timeByteCoded = selectedKey << timerValueWidth | (selectedValue & maxTimerValue);
1058 
1059             return Convert.ToString(timeByteCoded, 2).PadLeft(8, '0');
1060         }
1061 
ConvertEncodedStringToSeconds(string encoded, ModemTimerType timerType)1062         protected int ConvertEncodedStringToSeconds(string encoded, ModemTimerType timerType)
1063         {
1064             IReadOnlyDictionary<byte, int> keyMap;
1065             int defaultMultiplier;
1066             switch(timerType)
1067             {
1068                 case ModemTimerType.ActiveTimeT3324:
1069                 {
1070                     keyMap = activeTimeUnitToSecondsMultiplier;
1071                     defaultMultiplier = ActiveTimeDefaultUnitMultiplier;
1072                     break;
1073                 }
1074                 case ModemTimerType.PeriodicTauT3412:
1075                 {
1076                     keyMap = periodicTauTimeUnitToSecondsMultiplier;
1077                     defaultMultiplier = PeriodicTauDefaultUnitMultiplier;
1078                     break;
1079                 }
1080                 default:
1081                 {
1082                     throw new ArgumentException("Invalid timer type");
1083                 }
1084             }
1085 
1086             if(!Misc.TryParseBitPattern(encoded, out var parsed, out _))
1087             {
1088                 this.Log(LogLevel.Warning, "Unable to decode time code '{0}' - should be a bit-string", encoded);
1089                 return 0;
1090             }
1091             var timerValue = BitHelper.GetValue((byte)parsed, 0, 5);
1092             var unit = BitHelper.GetValue((byte)parsed, 5, 3);
1093 
1094             if(keyMap.TryGetValue(unit, out var multiplier))
1095             {
1096                 return multiplier * timerValue;
1097             }
1098             return defaultMultiplier * timerValue;
1099         }
1100 
1101         // When this is set to Numeric or Verbose, MT-related errors are reported with "+CME ERROR: "
1102         // instead of the plain "ERROR". This does not apply to syntax errors, invalid parameter
1103         // errors or Terminal Adapter functionality.
1104         private MobileTerminationResultCodeMode mtResultCodeMode;
1105         private MemoryStream dataBuffer;
1106         private int? dataBytesRemaining;
1107         private Action<byte[]> dataCallback;
1108         private bool inReset;
1109         private bool signalingConnectionActive;
1110         private bool powerSavingModeActive;
1111 
1112         private readonly string softwareVersionNumber;
1113         private readonly string serialNumber;
1114         private readonly IGPIO vddExt = new GPIO();
1115         private readonly IGPIO netLight = new GPIO();
1116         private readonly SocketService[] sockets = new SocketService[NumberOfConnections];
1117 
1118         private readonly IReadOnlyDictionary<byte, int> activeTimeUnitToSecondsMultiplier = new Dictionary<byte, int>
1119         {
1120             { 0b111, 0 },
1121             { 0b000, 2 },
1122             { 0b001, 60 },
1123             { 0b010, 6 * 60 }
1124         };
1125 
1126         private readonly IReadOnlyDictionary<byte, int> periodicTauTimeUnitToSecondsMultiplier = new Dictionary<byte, int>
1127         {
1128             { 0b111, 0 },
1129             { 0b011, 2 },
1130             { 0b100, 30 },
1131             { 0b101, 60 },
1132             { 0b000, 10 * 60 },
1133             { 0b001, 60 * 60 },
1134             { 0b010, 10 * 60 * 60 },
1135             { 0b110, 320 * 60 * 60 }
1136         };
1137 
1138         private const string DefaultImeiNumber = "866818039921444";
1139         private const string DefaultSoftwareVersionNumber = "31";
1140         private const string DefaultSerialNumber = "<serial number>";
1141         private const string DataModePrompt = ">";
1142         private const string SendOk = "SEND OK";
1143         private const string SendFailed = "SEND FAIL";
1144         private const string ModemReady = "RDY";
1145         private const int NumberOfConnections = 4;
1146         private const int ActiveTimeDefaultUnitMultiplier = 60;
1147         private const int PeriodicTauDefaultUnitMultiplier = 60 * 60;
1148 
1149         public enum NetworkRegistrationStates
1150         {
1151             NotRegisteredNotSearching,
1152             RegisteredHomeNetwork,
1153             NotRegisteredSearching,
1154             RegistrationDenied,
1155             Unknown,
1156             RegisteredRoaming,
1157         }
1158 
1159         public enum PublicLandMobileNetworkSearchingState
1160         {
1161             SearchingInactive,
1162             Searching,
1163             Selected,
1164             OutOfService
1165         }
1166 
1167         protected enum ModemTimerType
1168         {
1169             ActiveTimeT3324,
1170             PeriodicTauT3412
1171         }
1172 
1173         protected enum MobileTerminationResultCodeMode
1174         {
1175             Disabled,
1176             Numeric,
1177             Verbose,
1178         }
1179 
1180         protected enum ServiceType
1181         {
1182             Tcp,
1183             Udp,
1184             TcpListener,
1185             UdpService,
1186         }
1187 
1188         protected enum GPIOInput
1189         {
1190             Power,
1191             Reset,
1192             PsmEint,
1193         }
1194 
1195         protected enum PowerOffType
1196         {
1197             Normal,
1198             Immediate,
1199             Reset,
1200         }
1201 
1202         protected enum SerialNumberType
1203         {
1204             Device,
1205             Imei,
1206             ImeiSv,
1207             SoftwareVersionNumber,
1208         }
1209 
1210         protected enum FunctionalityLevel
1211         {
1212             Minimum,
1213             Full,
1214             RfTransmitReceiveDisabled = 4,
1215             UsimDisabled = 7,
1216         }
1217 
1218         protected enum NetworkRegistrationUrcType
1219         {
1220             Disabled,
1221             StatOnly,
1222             StatLocation,
1223             StatLocationEmmCause,
1224             StatLocationPsm,
1225             StatLocationEmmCausePsm,
1226         }
1227 
1228         protected enum DataFormat
1229         {
1230             Text,
1231             Hex,
1232         }
1233 
1234         protected enum PdpType
1235         {
1236             Ip,
1237             IpV6,
1238             IpV4V6,
1239             NonIp,
1240         }
1241 
1242         protected enum AuthenticationType
1243         {
1244             None,
1245             Pap,
1246             Chap,
1247         }
1248 
1249         protected enum NbiotEvent
1250         {
1251             EnterPowerSavingMode,
1252             ExitPowerSavingMode,
1253             EnterDeepsleep,
1254         }
1255 
1256         // One SocketService corresponds to one connectionId
1257         private class SocketService : IDisposable
1258         {
Open(QuectelModem owner, int connectionId, ServiceType type, string remoteHost, ushort remotePort)1259             public static SocketService Open(QuectelModem owner, int connectionId, ServiceType type, string remoteHost, ushort remotePort)
1260             {
1261                 var emulatedServices = EmulationManager.Instance.CurrentEmulation.ExternalsManager.GetExternalsOfType<IEmulatedNetworkService>();
1262                 var service = emulatedServices.FirstOrDefault(s => s.Host == remoteHost && s.Port == remotePort);
1263                 if(service == null)
1264                 {
1265                     owner.Log(LogLevel.Warning, "No external service found for {0}:{1}", remoteHost, remotePort);
1266                     return null;
1267                 }
1268 
1269                 return new SocketService(owner, connectionId, type, service, remoteHost, remotePort);
1270             }
1271 
1272             public bool Send(byte[] data) => connectedService.Send(data);
1273 
Receive(int bytes)1274             public byte[] Receive(int bytes) => connectedService.Receive(bytes);
1275 
Dispose()1276             public void Dispose()
1277             {
1278                 connectedService.BytesReceived -= BytesReceived;
1279                 connectedService.Disconnect();
1280             }
1281 
1282             public int BytesAvailable => connectedService.BytesAvailable;
1283 
1284             public string Qistate => $"+QISTATE: {ConnectionId},\"{Type.ToString().ToUpper()}\",\"{RemoteHost}\",{RemotePort}";
1285 
1286             public ServiceType Type { get; }
1287             public QuectelModem Owner { get; }
1288             public int ConnectionId { get; }
1289             public string RemoteHost { get; }
1290             public ushort RemotePort { get; }
1291 
BytesReceived(int byteCount)1292             private void BytesReceived(int byteCount) => Owner.BytesReceived(ConnectionId, byteCount);
1293 
1294             private readonly IEmulatedNetworkService connectedService;
1295 
SocketService(QuectelModem owner, int connectionId, ServiceType type, IEmulatedNetworkService conn, string remoteHost, ushort remotePort)1296             private SocketService(QuectelModem owner, int connectionId, ServiceType type, IEmulatedNetworkService conn, string remoteHost, ushort remotePort)
1297             {
1298                 Owner = owner;
1299                 ConnectionId = connectionId;
1300                 Type = type;
1301                 RemoteHost = remoteHost;
1302                 RemotePort = remotePort;
1303                 connectedService = conn;
1304                 connectedService.BytesReceived += BytesReceived;
1305             }
1306         }
1307     }
1308 }
1309