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