4 XCSoar Glide Computer - http://www.xcsoar.org/
5 Copyright (C) 2000-2013 The XCSoar Project
6 A detailed list of copyright holders can be found in the file "AUTHORS".
8 This program is free software; you can redistribute it and/or
9 modify it under the terms of the GNU General Public License
10 as published by the Free Software Foundation; either version 2
11 of the License, or (at your option) any later version.
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24 #include "Device/Descriptor.hpp"
25 #include "Device/Driver.hpp"
26 #include "Device/Parser.hpp"
27 #include "Driver/FLARM/Device.hpp"
28 #include "Driver/LX/Internal.hpp"
29 #include "Device/Internal.hpp"
30 #include "Device/Register.hpp"
31 #include "Blackboard/DeviceBlackboard.hpp"
32 #include "Components.hpp"
33 #include "Port/ConfiguredPort.hpp"
34 #include "NMEA/Info.hpp"
35 #include "Thread/Mutex.hpp"
36 #include "Util/StringUtil.hpp"
37 #include "Logger/NMEALogger.hpp"
38 #include "Language/Language.hpp"
39 #include "Operation/Operation.hpp"
40 #include "OS/Clock.hpp"
41 #include "../Simulator.hpp"
43 #include "Input/InputQueue.hpp"
44 #include "LogFile.hpp"
45 #include "Job/Job.hpp"
48 #include "Java/Object.hpp"
49 #include "Java/Global.hpp"
50 #include "Android/InternalSensors.hpp"
51 #include "Android/Main.hpp"
52 #include "Android/Product.hpp"
56 #include "Android/IOIOHelper.hpp"
57 #include "Android/BMP085Device.hpp"
58 #include "Android/I2CbaroDevice.hpp"
59 #include "Android/NunchuckDevice.hpp"
60 #include "Android/VoltageDevice.hpp"
66 * This scope class calls DeviceDescriptor::Return() and
67 * DeviceDescriptor::EnableNMEA() when the caller leaves the current
68 * scope. The caller must have called DeviceDescriptor::Borrow()
69 * successfully before constructing this class.
71 struct ScopeReturnDevice
{
72 DeviceDescriptor
&device
;
73 OperationEnvironment
&env
;
75 ScopeReturnDevice(DeviceDescriptor
&_device
, OperationEnvironment
&_env
)
76 :device(_device
), env(_env
) {
79 ~ScopeReturnDevice() {
80 device
.EnableNMEA(env
);
85 class OpenDeviceJob final
: public Job
{
86 DeviceDescriptor
&device
;
89 OpenDeviceJob(DeviceDescriptor
&_device
):device(_device
) {}
91 /* virtual methods from class Job */
92 virtual void Run(OperationEnvironment
&env
) {
97 DeviceDescriptor::DeviceDescriptor(unsigned _index
)
100 port(NULL
), monitor(NULL
), dispatcher(NULL
),
101 driver(NULL
), device(NULL
),
103 internal_sensors(NULL
),
105 droidsoar_v2(nullptr),
110 ticker(false), borrowed(false)
114 for (unsigned i
=0; i
<sizeof i2cbaro
/sizeof i2cbaro
[0]; i
++)
115 i2cbaro
[i
] = nullptr;
120 DeviceDescriptor::SetConfig(const DeviceConfig
&_config
)
124 if (config
.UsesDriver()) {
125 driver
= FindDriverByName(config
.driver_name
);
126 assert(driver
!= NULL
);
132 DeviceDescriptor::ClearConfig()
138 DeviceDescriptor::GetState() const
140 if (open_job
!= nullptr)
141 return PortState::LIMBO
;
144 return port
->GetState();
147 if (internal_sensors
!= nullptr)
148 return PortState::READY
;
151 if (droidsoar_v2
!= nullptr)
152 return PortState::READY
;
154 if (i2cbaro
[0] != nullptr)
155 return PortState::READY
;
157 if (nunchuck
!= nullptr)
158 return PortState::READY
;
160 if (voltage
!= nullptr)
161 return PortState::READY
;
165 return PortState::FAILED
;
169 DeviceDescriptor::ShouldReopenDriverOnTimeout() const
171 return driver
== NULL
|| driver
->HasTimeout();
175 DeviceDescriptor::CancelAsync()
177 assert(InMainThread());
182 assert(open_job
!= NULL
);
192 DeviceDescriptor::Open(Port
&_port
, OperationEnvironment
&env
)
194 assert(port
== NULL
);
195 assert(device
== NULL
);
196 assert(driver
!= NULL
);
198 assert(!IsBorrowed());
200 reopen_clock
.Update();
202 device_blackboard
->mutex
.Lock();
203 device_blackboard
->SetRealState(index
).Reset();
204 device_blackboard
->ScheduleMerge();
205 device_blackboard
->mutex
.Unlock();
207 settings_sent
.Clear();
208 settings_received
.Clear();
214 parser
.SetReal(_tcscmp(driver
->name
, _T("Condor")) != 0);
215 parser
.SetIgnoreChecksum(config
.ignore_checksum
);
216 if (config
.IsDriver(_T("Condor")))
217 parser
.DisableGeoid();
219 if (driver
->CreateOnPort
!= nullptr) {
220 Device
*new_device
= driver
->CreateOnPort(config
, *port
);
222 const ScopeLock
protect(mutex
);
225 port
->StartRxThread();
232 DeviceDescriptor::OpenInternalSensors()
238 if (IsNookSimpleTouch())
239 /* avoid a crash on startup b/c nook has no internal sensors */
243 InternalSensors::create(Java::GetEnv(), context
, GetIndex());
244 if (internal_sensors
) {
245 // TODO: Allow user to specify whether they want certain sensors.
246 internal_sensors
->subscribeToSensor(InternalSensors::TYPE_PRESSURE
);
254 DeviceDescriptor::OpenDroidSoarV2()
260 if (ioio_helper
== nullptr)
263 if (i2cbaro
[0] == NULL
) {
264 i2cbaro
[0] = new I2CbaroDevice(GetIndex(), Java::GetEnv(),
265 ioio_helper
->GetHolder(),
266 DeviceConfig::PressureUse::STATIC_WITH_VARIO
,
267 config
.sensor_offset
,
268 2 + (0x77 << 8) + (27 << 16), 0, // bus, address
272 i2cbaro
[1] = new I2CbaroDevice(GetIndex(), Java::GetEnv(),
273 ioio_helper
->GetHolder(),
274 // needs calibration ?
275 (config
.sensor_factor
== fixed(0)) ? DeviceConfig::PressureUse::PITOT_ZERO
:
276 DeviceConfig::PressureUse::PITOT
,
277 config
.sensor_offset
, 1 + (0x77 << 8) + (46 << 16), 0 ,
287 DeviceDescriptor::OpenI2Cbaro()
293 if (ioio_helper
== nullptr)
296 for (unsigned i
=0; i
<sizeof i2cbaro
/sizeof i2cbaro
[0]; i
++) {
297 if (i2cbaro
[i
] == NULL
) {
298 i2cbaro
[i
] = new I2CbaroDevice(GetIndex(), Java::GetEnv(),
299 ioio_helper
->GetHolder(),
300 // needs calibration ?
301 (config
.sensor_factor
== fixed(0) && config
.press_use
== DeviceConfig::PressureUse::PITOT
) ?
302 DeviceConfig::PressureUse::PITOT_ZERO
:
304 config
.sensor_offset
,
305 config
.i2c_bus
, config
.i2c_addr
,
306 config
.press_use
== DeviceConfig::PressureUse::TEK_PRESSURE
? 20 : 5,
307 0); // called flags, actually reserved for future use.
316 DeviceDescriptor::OpenNunchuck()
322 if (ioio_helper
== nullptr)
325 nunchuck
= new NunchuckDevice(GetIndex(), Java::GetEnv(),
326 ioio_helper
->GetHolder(),
327 config
.i2c_bus
, 5); // twi, sample_rate
335 DeviceDescriptor::OpenVoltage()
341 if (ioio_helper
== nullptr)
344 voltage
= new VoltageDevice(GetIndex(), Java::GetEnv(),
345 ioio_helper
->GetHolder(),
346 config
.sensor_offset
, config
.sensor_factor
,
347 60); // sample_rate per minute
355 DeviceDescriptor::DoOpen(OperationEnvironment
&env
)
357 assert(config
.IsAvailable());
359 if (config
.port_type
== DeviceConfig::PortType::INTERNAL
)
360 return OpenInternalSensors();
362 if (config
.port_type
== DeviceConfig::PortType::DROIDSOAR_V2
)
363 return OpenDroidSoarV2();
365 if (config
.port_type
== DeviceConfig::PortType::I2CPRESSURESENSOR
)
366 return OpenI2Cbaro();
368 if (config
.port_type
== DeviceConfig::PortType::NUNCHUCK
)
369 return OpenNunchuck();
371 if (config
.port_type
== DeviceConfig::PortType::IOIOVOLTAGE
)
372 return OpenVoltage();
374 reopen_clock
.Update();
376 Port
*port
= OpenPort(config
, *this);
378 TCHAR name_buffer
[64];
379 const TCHAR
*name
= config
.GetPortName(name_buffer
, 64);
381 StaticString
<256> msg
;
382 msg
.Format(_T("%s: %s."), _("Unable to open port"), name
);
383 env
.SetErrorMessage(msg
);
387 if (!port
->WaitConnected(env
) || !Open(*port
, env
)) {
396 DeviceDescriptor::Open(OperationEnvironment
&env
)
398 assert(InMainThread());
399 assert(port
== NULL
);
400 assert(device
== NULL
);
402 assert(!IsBorrowed());
404 if (is_simulator() || !config
.IsAvailable())
409 assert(!IsOccupied());
410 assert(open_job
== NULL
);
413 LogFormat(_T("Opening device %s"), config
.GetPortName(buffer
, 64));
415 open_job
= new OpenDeviceJob(*this);
416 async
.Start(open_job
, env
, this);
420 DeviceDescriptor::Close()
422 assert(InMainThread());
423 assert(!IsBorrowed());
428 delete internal_sensors
;
429 internal_sensors
= NULL
;
433 droidsoar_v2
= nullptr;
435 for (unsigned i
=0; i
<sizeof i2cbaro
/sizeof i2cbaro
[0]; i
++) {
437 i2cbaro
[i
] = nullptr;
449 /* safely delete the Device object */
450 Device
*old_device
= device
;
453 const ScopeLock
protect(mutex
);
455 /* after leaving this scope, no other thread may use the old
456 object; to avoid locking the mutex for too long, the "delete"
457 is called after the scope */
462 Port
*old_port
= port
;
468 device_blackboard
->mutex
.Lock();
469 device_blackboard
->SetRealState(index
).Reset();
470 device_blackboard
->ScheduleMerge();
471 device_blackboard
->mutex
.Unlock();
473 settings_sent
.Clear();
474 settings_received
.Clear();
478 DeviceDescriptor::Reopen(OperationEnvironment
&env
)
480 assert(InMainThread());
481 assert(!IsBorrowed());
488 DeviceDescriptor::AutoReopen(OperationEnvironment
&env
)
490 assert(InMainThread());
492 if (/* don't reopen a device that is occupied */
494 !config
.IsAvailable() ||
496 /* attempt to reopen a failed device every 30 seconds */
497 !reopen_clock
.CheckUpdate(30000))
501 LogFormat(_T("Reconnecting to device %s"), config
.GetPortName(buffer
, 64));
503 InputEvents::processGlideComputer(GCE_COMMPORT_RESTART
);
508 DeviceDescriptor::EnableNMEA(OperationEnvironment
&env
)
513 bool success
= device
->EnableNMEA(env
);
516 /* re-enable the NMEA handler if it has been disabled by the
518 port
->StartRxThread();
524 DeviceDescriptor::GetDisplayName() const
526 return driver
!= NULL
? driver
->display_name
: NULL
;
530 DeviceDescriptor::IsDriver(const TCHAR
*name
) const
532 return driver
!= NULL
533 ? _tcscmp(driver
->name
, name
) == 0
538 DeviceDescriptor::CanDeclare() const
540 return driver
!= NULL
&&
541 (driver
->CanDeclare() ||
542 device_blackboard
->IsFLARM(index
));
546 DeviceDescriptor::IsLogger() const
548 return driver
!= NULL
&& driver
->IsLogger();
552 DeviceDescriptor::IsNMEAOut() const
554 return driver
!= NULL
&& driver
->IsNMEAOut();
558 DeviceDescriptor::IsManageable() const
560 if (driver
!= NULL
) {
561 if (driver
->IsManageable())
564 if (_tcscmp(driver
->name
, _T("LX")) == 0 && device
!= NULL
) {
565 const LXDevice
&lx
= *(const LXDevice
*)device
;
566 return lx
.IsV7() || lx
.IsNano() || lx
.IsLX16xx();
574 DeviceDescriptor::Borrow()
576 assert(InMainThread());
586 DeviceDescriptor::Return()
588 assert(InMainThread());
589 assert(IsBorrowed());
592 assert(!IsOccupied());
594 /* if the caller has disabled the NMEA while the device was
595 borrowed, we may not have received new values for some time, but
596 that doesn't mean the device has failed; give it some time to
597 recover, and don't reopen right away */
598 reopen_clock
.Update();
602 DeviceDescriptor::IsAlive() const
604 ScopeLock
protect(device_blackboard
->mutex
);
605 return device_blackboard
->RealState(index
).alive
;
609 DeviceDescriptor::ParseNMEA(const char *line
, NMEAInfo
&info
)
611 assert(line
!= NULL
);
613 /* restore the driver's ExternalSettings */
614 const ExternalSettings old_settings
= info
.settings
;
615 info
.settings
= settings_received
;
617 if (device
!= NULL
&& device
->ParseNMEA(line
, info
)) {
618 info
.alive
.Update(info
.clock
);
620 if (!config
.sync_from_device
)
621 info
.settings
= old_settings
;
623 /* clear the settings when the values are the same that we already
624 sent to the device */
625 const ExternalSettings old_received
= settings_received
;
626 settings_received
= info
.settings
;
627 info
.settings
.EliminateRedundant(settings_sent
, old_received
);
632 /* no change - restore the ExternalSettings that we returned last
634 info
.settings
= old_settings
;
636 // Additional "if" to find GPS strings
637 if (parser
.ParseLine(line
, info
)) {
638 info
.alive
.Update(fixed(MonotonicClockMS()) / 1000);
646 DeviceDescriptor::ForwardLine(const char *line
)
648 /* XXX make this method thread-safe; this method can be called from
649 any thread, and if the Port gets closed, bad things happen */
651 if (IsNMEAOut() && port
!= NULL
) {
658 DeviceDescriptor::WriteNMEA(const char *line
, OperationEnvironment
&env
)
660 assert(line
!= NULL
);
662 return port
!= NULL
&& PortWriteNMEA(*port
, line
, env
);
667 DeviceDescriptor::WriteNMEA(const TCHAR
*line
, OperationEnvironment
&env
)
669 assert(line
!= NULL
);
674 char buffer
[_tcslen(line
) * 4 + 1];
675 if (::WideCharToMultiByte(CP_ACP
, 0, line
, -1, buffer
, sizeof(buffer
),
679 return WriteNMEA(buffer
, env
);
684 DeviceDescriptor::PutMacCready(fixed value
, OperationEnvironment
&env
)
686 assert(InMainThread());
688 if (device
== NULL
|| settings_sent
.CompareMacCready(value
) ||
689 !config
.sync_to_device
)
693 /* TODO: postpone until the borrowed device has been returned */
696 ScopeReturnDevice
restore(*this, env
);
697 if (!device
->PutMacCready(value
, env
))
700 ScopeLock
protect(device_blackboard
->mutex
);
701 NMEAInfo
&basic
= device_blackboard
->SetRealState(index
);
702 settings_sent
.mac_cready
= value
;
703 settings_sent
.mac_cready_available
.Update(basic
.clock
);
709 DeviceDescriptor::PutBugs(fixed value
, OperationEnvironment
&env
)
711 assert(InMainThread());
713 if (device
== NULL
|| settings_sent
.CompareBugs(value
) ||
714 !config
.sync_to_device
)
718 /* TODO: postpone until the borrowed device has been returned */
721 ScopeReturnDevice
restore(*this, env
);
722 if (!device
->PutBugs(value
, env
))
725 ScopeLock
protect(device_blackboard
->mutex
);
726 NMEAInfo
&basic
= device_blackboard
->SetRealState(index
);
727 settings_sent
.bugs
= value
;
728 settings_sent
.bugs_available
.Update(basic
.clock
);
734 DeviceDescriptor::PutBallast(fixed fraction
, fixed overload
,
735 OperationEnvironment
&env
)
737 assert(InMainThread());
739 if (device
== NULL
|| !config
.sync_to_device
||
740 (settings_sent
.CompareBallastFraction(fraction
) &&
741 settings_sent
.CompareBallastOverload(overload
)))
745 /* TODO: postpone until the borrowed device has been returned */
748 ScopeReturnDevice
restore(*this, env
);
749 if (!device
->PutBallast(fraction
, overload
, env
))
752 ScopeLock
protect(device_blackboard
->mutex
);
753 NMEAInfo
&basic
= device_blackboard
->SetRealState(index
);
754 settings_sent
.ballast_fraction
= fraction
;
755 settings_sent
.ballast_fraction_available
.Update(basic
.clock
);
756 settings_sent
.ballast_overload
= overload
;
757 settings_sent
.ballast_overload_available
.Update(basic
.clock
);
763 DeviceDescriptor::PutVolume(unsigned volume
, OperationEnvironment
&env
)
765 assert(InMainThread());
767 if (device
== NULL
|| !config
.sync_to_device
)
771 /* TODO: postpone until the borrowed device has been returned */
774 ScopeReturnDevice
restore(*this, env
);
775 return device
->PutVolume(volume
, env
);
779 DeviceDescriptor::PutActiveFrequency(RadioFrequency frequency
,
780 OperationEnvironment
&env
)
782 assert(InMainThread());
784 if (device
== NULL
|| !config
.sync_to_device
)
788 /* TODO: postpone until the borrowed device has been returned */
791 ScopeReturnDevice
restore(*this, env
);
792 return device
->PutActiveFrequency(frequency
, env
);
796 DeviceDescriptor::PutStandbyFrequency(RadioFrequency frequency
,
797 OperationEnvironment
&env
)
799 assert(InMainThread());
801 if (device
== NULL
|| !config
.sync_to_device
)
805 /* TODO: postpone until the borrowed device has been returned */
808 ScopeReturnDevice
restore(*this, env
);
809 return device
->PutStandbyFrequency(frequency
, env
);
813 DeviceDescriptor::PutQNH(const AtmosphericPressure
&value
,
814 OperationEnvironment
&env
)
816 assert(InMainThread());
818 if (device
== NULL
|| settings_sent
.CompareQNH(value
) ||
819 !config
.sync_to_device
)
823 /* TODO: postpone until the borrowed device has been returned */
826 ScopeReturnDevice
restore(*this, env
);
827 if (!device
->PutQNH(value
, env
))
830 ScopeLock
protect(device_blackboard
->mutex
);
831 NMEAInfo
&basic
= device_blackboard
->SetRealState(index
);
832 settings_sent
.qnh
= value
;
833 settings_sent
.qnh_available
.Update(basic
.clock
);
839 DeclareToFLARM(const struct Declaration
&declaration
, Port
&port
,
840 const Waypoint
*home
, OperationEnvironment
&env
)
842 return FlarmDevice(port
).Declare(declaration
, home
, env
);
846 DeclareToFLARM(const struct Declaration
&declaration
,
847 Port
&port
, const DeviceRegister
&driver
, Device
*device
,
848 const Waypoint
*home
,
849 OperationEnvironment
&env
)
851 /* enable pass-through mode in the "front" device */
852 if (driver
.HasPassThrough() && device
!= NULL
&&
853 !device
->EnablePassThrough(env
))
856 return DeclareToFLARM(declaration
, port
, home
, env
);
860 DoDeclare(const struct Declaration
&declaration
,
861 Port
&port
, const DeviceRegister
&driver
, Device
*device
,
862 bool flarm
, const Waypoint
*home
,
863 OperationEnvironment
&env
)
865 StaticString
<60> text
;
866 text
.Format(_T("%s: %s."), _("Sending declaration"), driver
.display_name
);
869 bool result
= device
!= NULL
&& device
->Declare(declaration
, home
, env
);
872 text
.Format(_T("%s: FLARM."), _("Sending declaration"));
875 result
|= DeclareToFLARM(declaration
, port
, driver
, device
, home
, env
);
882 DeviceDescriptor::Declare(const struct Declaration
&declaration
,
883 const Waypoint
*home
,
884 OperationEnvironment
&env
)
887 assert(port
!= NULL
);
888 assert(driver
!= NULL
);
889 assert(device
!= NULL
);
891 /* enable the "muxed FLARM" hack? */
892 const bool flarm
= device_blackboard
->IsFLARM(index
) &&
893 !IsDriver(_T("FLARM"));
895 return DoDeclare(declaration
, *port
, *driver
, device
, flarm
,
900 DeviceDescriptor::ReadFlightList(RecordedFlightList
&flight_list
,
901 OperationEnvironment
&env
)
904 assert(port
!= NULL
);
905 assert(driver
!= NULL
);
906 assert(device
!= NULL
);
908 StaticString
<60> text
;
909 text
.Format(_T("%s: %s."), _("Reading flight list"), driver
->display_name
);
912 return device
->ReadFlightList(flight_list
, env
);
916 DeviceDescriptor::DownloadFlight(const RecordedFlightInfo
&flight
,
918 OperationEnvironment
&env
)
921 assert(port
!= NULL
);
922 assert(driver
!= NULL
);
923 assert(device
!= NULL
);
925 if (port
== NULL
|| driver
== NULL
|| device
== NULL
)
928 StaticString
<60> text
;
929 text
.Format(_T("%s: %s."), _("Downloading flight log"), driver
->display_name
);
932 return device
->DownloadFlight(flight
, path
, env
);
936 DeviceDescriptor::OnSysTicker()
938 assert(InMainThread());
940 if (port
!= NULL
&& port
->GetState() == PortState::FAILED
&& !IsOccupied())
946 const bool now_alive
= IsAlive();
947 if (!now_alive
&& was_alive
&& !IsOccupied()) {
948 /* connection was just lost */
949 device
->LinkTimeout();
951 NullOperationEnvironment env
;
955 was_alive
= now_alive
;
957 if (now_alive
|| IsBorrowed()) {
960 // write settings to vario every second
961 device
->OnSysTicker();
966 DeviceDescriptor::OnSensorUpdate(const MoreData
&basic
)
968 /* must hold the mutex because this method may run in any thread,
969 just in case the main thread deletes the Device while this method
971 const ScopeLock
protect(mutex
);
973 if (device
!= nullptr)
974 device
->OnSensorUpdate(basic
);
978 DeviceDescriptor::OnCalculatedUpdate(const MoreData
&basic
,
979 const DerivedInfo
&calculated
)
981 assert(InMainThread());
983 if (device
!= nullptr)
984 device
->OnCalculatedUpdate(basic
, calculated
);
988 DeviceDescriptor::ParseLine(const char *line
)
990 ScopeLock
protect(device_blackboard
->mutex
);
991 NMEAInfo
&basic
= device_blackboard
->SetRealState(index
);
993 return ParseNMEA(line
, basic
);
997 DeviceDescriptor::OnNotification()
999 /* notification from AsyncJobRunner, the Job was finished */
1001 assert(InMainThread());
1002 assert(open_job
!= NULL
);
1011 DeviceDescriptor::DataReceived(const void *data
, size_t length
)
1013 if (monitor
!= NULL
)
1014 monitor
->DataReceived(data
, length
);
1016 // Pass data directly to drivers that use binary data protocols
1017 if (driver
!= NULL
&& device
!= NULL
&& driver
->UsesRawData()) {
1018 ScopeLock
protect(device_blackboard
->mutex
);
1019 NMEAInfo
&basic
= device_blackboard
->SetRealState(index
);
1020 basic
.UpdateClock();
1022 const ExternalSettings old_settings
= basic
.settings
;
1024 if (device
->DataReceived(data
, length
, basic
)) {
1025 if (!config
.sync_from_device
)
1026 basic
.settings
= old_settings
;
1028 device_blackboard
->ScheduleMerge();
1035 PortLineSplitter::DataReceived(data
, length
);
1039 DeviceDescriptor::LineReceived(const char *line
)
1041 NMEALogger::Log(line
);
1043 if (dispatcher
!= NULL
)
1044 dispatcher
->LineReceived(line
);
1046 if (ParseLine(line
))
1047 device_blackboard
->ScheduleMerge();