aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/traccar
diff options
context:
space:
mode:
authorAnton Tananaev <anton.tananaev@gmail.com>2019-03-31 22:35:39 -0700
committerAnton Tananaev <anton.tananaev@gmail.com>2019-03-31 22:35:39 -0700
commit59416923dcb3a756eaf532cc4259f2f6625c0762 (patch)
tree9082dae6616deac8fda432b7bfd80e4a52b6d9dc /src/main/java/org/traccar
parent79a129dd6327d932133d6b9a50190d3f4927bff9 (diff)
downloadtraccar-server-59416923dcb3a756eaf532cc4259f2f6625c0762.tar.gz
traccar-server-59416923dcb3a756eaf532cc4259f2f6625c0762.tar.bz2
traccar-server-59416923dcb3a756eaf532cc4259f2f6625c0762.zip
Convert project to gradle
Diffstat (limited to 'src/main/java/org/traccar')
-rw-r--r--src/main/java/org/traccar/BaseDataHandler.java38
-rw-r--r--src/main/java/org/traccar/BaseFrameDecoder.java37
-rw-r--r--src/main/java/org/traccar/BaseHttpProtocolDecoder.java39
-rw-r--r--src/main/java/org/traccar/BasePipelineFactory.java169
-rw-r--r--src/main/java/org/traccar/BaseProtocol.java131
-rw-r--r--src/main/java/org/traccar/BaseProtocolDecoder.java255
-rw-r--r--src/main/java/org/traccar/BaseProtocolEncoder.java85
-rw-r--r--src/main/java/org/traccar/CharacterDelimiterFrameDecoder.java65
-rw-r--r--src/main/java/org/traccar/Context.java410
-rw-r--r--src/main/java/org/traccar/DeviceSession.java42
-rw-r--r--src/main/java/org/traccar/EventLoopGroupFactory.java37
-rw-r--r--src/main/java/org/traccar/ExtendedObjectDecoder.java82
-rw-r--r--src/main/java/org/traccar/GlobalTimer.java42
-rw-r--r--src/main/java/org/traccar/Main.java156
-rw-r--r--src/main/java/org/traccar/MainEventHandler.java161
-rw-r--r--src/main/java/org/traccar/MainModule.java373
-rw-r--r--src/main/java/org/traccar/NetworkMessage.java38
-rw-r--r--src/main/java/org/traccar/PipelineBuilder.java24
-rw-r--r--src/main/java/org/traccar/Protocol.java37
-rw-r--r--src/main/java/org/traccar/ServerManager.java103
-rw-r--r--src/main/java/org/traccar/StringProtocolEncoder.java51
-rw-r--r--src/main/java/org/traccar/TrackerServer.java115
-rw-r--r--src/main/java/org/traccar/WebDataHandler.java201
-rw-r--r--src/main/java/org/traccar/WindowsService.java231
-rw-r--r--src/main/java/org/traccar/WrapperContext.java255
-rw-r--r--src/main/java/org/traccar/WrapperInboundHandler.java94
-rw-r--r--src/main/java/org/traccar/WrapperOutboundHandler.java99
-rw-r--r--src/main/java/org/traccar/api/AsyncSocket.java96
-rw-r--r--src/main/java/org/traccar/api/AsyncSocketServlet.java46
-rw-r--r--src/main/java/org/traccar/api/BaseObjectResource.java176
-rw-r--r--src/main/java/org/traccar/api/BaseResource.java32
-rw-r--r--src/main/java/org/traccar/api/CorsResponseFilter.java58
-rw-r--r--src/main/java/org/traccar/api/ExtendedObjectResource.java62
-rw-r--r--src/main/java/org/traccar/api/MediaFilter.java92
-rw-r--r--src/main/java/org/traccar/api/ObjectMapperProvider.java32
-rw-r--r--src/main/java/org/traccar/api/ResourceErrorHandler.java42
-rw-r--r--src/main/java/org/traccar/api/SecurityRequestFilter.java119
-rw-r--r--src/main/java/org/traccar/api/SimpleObjectResource.java43
-rw-r--r--src/main/java/org/traccar/api/UserPrincipal.java37
-rw-r--r--src/main/java/org/traccar/api/UserSecurityContext.java49
-rw-r--r--src/main/java/org/traccar/api/resource/AttributeResource.java97
-rw-r--r--src/main/java/org/traccar/api/resource/CalendarResource.java36
-rw-r--r--src/main/java/org/traccar/api/resource/CommandResource.java89
-rw-r--r--src/main/java/org/traccar/api/resource/DeviceResource.java100
-rw-r--r--src/main/java/org/traccar/api/resource/DriverResource.java36
-rw-r--r--src/main/java/org/traccar/api/resource/EventResource.java38
-rw-r--r--src/main/java/org/traccar/api/resource/GeofenceResource.java35
-rw-r--r--src/main/java/org/traccar/api/resource/GroupResource.java35
-rw-r--r--src/main/java/org/traccar/api/resource/MaintenanceResource.java36
-rw-r--r--src/main/java/org/traccar/api/resource/NotificationResource.java76
-rw-r--r--src/main/java/org/traccar/api/resource/PermissionsResource.java84
-rw-r--r--src/main/java/org/traccar/api/resource/PositionResource.java96
-rw-r--r--src/main/java/org/traccar/api/resource/ReportResource.java210
-rw-r--r--src/main/java/org/traccar/api/resource/ServerResource.java63
-rw-r--r--src/main/java/org/traccar/api/resource/SessionResource.java120
-rw-r--r--src/main/java/org/traccar/api/resource/StatisticsResource.java44
-rw-r--r--src/main/java/org/traccar/api/resource/UserResource.java94
-rw-r--r--src/main/java/org/traccar/config/Config.java166
-rw-r--r--src/main/java/org/traccar/config/ConfigKey.java36
-rw-r--r--src/main/java/org/traccar/config/ConfigSuffix.java28
-rw-r--r--src/main/java/org/traccar/config/Keys.java356
-rw-r--r--src/main/java/org/traccar/database/ActiveDevice.java55
-rw-r--r--src/main/java/org/traccar/database/AttributesManager.java36
-rw-r--r--src/main/java/org/traccar/database/BaseObjectManager.java127
-rw-r--r--src/main/java/org/traccar/database/CalendarManager.java27
-rw-r--r--src/main/java/org/traccar/database/CommandsManager.java157
-rw-r--r--src/main/java/org/traccar/database/ConnectionManager.java218
-rw-r--r--src/main/java/org/traccar/database/DataManager.java478
-rw-r--r--src/main/java/org/traccar/database/DeviceManager.java419
-rw-r--r--src/main/java/org/traccar/database/DriversManager.java73
-rw-r--r--src/main/java/org/traccar/database/ExtendedObjectManager.java115
-rw-r--r--src/main/java/org/traccar/database/GeofenceManager.java66
-rw-r--r--src/main/java/org/traccar/database/GroupTree.java151
-rw-r--r--src/main/java/org/traccar/database/GroupsManager.java106
-rw-r--r--src/main/java/org/traccar/database/IdentityManager.java43
-rw-r--r--src/main/java/org/traccar/database/LdapProvider.java179
-rw-r--r--src/main/java/org/traccar/database/MailManager.java147
-rw-r--r--src/main/java/org/traccar/database/MaintenancesManager.java27
-rw-r--r--src/main/java/org/traccar/database/ManagableObjects.java27
-rw-r--r--src/main/java/org/traccar/database/MediaManager.java72
-rw-r--r--src/main/java/org/traccar/database/NotificationManager.java135
-rw-r--r--src/main/java/org/traccar/database/PermissionsManager.java459
-rw-r--r--src/main/java/org/traccar/database/QueryBuilder.java519
-rw-r--r--src/main/java/org/traccar/database/QueryExtended.java27
-rw-r--r--src/main/java/org/traccar/database/QueryIgnore.java26
-rw-r--r--src/main/java/org/traccar/database/SimpleObjectManager.java94
-rw-r--r--src/main/java/org/traccar/database/StatisticsManager.java160
-rw-r--r--src/main/java/org/traccar/database/UsersManager.java86
-rw-r--r--src/main/java/org/traccar/geocoder/Address.java110
-rw-r--r--src/main/java/org/traccar/geocoder/AddressFormat.java82
-rw-r--r--src/main/java/org/traccar/geocoder/BanGeocoder.java66
-rw-r--r--src/main/java/org/traccar/geocoder/BingMapsGeocoder.java63
-rw-r--r--src/main/java/org/traccar/geocoder/FactualGeocoder.java58
-rw-r--r--src/main/java/org/traccar/geocoder/GeocodeFarmGeocoder.java70
-rw-r--r--src/main/java/org/traccar/geocoder/GeocodeXyzGeocoder.java60
-rw-r--r--src/main/java/org/traccar/geocoder/Geocoder.java30
-rw-r--r--src/main/java/org/traccar/geocoder/GeocoderException.java24
-rw-r--r--src/main/java/org/traccar/geocoder/GisgraphyGeocoder.java55
-rw-r--r--src/main/java/org/traccar/geocoder/GoogleGeocoder.java98
-rw-r--r--src/main/java/org/traccar/geocoder/HereGeocoder.java84
-rw-r--r--src/main/java/org/traccar/geocoder/JsonGeocoder.java121
-rw-r--r--src/main/java/org/traccar/geocoder/MapQuestGeocoder.java63
-rw-r--r--src/main/java/org/traccar/geocoder/MapmyIndiaGeocoder.java82
-rw-r--r--src/main/java/org/traccar/geocoder/NominatimGeocoder.java91
-rw-r--r--src/main/java/org/traccar/geocoder/OpenCageGeocoder.java76
-rw-r--r--src/main/java/org/traccar/geofence/GeofenceCircle.java98
-rw-r--r--src/main/java/org/traccar/geofence/GeofenceGeometry.java50
-rw-r--r--src/main/java/org/traccar/geofence/GeofencePolygon.java164
-rw-r--r--src/main/java/org/traccar/geofence/GeofencePolyline.java107
-rw-r--r--src/main/java/org/traccar/geolocation/GeolocationException.java24
-rw-r--r--src/main/java/org/traccar/geolocation/GeolocationProvider.java32
-rw-r--r--src/main/java/org/traccar/geolocation/GoogleGeolocationProvider.java26
-rw-r--r--src/main/java/org/traccar/geolocation/MozillaGeolocationProvider.java26
-rw-r--r--src/main/java/org/traccar/geolocation/OpenCellIdGeolocationProvider.java68
-rw-r--r--src/main/java/org/traccar/geolocation/UniversalGeolocationProvider.java58
-rw-r--r--src/main/java/org/traccar/geolocation/UnwiredGeolocationProvider.java111
-rw-r--r--src/main/java/org/traccar/handler/ComputedAttributesHandler.java140
-rw-r--r--src/main/java/org/traccar/handler/CopyAttributesHandler.java53
-rw-r--r--src/main/java/org/traccar/handler/DefaultDataHandler.java48
-rw-r--r--src/main/java/org/traccar/handler/DistanceHandler.java82
-rw-r--r--src/main/java/org/traccar/handler/EngineHoursHandler.java50
-rw-r--r--src/main/java/org/traccar/handler/FilterHandler.java206
-rw-r--r--src/main/java/org/traccar/handler/GeocoderHandler.java94
-rw-r--r--src/main/java/org/traccar/handler/GeolocationHandler.java86
-rw-r--r--src/main/java/org/traccar/handler/HemisphereHandler.java60
-rw-r--r--src/main/java/org/traccar/handler/MotionHandler.java40
-rw-r--r--src/main/java/org/traccar/handler/NetworkMessageHandler.java57
-rw-r--r--src/main/java/org/traccar/handler/OpenChannelHandler.java42
-rw-r--r--src/main/java/org/traccar/handler/RemoteAddressHandler.java42
-rw-r--r--src/main/java/org/traccar/handler/StandardLoggingHandler.java81
-rw-r--r--src/main/java/org/traccar/handler/events/AlertEventHandler.java59
-rw-r--r--src/main/java/org/traccar/handler/events/BaseEventHandler.java38
-rw-r--r--src/main/java/org/traccar/handler/events/CommandResultEventHandler.java39
-rw-r--r--src/main/java/org/traccar/handler/events/DriverEventHandler.java57
-rw-r--r--src/main/java/org/traccar/handler/events/FuelDropEventHandler.java70
-rw-r--r--src/main/java/org/traccar/handler/events/GeofenceEventHandler.java89
-rw-r--r--src/main/java/org/traccar/handler/events/IgnitionEventHandler.java65
-rw-r--r--src/main/java/org/traccar/handler/events/MaintenanceEventHandler.java72
-rw-r--r--src/main/java/org/traccar/handler/events/MotionEventHandler.java135
-rw-r--r--src/main/java/org/traccar/handler/events/OverspeedEventHandler.java164
-rw-r--r--src/main/java/org/traccar/helper/BcdUtil.java63
-rw-r--r--src/main/java/org/traccar/helper/BitBuffer.java101
-rw-r--r--src/main/java/org/traccar/helper/BitUtil.java51
-rw-r--r--src/main/java/org/traccar/helper/BufferUtil.java47
-rw-r--r--src/main/java/org/traccar/helper/Checksum.java200
-rw-r--r--src/main/java/org/traccar/helper/DataConverter.java47
-rw-r--r--src/main/java/org/traccar/helper/DateBuilder.java126
-rw-r--r--src/main/java/org/traccar/helper/DateUtil.java78
-rw-r--r--src/main/java/org/traccar/helper/DistanceCalculator.java53
-rw-r--r--src/main/java/org/traccar/helper/Hashing.java100
-rw-r--r--src/main/java/org/traccar/helper/LocationTree.java125
-rw-r--r--src/main/java/org/traccar/helper/Log.java265
-rw-r--r--src/main/java/org/traccar/helper/LogAction.java99
-rw-r--r--src/main/java/org/traccar/helper/ObdDecoder.java105
-rw-r--r--src/main/java/org/traccar/helper/Parser.java348
-rw-r--r--src/main/java/org/traccar/helper/PatternBuilder.java98
-rw-r--r--src/main/java/org/traccar/helper/PatternUtil.java81
-rw-r--r--src/main/java/org/traccar/helper/SanitizerModule.java45
-rw-r--r--src/main/java/org/traccar/helper/UnitsConverter.java88
-rw-r--r--src/main/java/org/traccar/model/Attribute.java61
-rw-r--r--src/main/java/org/traccar/model/BaseModel.java31
-rw-r--r--src/main/java/org/traccar/model/Calendar.java82
-rw-r--r--src/main/java/org/traccar/model/CellTower.java115
-rw-r--r--src/main/java/org/traccar/model/Command.java113
-rw-r--r--src/main/java/org/traccar/model/Device.java152
-rw-r--r--src/main/java/org/traccar/model/DeviceAccumulators.java51
-rw-r--r--src/main/java/org/traccar/model/DeviceState.java71
-rw-r--r--src/main/java/org/traccar/model/Driver.java40
-rw-r--r--src/main/java/org/traccar/model/Event.java104
-rw-r--r--src/main/java/org/traccar/model/ExtendedModel.java127
-rw-r--r--src/main/java/org/traccar/model/Geofence.java92
-rw-r--r--src/main/java/org/traccar/model/Group.java30
-rw-r--r--src/main/java/org/traccar/model/GroupedModel.java31
-rw-r--r--src/main/java/org/traccar/model/Maintenance.java61
-rw-r--r--src/main/java/org/traccar/model/ManagedUser.java21
-rw-r--r--src/main/java/org/traccar/model/Message.java40
-rw-r--r--src/main/java/org/traccar/model/MiscFormatter.java56
-rw-r--r--src/main/java/org/traccar/model/Network.java117
-rw-r--r--src/main/java/org/traccar/model/Notification.java72
-rw-r--r--src/main/java/org/traccar/model/Permission.java57
-rw-r--r--src/main/java/org/traccar/model/Position.java302
-rw-r--r--src/main/java/org/traccar/model/ScheduledModel.java30
-rw-r--r--src/main/java/org/traccar/model/Server.java169
-rw-r--r--src/main/java/org/traccar/model/Statistics.java122
-rw-r--r--src/main/java/org/traccar/model/Typed.java33
-rw-r--r--src/main/java/org/traccar/model/User.java276
-rw-r--r--src/main/java/org/traccar/model/WifiAccessPoint.java66
-rw-r--r--src/main/java/org/traccar/notification/EventForwarder.java91
-rw-r--r--src/main/java/org/traccar/notification/FullMessage.java36
-rw-r--r--src/main/java/org/traccar/notification/JsonTypeEventForwarder.java18
-rw-r--r--src/main/java/org/traccar/notification/MessageException.java29
-rw-r--r--src/main/java/org/traccar/notification/NotificationFormatter.java118
-rw-r--r--src/main/java/org/traccar/notification/NotificatorManager.java90
-rw-r--r--src/main/java/org/traccar/notification/PropertiesProvider.java81
-rw-r--r--src/main/java/org/traccar/notificators/Notificator.java44
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorFirebase.java87
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorMail.java40
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorNull.java38
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorSms.java62
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorWeb.java30
-rw-r--r--src/main/java/org/traccar/protocol/AdmProtocol.java44
-rw-r--r--src/main/java/org/traccar/protocol/AdmProtocolDecoder.java198
-rw-r--r--src/main/java/org/traccar/protocol/AdmProtocolEncoder.java39
-rw-r--r--src/main/java/org/traccar/protocol/AisProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/AisProtocolDecoder.java140
-rw-r--r--src/main/java/org/traccar/protocol/AlematicsFrameDecoder.java50
-rw-r--r--src/main/java/org/traccar/protocol/AlematicsProtocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/AlematicsProtocolDecoder.java157
-rw-r--r--src/main/java/org/traccar/protocol/AnytrekProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/AnytrekProtocolDecoder.java120
-rw-r--r--src/main/java/org/traccar/protocol/ApelProtocol.java36
-rw-r--r--src/main/java/org/traccar/protocol/ApelProtocolDecoder.java210
-rw-r--r--src/main/java/org/traccar/protocol/AplicomFrameDecoder.java62
-rw-r--r--src/main/java/org/traccar/protocol/AplicomProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/AplicomProtocolDecoder.java702
-rw-r--r--src/main/java/org/traccar/protocol/AppelloProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/AppelloProtocolDecoder.java92
-rw-r--r--src/main/java/org/traccar/protocol/AppletProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/AppletProtocolDecoder.java48
-rw-r--r--src/main/java/org/traccar/protocol/AquilaProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/AquilaProtocolDecoder.java403
-rw-r--r--src/main/java/org/traccar/protocol/Ardi01Protocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/Ardi01ProtocolDecoder.java87
-rw-r--r--src/main/java/org/traccar/protocol/ArknavProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/ArknavProtocolDecoder.java83
-rw-r--r--src/main/java/org/traccar/protocol/ArknavX8Protocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/ArknavX8ProtocolDecoder.java139
-rw-r--r--src/main/java/org/traccar/protocol/ArnaviProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/ArnaviProtocolDecoder.java105
-rw-r--r--src/main/java/org/traccar/protocol/AstraProtocol.java41
-rw-r--r--src/main/java/org/traccar/protocol/AstraProtocolDecoder.java129
-rw-r--r--src/main/java/org/traccar/protocol/At2000FrameDecoder.java81
-rw-r--r--src/main/java/org/traccar/protocol/At2000Protocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/At2000ProtocolDecoder.java171
-rw-r--r--src/main/java/org/traccar/protocol/AtrackFrameDecoder.java80
-rw-r--r--src/main/java/org/traccar/protocol/AtrackProtocol.java45
-rw-r--r--src/main/java/org/traccar/protocol/AtrackProtocolDecoder.java586
-rw-r--r--src/main/java/org/traccar/protocol/AtrackProtocolEncoder.java38
-rw-r--r--src/main/java/org/traccar/protocol/AuroProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/AuroProtocolDecoder.java91
-rw-r--r--src/main/java/org/traccar/protocol/AustinNbProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/AustinNbProtocolDecoder.java81
-rw-r--r--src/main/java/org/traccar/protocol/AutoFonFrameDecoder.java65
-rw-r--r--src/main/java/org/traccar/protocol/AutoFonProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/AutoFonProtocolDecoder.java215
-rw-r--r--src/main/java/org/traccar/protocol/AutoGradeProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/AutoGradeProtocolDecoder.java107
-rw-r--r--src/main/java/org/traccar/protocol/AutoTrackProtocol.java36
-rw-r--r--src/main/java/org/traccar/protocol/AutoTrackProtocolDecoder.java138
-rw-r--r--src/main/java/org/traccar/protocol/AvemaProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/AvemaProtocolDecoder.java107
-rw-r--r--src/main/java/org/traccar/protocol/Avl301Protocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/Avl301ProtocolDecoder.java145
-rw-r--r--src/main/java/org/traccar/protocol/BceFrameDecoder.java59
-rw-r--r--src/main/java/org/traccar/protocol/BceProtocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/BceProtocolDecoder.java175
-rw-r--r--src/main/java/org/traccar/protocol/BceProtocolEncoder.java47
-rw-r--r--src/main/java/org/traccar/protocol/BlackKiteProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/BlackKiteProtocolDecoder.java195
-rw-r--r--src/main/java/org/traccar/protocol/BoxProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/BoxProtocolDecoder.java108
-rw-r--r--src/main/java/org/traccar/protocol/C2stekProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/C2stekProtocolDecoder.java121
-rw-r--r--src/main/java/org/traccar/protocol/CalAmpProtocol.java33
-rw-r--r--src/main/java/org/traccar/protocol/CalAmpProtocolDecoder.java202
-rw-r--r--src/main/java/org/traccar/protocol/CarTrackProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/CarTrackProtocolDecoder.java108
-rw-r--r--src/main/java/org/traccar/protocol/CarcellProtocol.java44
-rw-r--r--src/main/java/org/traccar/protocol/CarcellProtocolDecoder.java164
-rw-r--r--src/main/java/org/traccar/protocol/CarcellProtocolEncoder.java36
-rw-r--r--src/main/java/org/traccar/protocol/CarscopProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/CarscopProtocolDecoder.java101
-rw-r--r--src/main/java/org/traccar/protocol/CastelProtocol.java48
-rw-r--r--src/main/java/org/traccar/protocol/CastelProtocolDecoder.java573
-rw-r--r--src/main/java/org/traccar/protocol/CastelProtocolEncoder.java70
-rw-r--r--src/main/java/org/traccar/protocol/CautelaProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/CautelaProtocolDecoder.java78
-rw-r--r--src/main/java/org/traccar/protocol/CellocatorFrameDecoder.java66
-rw-r--r--src/main/java/org/traccar/protocol/CellocatorProtocol.java45
-rw-r--r--src/main/java/org/traccar/protocol/CellocatorProtocolDecoder.java177
-rw-r--r--src/main/java/org/traccar/protocol/CellocatorProtocolEncoder.java66
-rw-r--r--src/main/java/org/traccar/protocol/CguardProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/CguardProtocolDecoder.java147
-rw-r--r--src/main/java/org/traccar/protocol/CityeasyProtocol.java42
-rw-r--r--src/main/java/org/traccar/protocol/CityeasyProtocolDecoder.java127
-rw-r--r--src/main/java/org/traccar/protocol/CityeasyProtocolEncoder.java73
-rw-r--r--src/main/java/org/traccar/protocol/ContinentalProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/ContinentalProtocolDecoder.java114
-rw-r--r--src/main/java/org/traccar/protocol/CradlepointProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/CradlepointProtocolDecoder.java95
-rw-r--r--src/main/java/org/traccar/protocol/DishaProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/DishaProtocolDecoder.java102
-rw-r--r--src/main/java/org/traccar/protocol/DmtHttpProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/DmtHttpProtocolDecoder.java133
-rw-r--r--src/main/java/org/traccar/protocol/DmtProtocol.java36
-rw-r--r--src/main/java/org/traccar/protocol/DmtProtocolDecoder.java289
-rw-r--r--src/main/java/org/traccar/protocol/DwayProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/DwayProtocolDecoder.java103
-rw-r--r--src/main/java/org/traccar/protocol/EasyTrackProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/EasyTrackProtocolDecoder.java138
-rw-r--r--src/main/java/org/traccar/protocol/EelinkProtocol.java50
-rw-r--r--src/main/java/org/traccar/protocol/EelinkProtocolDecoder.java441
-rw-r--r--src/main/java/org/traccar/protocol/EelinkProtocolEncoder.java107
-rw-r--r--src/main/java/org/traccar/protocol/EgtsFrameDecoder.java45
-rw-r--r--src/main/java/org/traccar/protocol/EgtsProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/EgtsProtocolDecoder.java265
-rw-r--r--src/main/java/org/traccar/protocol/EnforaProtocol.java48
-rw-r--r--src/main/java/org/traccar/protocol/EnforaProtocolDecoder.java115
-rw-r--r--src/main/java/org/traccar/protocol/EnforaProtocolEncoder.java55
-rw-r--r--src/main/java/org/traccar/protocol/EsealProtocol.java45
-rw-r--r--src/main/java/org/traccar/protocol/EsealProtocolDecoder.java158
-rw-r--r--src/main/java/org/traccar/protocol/EsealProtocolEncoder.java41
-rw-r--r--src/main/java/org/traccar/protocol/EskyFrameDecoder.java39
-rw-r--r--src/main/java/org/traccar/protocol/EskyProtocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/EskyProtocolDecoder.java94
-rw-r--r--src/main/java/org/traccar/protocol/ExtremTracProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/ExtremTracProtocolDecoder.java83
-rw-r--r--src/main/java/org/traccar/protocol/FifotrackProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/FifotrackProtocolDecoder.java198
-rw-r--r--src/main/java/org/traccar/protocol/FlespiProtocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/FlespiProtocolDecoder.java246
-rw-r--r--src/main/java/org/traccar/protocol/FlexCommProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/FlexCommProtocolDecoder.java128
-rw-r--r--src/main/java/org/traccar/protocol/FlextrackProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/FlextrackProtocolDecoder.java144
-rw-r--r--src/main/java/org/traccar/protocol/FoxProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/FoxProtocolDecoder.java124
-rw-r--r--src/main/java/org/traccar/protocol/FreedomProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/FreedomProtocolDecoder.java77
-rw-r--r--src/main/java/org/traccar/protocol/FreematicsProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/FreematicsProtocolDecoder.java184
-rw-r--r--src/main/java/org/traccar/protocol/GalileoFrameDecoder.java43
-rw-r--r--src/main/java/org/traccar/protocol/GalileoProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/GalileoProtocolDecoder.java342
-rw-r--r--src/main/java/org/traccar/protocol/GalileoProtocolEncoder.java67
-rw-r--r--src/main/java/org/traccar/protocol/GatorProtocol.java41
-rw-r--r--src/main/java/org/traccar/protocol/GatorProtocolDecoder.java133
-rw-r--r--src/main/java/org/traccar/protocol/GenxProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/GenxProtocolDecoder.java99
-rw-r--r--src/main/java/org/traccar/protocol/Gl100Protocol.java47
-rw-r--r--src/main/java/org/traccar/protocol/Gl100ProtocolDecoder.java97
-rw-r--r--src/main/java/org/traccar/protocol/Gl200BinaryProtocolDecoder.java403
-rw-r--r--src/main/java/org/traccar/protocol/Gl200FrameDecoder.java97
-rw-r--r--src/main/java/org/traccar/protocol/Gl200Protocol.java53
-rw-r--r--src/main/java/org/traccar/protocol/Gl200ProtocolDecoder.java50
-rw-r--r--src/main/java/org/traccar/protocol/Gl200ProtocolEncoder.java46
-rw-r--r--src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java1266
-rw-r--r--src/main/java/org/traccar/protocol/GlobalSatProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/GlobalSatProtocolDecoder.java248
-rw-r--r--src/main/java/org/traccar/protocol/GnxProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/GnxProtocolDecoder.java111
-rw-r--r--src/main/java/org/traccar/protocol/GoSafeProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/GoSafeProtocolDecoder.java269
-rw-r--r--src/main/java/org/traccar/protocol/GotopProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/GotopProtocolDecoder.java82
-rw-r--r--src/main/java/org/traccar/protocol/Gps056FrameDecoder.java47
-rw-r--r--src/main/java/org/traccar/protocol/Gps056Protocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/Gps056ProtocolDecoder.java140
-rw-r--r--src/main/java/org/traccar/protocol/Gps103Protocol.java60
-rw-r--r--src/main/java/org/traccar/protocol/Gps103ProtocolDecoder.java422
-rw-r--r--src/main/java/org/traccar/protocol/Gps103ProtocolEncoder.java68
-rw-r--r--src/main/java/org/traccar/protocol/GpsGateProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/GpsGateProtocolDecoder.java170
-rw-r--r--src/main/java/org/traccar/protocol/GpsMarkerProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/GpsMarkerProtocolDecoder.java90
-rw-r--r--src/main/java/org/traccar/protocol/GpsmtaProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/GpsmtaProtocolDecoder.java92
-rw-r--r--src/main/java/org/traccar/protocol/GranitFrameDecoder.java47
-rw-r--r--src/main/java/org/traccar/protocol/GranitProtocol.java45
-rw-r--r--src/main/java/org/traccar/protocol/GranitProtocolDecoder.java239
-rw-r--r--src/main/java/org/traccar/protocol/GranitProtocolEncoder.java48
-rw-r--r--src/main/java/org/traccar/protocol/GranitProtocolSmsEncoder.java36
-rw-r--r--src/main/java/org/traccar/protocol/Gt02Protocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/Gt02ProtocolDecoder.java125
-rw-r--r--src/main/java/org/traccar/protocol/Gt06FrameDecoder.java56
-rw-r--r--src/main/java/org/traccar/protocol/Gt06Protocol.java40
-rw-r--r--src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java929
-rw-r--r--src/main/java/org/traccar/protocol/Gt06ProtocolEncoder.java78
-rw-r--r--src/main/java/org/traccar/protocol/Gt30Protocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/Gt30ProtocolDecoder.java114
-rw-r--r--src/main/java/org/traccar/protocol/H02FrameDecoder.java95
-rw-r--r--src/main/java/org/traccar/protocol/H02Protocol.java54
-rw-r--r--src/main/java/org/traccar/protocol/H02ProtocolDecoder.java583
-rw-r--r--src/main/java/org/traccar/protocol/H02ProtocolEncoder.java73
-rw-r--r--src/main/java/org/traccar/protocol/HaicomProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/HaicomProtocolDecoder.java109
-rw-r--r--src/main/java/org/traccar/protocol/HomtecsProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/HomtecsProtocolDecoder.java91
-rw-r--r--src/main/java/org/traccar/protocol/HuaShengFrameDecoder.java58
-rw-r--r--src/main/java/org/traccar/protocol/HuaShengProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/HuaShengProtocolDecoder.java155
-rw-r--r--src/main/java/org/traccar/protocol/HuabaoFrameDecoder.java58
-rw-r--r--src/main/java/org/traccar/protocol/HuabaoProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java235
-rw-r--r--src/main/java/org/traccar/protocol/HuabaoProtocolEncoder.java73
-rw-r--r--src/main/java/org/traccar/protocol/HunterProProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/HunterProProtocolDecoder.java83
-rw-r--r--src/main/java/org/traccar/protocol/IdplProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/IdplProtocolDecoder.java112
-rw-r--r--src/main/java/org/traccar/protocol/IntellitracFrameDecoder.java52
-rw-r--r--src/main/java/org/traccar/protocol/IntellitracProtocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/IntellitracProtocolDecoder.java117
-rw-r--r--src/main/java/org/traccar/protocol/ItsProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/ItsProtocolDecoder.java170
-rw-r--r--src/main/java/org/traccar/protocol/Ivt401Protocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/Ivt401ProtocolDecoder.java183
-rw-r--r--src/main/java/org/traccar/protocol/JpKorjarFrameDecoder.java47
-rw-r--r--src/main/java/org/traccar/protocol/JpKorjarProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/JpKorjarProtocolDecoder.java89
-rw-r--r--src/main/java/org/traccar/protocol/Jt600FrameDecoder.java55
-rw-r--r--src/main/java/org/traccar/protocol/Jt600Protocol.java43
-rw-r--r--src/main/java/org/traccar/protocol/Jt600ProtocolDecoder.java375
-rw-r--r--src/main/java/org/traccar/protocol/Jt600ProtocolEncoder.java43
-rw-r--r--src/main/java/org/traccar/protocol/KenjiProtocol.java40
-rw-r--r--src/main/java/org/traccar/protocol/KenjiProtocolDecoder.java110
-rw-r--r--src/main/java/org/traccar/protocol/KhdProtocol.java40
-rw-r--r--src/main/java/org/traccar/protocol/KhdProtocolDecoder.java157
-rw-r--r--src/main/java/org/traccar/protocol/KhdProtocolEncoder.java67
-rw-r--r--src/main/java/org/traccar/protocol/L100FrameDecoder.java90
-rw-r--r--src/main/java/org/traccar/protocol/L100Protocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/L100ProtocolDecoder.java341
-rw-r--r--src/main/java/org/traccar/protocol/LaipacProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/LaipacProtocolDecoder.java167
-rw-r--r--src/main/java/org/traccar/protocol/M2cProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/M2cProtocolDecoder.java131
-rw-r--r--src/main/java/org/traccar/protocol/M2mProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/M2mProtocolDecoder.java127
-rw-r--r--src/main/java/org/traccar/protocol/MaestroProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/MaestroProtocolDecoder.java103
-rw-r--r--src/main/java/org/traccar/protocol/ManPowerProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/ManPowerProtocolDecoder.java81
-rw-r--r--src/main/java/org/traccar/protocol/MegastekFrameDecoder.java59
-rw-r--r--src/main/java/org/traccar/protocol/MegastekProtocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/MegastekProtocolDecoder.java419
-rw-r--r--src/main/java/org/traccar/protocol/MeiligaoFrameDecoder.java47
-rw-r--r--src/main/java/org/traccar/protocol/MeiligaoProtocol.java52
-rw-r--r--src/main/java/org/traccar/protocol/MeiligaoProtocolDecoder.java499
-rw-r--r--src/main/java/org/traccar/protocol/MeiligaoProtocolEncoder.java86
-rw-r--r--src/main/java/org/traccar/protocol/MeitrackFrameDecoder.java47
-rw-r--r--src/main/java/org/traccar/protocol/MeitrackProtocol.java54
-rw-r--r--src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java534
-rw-r--r--src/main/java/org/traccar/protocol/MeitrackProtocolEncoder.java65
-rw-r--r--src/main/java/org/traccar/protocol/MilesmateProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/MilesmateProtocolDecoder.java108
-rw-r--r--src/main/java/org/traccar/protocol/MiniFinderProtocol.java53
-rw-r--r--src/main/java/org/traccar/protocol/MiniFinderProtocolDecoder.java208
-rw-r--r--src/main/java/org/traccar/protocol/MiniFinderProtocolEncoder.java82
-rw-r--r--src/main/java/org/traccar/protocol/Mta6Protocol.java41
-rw-r--r--src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java319
-rw-r--r--src/main/java/org/traccar/protocol/MtxProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/MtxProtocolDecoder.java98
-rw-r--r--src/main/java/org/traccar/protocol/MxtFrameDecoder.java53
-rw-r--r--src/main/java/org/traccar/protocol/MxtProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/MxtProtocolDecoder.java175
-rw-r--r--src/main/java/org/traccar/protocol/NavigilFrameDecoder.java56
-rw-r--r--src/main/java/org/traccar/protocol/NavigilProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/NavigilProtocolDecoder.java308
-rw-r--r--src/main/java/org/traccar/protocol/NavisFrameDecoder.java109
-rw-r--r--src/main/java/org/traccar/protocol/NavisProtocol.java33
-rw-r--r--src/main/java/org/traccar/protocol/NavisProtocolDecoder.java683
-rw-r--r--src/main/java/org/traccar/protocol/NeosProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/NeosProtocolDecoder.java98
-rw-r--r--src/main/java/org/traccar/protocol/NoranProtocol.java41
-rw-r--r--src/main/java/org/traccar/protocol/NoranProtocolDecoder.java164
-rw-r--r--src/main/java/org/traccar/protocol/NoranProtocolEncoder.java64
-rw-r--r--src/main/java/org/traccar/protocol/NvsFrameDecoder.java47
-rw-r--r--src/main/java/org/traccar/protocol/NvsProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/NvsProtocolDecoder.java137
-rw-r--r--src/main/java/org/traccar/protocol/NyitechProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/NyitechProtocolDecoder.java123
-rw-r--r--src/main/java/org/traccar/protocol/ObdDongleProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/ObdDongleProtocolDecoder.java130
-rw-r--r--src/main/java/org/traccar/protocol/OigoProtocol.java33
-rw-r--r--src/main/java/org/traccar/protocol/OigoProtocolDecoder.java240
-rw-r--r--src/main/java/org/traccar/protocol/OkoProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/OkoProtocolDecoder.java100
-rw-r--r--src/main/java/org/traccar/protocol/OpenGtsProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/OpenGtsProtocolDecoder.java115
-rw-r--r--src/main/java/org/traccar/protocol/OrionFrameDecoder.java68
-rw-r--r--src/main/java/org/traccar/protocol/OrionProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/OrionProtocolDecoder.java115
-rw-r--r--src/main/java/org/traccar/protocol/OsmAndProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/OsmAndProtocolDecoder.java184
-rw-r--r--src/main/java/org/traccar/protocol/OwnTracksProtocol.java40
-rw-r--r--src/main/java/org/traccar/protocol/OwnTracksProtocolDecoder.java215
-rw-r--r--src/main/java/org/traccar/protocol/PathAwayProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/PathAwayProtocolDecoder.java97
-rw-r--r--src/main/java/org/traccar/protocol/PiligrimProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/PiligrimProtocolDecoder.java167
-rw-r--r--src/main/java/org/traccar/protocol/PretraceProtocol.java44
-rw-r--r--src/main/java/org/traccar/protocol/PretraceProtocolDecoder.java132
-rw-r--r--src/main/java/org/traccar/protocol/PretraceProtocolEncoder.java46
-rw-r--r--src/main/java/org/traccar/protocol/PricolProtocol.java41
-rw-r--r--src/main/java/org/traccar/protocol/PricolProtocolDecoder.java98
-rw-r--r--src/main/java/org/traccar/protocol/ProgressProtocol.java36
-rw-r--r--src/main/java/org/traccar/protocol/ProgressProtocolDecoder.java172
-rw-r--r--src/main/java/org/traccar/protocol/Pt3000Protocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/Pt3000ProtocolDecoder.java83
-rw-r--r--src/main/java/org/traccar/protocol/Pt502FrameDecoder.java73
-rw-r--r--src/main/java/org/traccar/protocol/Pt502Protocol.java44
-rw-r--r--src/main/java/org/traccar/protocol/Pt502ProtocolDecoder.java212
-rw-r--r--src/main/java/org/traccar/protocol/Pt502ProtocolEncoder.java58
-rw-r--r--src/main/java/org/traccar/protocol/Pt60Protocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/Pt60ProtocolDecoder.java184
-rw-r--r--src/main/java/org/traccar/protocol/RaveonProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/RaveonProtocolDecoder.java99
-rw-r--r--src/main/java/org/traccar/protocol/RecodaProtocol.java36
-rw-r--r--src/main/java/org/traccar/protocol/RecodaProtocolDecoder.java110
-rw-r--r--src/main/java/org/traccar/protocol/RetranslatorFrameDecoder.java37
-rw-r--r--src/main/java/org/traccar/protocol/RetranslatorProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/RetranslatorProtocolDecoder.java114
-rw-r--r--src/main/java/org/traccar/protocol/RitiProtocol.java36
-rw-r--r--src/main/java/org/traccar/protocol/RitiProtocolDecoder.java102
-rw-r--r--src/main/java/org/traccar/protocol/RoboTrackFrameDecoder.java57
-rw-r--r--src/main/java/org/traccar/protocol/RoboTrackProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/RoboTrackProtocolDecoder.java131
-rw-r--r--src/main/java/org/traccar/protocol/RuptelaProtocol.java45
-rw-r--r--src/main/java/org/traccar/protocol/RuptelaProtocolDecoder.java248
-rw-r--r--src/main/java/org/traccar/protocol/RuptelaProtocolEncoder.java73
-rw-r--r--src/main/java/org/traccar/protocol/SabertekFrameDecoder.java44
-rw-r--r--src/main/java/org/traccar/protocol/SabertekProtocol.java36
-rw-r--r--src/main/java/org/traccar/protocol/SabertekProtocolDecoder.java135
-rw-r--r--src/main/java/org/traccar/protocol/SanavProtocol.java45
-rw-r--r--src/main/java/org/traccar/protocol/SanavProtocolDecoder.java107
-rw-r--r--src/main/java/org/traccar/protocol/SatsolProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/SatsolProtocolDecoder.java104
-rw-r--r--src/main/java/org/traccar/protocol/SigfoxProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/SigfoxProtocolDecoder.java92
-rw-r--r--src/main/java/org/traccar/protocol/SiwiProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/SiwiProtocolDecoder.java96
-rw-r--r--src/main/java/org/traccar/protocol/SkypatrolProtocol.java33
-rw-r--r--src/main/java/org/traccar/protocol/SkypatrolProtocolDecoder.java193
-rw-r--r--src/main/java/org/traccar/protocol/SmartSoleProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/SmartSoleProtocolDecoder.java91
-rw-r--r--src/main/java/org/traccar/protocol/SmokeyProtocol.java33
-rw-r--r--src/main/java/org/traccar/protocol/SmokeyProtocolDecoder.java161
-rw-r--r--src/main/java/org/traccar/protocol/SpotProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/SpotProtocolDecoder.java102
-rw-r--r--src/main/java/org/traccar/protocol/StarLinkProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/StarLinkProtocolDecoder.java229
-rw-r--r--src/main/java/org/traccar/protocol/Stl060FrameDecoder.java48
-rw-r--r--src/main/java/org/traccar/protocol/Stl060Protocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/Stl060ProtocolDecoder.java120
-rw-r--r--src/main/java/org/traccar/protocol/SuntechProtocol.java49
-rw-r--r--src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java467
-rw-r--r--src/main/java/org/traccar/protocol/SuntechProtocolEncoder.java54
-rw-r--r--src/main/java/org/traccar/protocol/SupermateProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/SupermateProtocolDecoder.java122
-rw-r--r--src/main/java/org/traccar/protocol/SviasProtocol.java51
-rw-r--r--src/main/java/org/traccar/protocol/SviasProtocolDecoder.java105
-rw-r--r--src/main/java/org/traccar/protocol/SviasProtocolEncoder.java48
-rw-r--r--src/main/java/org/traccar/protocol/T55Protocol.java47
-rw-r--r--src/main/java/org/traccar/protocol/T55ProtocolDecoder.java283
-rw-r--r--src/main/java/org/traccar/protocol/T57FrameDecoder.java49
-rw-r--r--src/main/java/org/traccar/protocol/T57Protocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/T57ProtocolDecoder.java84
-rw-r--r--src/main/java/org/traccar/protocol/T800xProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/T800xProtocolDecoder.java199
-rw-r--r--src/main/java/org/traccar/protocol/T800xProtocolEncoder.java59
-rw-r--r--src/main/java/org/traccar/protocol/TaipProtocol.java47
-rw-r--r--src/main/java/org/traccar/protocol/TaipProtocolDecoder.java287
-rw-r--r--src/main/java/org/traccar/protocol/TekFrameDecoder.java42
-rw-r--r--src/main/java/org/traccar/protocol/TekProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/TekProtocolDecoder.java139
-rw-r--r--src/main/java/org/traccar/protocol/TelemaxProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/TelemaxProtocolDecoder.java112
-rw-r--r--src/main/java/org/traccar/protocol/TelicFrameDecoder.java55
-rw-r--r--src/main/java/org/traccar/protocol/TelicProtocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/TelicProtocolDecoder.java135
-rw-r--r--src/main/java/org/traccar/protocol/TeltonikaFrameDecoder.java53
-rw-r--r--src/main/java/org/traccar/protocol/TeltonikaProtocol.java45
-rw-r--r--src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java597
-rw-r--r--src/main/java/org/traccar/protocol/TeltonikaProtocolEncoder.java59
-rw-r--r--src/main/java/org/traccar/protocol/ThinkRaceProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/ThinkRaceProtocolDecoder.java116
-rw-r--r--src/main/java/org/traccar/protocol/Tk102Protocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/Tk102ProtocolDecoder.java144
-rw-r--r--src/main/java/org/traccar/protocol/Tk103FrameDecoder.java75
-rw-r--r--src/main/java/org/traccar/protocol/Tk103Protocol.java69
-rw-r--r--src/main/java/org/traccar/protocol/Tk103ProtocolDecoder.java453
-rw-r--r--src/main/java/org/traccar/protocol/Tk103ProtocolEncoder.java110
-rw-r--r--src/main/java/org/traccar/protocol/Tlt2hProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/Tlt2hProtocolDecoder.java152
-rw-r--r--src/main/java/org/traccar/protocol/TlvProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/TlvProtocolDecoder.java110
-rw-r--r--src/main/java/org/traccar/protocol/TmgFrameDecoder.java67
-rw-r--r--src/main/java/org/traccar/protocol/TmgProtocol.java38
-rw-r--r--src/main/java/org/traccar/protocol/TmgProtocolDecoder.java194
-rw-r--r--src/main/java/org/traccar/protocol/TopflytechProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/TopflytechProtocolDecoder.java76
-rw-r--r--src/main/java/org/traccar/protocol/TotemFrameDecoder.java59
-rw-r--r--src/main/java/org/traccar/protocol/TotemProtocol.java44
-rw-r--r--src/main/java/org/traccar/protocol/TotemProtocolDecoder.java444
-rw-r--r--src/main/java/org/traccar/protocol/TotemProtocolEncoder.java40
-rw-r--r--src/main/java/org/traccar/protocol/Tr20Protocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/Tr20ProtocolDecoder.java103
-rw-r--r--src/main/java/org/traccar/protocol/Tr900Protocol.java47
-rw-r--r--src/main/java/org/traccar/protocol/Tr900ProtocolDecoder.java94
-rw-r--r--src/main/java/org/traccar/protocol/TrackboxProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/TrackboxProtocolDecoder.java110
-rw-r--r--src/main/java/org/traccar/protocol/TrakMateProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/TrakMateProtocolDecoder.java233
-rw-r--r--src/main/java/org/traccar/protocol/TramigoFrameDecoder.java46
-rw-r--r--src/main/java/org/traccar/protocol/TramigoProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/TramigoProtocolDecoder.java160
-rw-r--r--src/main/java/org/traccar/protocol/TrvProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/TrvProtocolDecoder.java258
-rw-r--r--src/main/java/org/traccar/protocol/Tt8850Protocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/Tt8850ProtocolDecoder.java99
-rw-r--r--src/main/java/org/traccar/protocol/TytanProtocol.java33
-rw-r--r--src/main/java/org/traccar/protocol/TytanProtocolDecoder.java192
-rw-r--r--src/main/java/org/traccar/protocol/TzoneProtocol.java36
-rw-r--r--src/main/java/org/traccar/protocol/TzoneProtocolDecoder.java293
-rw-r--r--src/main/java/org/traccar/protocol/UlbotechFrameDecoder.java70
-rw-r--r--src/main/java/org/traccar/protocol/UlbotechProtocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/UlbotechProtocolDecoder.java371
-rw-r--r--src/main/java/org/traccar/protocol/UproProtocol.java37
-rw-r--r--src/main/java/org/traccar/protocol/UproProtocolDecoder.java212
-rw-r--r--src/main/java/org/traccar/protocol/V680Protocol.java47
-rw-r--r--src/main/java/org/traccar/protocol/V680ProtocolDecoder.java132
-rw-r--r--src/main/java/org/traccar/protocol/VisiontekProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/VisiontekProtocolDecoder.java138
-rw-r--r--src/main/java/org/traccar/protocol/Vt200FrameDecoder.java52
-rw-r--r--src/main/java/org/traccar/protocol/Vt200Protocol.java34
-rw-r--r--src/main/java/org/traccar/protocol/Vt200ProtocolDecoder.java150
-rw-r--r--src/main/java/org/traccar/protocol/VtfmsFrameDecoder.java41
-rw-r--r--src/main/java/org/traccar/protocol/VtfmsProtocol.java44
-rw-r--r--src/main/java/org/traccar/protocol/VtfmsProtocolDecoder.java167
-rw-r--r--src/main/java/org/traccar/protocol/WatchFrameDecoder.java67
-rw-r--r--src/main/java/org/traccar/protocol/WatchProtocol.java53
-rw-r--r--src/main/java/org/traccar/protocol/WatchProtocolDecoder.java329
-rw-r--r--src/main/java/org/traccar/protocol/WatchProtocolEncoder.java167
-rw-r--r--src/main/java/org/traccar/protocol/WialonProtocol.java54
-rw-r--r--src/main/java/org/traccar/protocol/WialonProtocolDecoder.java196
-rw-r--r--src/main/java/org/traccar/protocol/WialonProtocolEncoder.java40
-rw-r--r--src/main/java/org/traccar/protocol/WondexFrameDecoder.java62
-rw-r--r--src/main/java/org/traccar/protocol/WondexProtocol.java55
-rw-r--r--src/main/java/org/traccar/protocol/WondexProtocolDecoder.java128
-rw-r--r--src/main/java/org/traccar/protocol/WondexProtocolEncoder.java46
-rw-r--r--src/main/java/org/traccar/protocol/WristbandProtocol.java35
-rw-r--r--src/main/java/org/traccar/protocol/WristbandProtocolDecoder.java207
-rw-r--r--src/main/java/org/traccar/protocol/XexunFrameDecoder.java58
-rw-r--r--src/main/java/org/traccar/protocol/XexunProtocol.java51
-rw-r--r--src/main/java/org/traccar/protocol/XexunProtocolDecoder.java152
-rw-r--r--src/main/java/org/traccar/protocol/XexunProtocolEncoder.java38
-rw-r--r--src/main/java/org/traccar/protocol/XirgoProtocol.java53
-rw-r--r--src/main/java/org/traccar/protocol/XirgoProtocolDecoder.java355
-rw-r--r--src/main/java/org/traccar/protocol/XirgoProtocolEncoder.java34
-rw-r--r--src/main/java/org/traccar/protocol/Xrb28Protocol.java49
-rw-r--r--src/main/java/org/traccar/protocol/Xrb28ProtocolDecoder.java187
-rw-r--r--src/main/java/org/traccar/protocol/Xrb28ProtocolEncoder.java52
-rw-r--r--src/main/java/org/traccar/protocol/Xt013Protocol.java40
-rw-r--r--src/main/java/org/traccar/protocol/Xt013ProtocolDecoder.java94
-rw-r--r--src/main/java/org/traccar/protocol/Xt2400Protocol.java33
-rw-r--r--src/main/java/org/traccar/protocol/Xt2400ProtocolDecoder.java198
-rw-r--r--src/main/java/org/traccar/protocol/YwtProtocol.java39
-rw-r--r--src/main/java/org/traccar/protocol/YwtProtocolDecoder.java116
-rw-r--r--src/main/java/org/traccar/reports/Events.java133
-rw-r--r--src/main/java/org/traccar/reports/ReportUtils.java378
-rw-r--r--src/main/java/org/traccar/reports/Route.java85
-rw-r--r--src/main/java/org/traccar/reports/Stops.java102
-rw-r--r--src/main/java/org/traccar/reports/Summary.java117
-rw-r--r--src/main/java/org/traccar/reports/Trips.java100
-rw-r--r--src/main/java/org/traccar/reports/model/BaseReport.java106
-rw-r--r--src/main/java/org/traccar/reports/model/DeviceReport.java55
-rw-r--r--src/main/java/org/traccar/reports/model/StopReport.java106
-rw-r--r--src/main/java/org/traccar/reports/model/SummaryReport.java34
-rw-r--r--src/main/java/org/traccar/reports/model/TripReport.java152
-rw-r--r--src/main/java/org/traccar/reports/model/TripsConfig.java105
-rw-r--r--src/main/java/org/traccar/sms/HttpSmsClient.java110
-rw-r--r--src/main/java/org/traccar/sms/SmsManager.java29
-rw-r--r--src/main/java/org/traccar/sms/smpp/ClientSmppSessionHandler.java82
-rw-r--r--src/main/java/org/traccar/sms/smpp/EnquireLinkTask.java59
-rw-r--r--src/main/java/org/traccar/sms/smpp/ReconnectionTask.java32
-rw-r--r--src/main/java/org/traccar/sms/smpp/SmppClient.java272
-rw-r--r--src/main/java/org/traccar/sms/smpp/TextMessageEventHandler.java37
-rw-r--r--src/main/java/org/traccar/web/ConsoleServlet.java61
-rw-r--r--src/main/java/org/traccar/web/CsvBuilder.java164
-rw-r--r--src/main/java/org/traccar/web/GpxBuilder.java69
-rw-r--r--src/main/java/org/traccar/web/WebServer.java173
680 files changed, 70494 insertions, 0 deletions
diff --git a/src/main/java/org/traccar/BaseDataHandler.java b/src/main/java/org/traccar/BaseDataHandler.java
new file mode 100644
index 000000000..48794b0d7
--- /dev/null
+++ b/src/main/java/org/traccar/BaseDataHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import org.traccar.model.Position;
+
+public abstract class BaseDataHandler extends ChannelInboundHandlerAdapter {
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ if (msg instanceof Position) {
+ Position position = handlePosition((Position) msg);
+ if (position != null) {
+ ctx.fireChannelRead(position);
+ }
+ } else {
+ super.channelRead(ctx, msg);
+ }
+ }
+
+ protected abstract Position handlePosition(Position position);
+
+}
diff --git a/src/main/java/org/traccar/BaseFrameDecoder.java b/src/main/java/org/traccar/BaseFrameDecoder.java
new file mode 100644
index 000000000..f90f90e4b
--- /dev/null
+++ b/src/main/java/org/traccar/BaseFrameDecoder.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.ByteToMessageDecoder;
+
+import java.util.List;
+
+public abstract class BaseFrameDecoder extends ByteToMessageDecoder {
+
+ @Override
+ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+ Object decoded = decode(ctx, ctx != null ? ctx.channel() : null, in);
+ if (decoded != null) {
+ out.add(decoded);
+ }
+ }
+
+ protected abstract Object decode(ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception;
+
+}
diff --git a/src/main/java/org/traccar/BaseHttpProtocolDecoder.java b/src/main/java/org/traccar/BaseHttpProtocolDecoder.java
new file mode 100644
index 000000000..57a68acac
--- /dev/null
+++ b/src/main/java/org/traccar/BaseHttpProtocolDecoder.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+
+public abstract class BaseHttpProtocolDecoder extends BaseProtocolDecoder {
+
+ public BaseHttpProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public void sendResponse(Channel channel, HttpResponseStatus status) {
+ if (channel != null) {
+ HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status);
+ response.headers().add(HttpHeaderNames.CONTENT_LENGTH, 0);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/BasePipelineFactory.java b/src/main/java/org/traccar/BasePipelineFactory.java
new file mode 100644
index 000000000..b3d37f689
--- /dev/null
+++ b/src/main/java/org/traccar/BasePipelineFactory.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelInboundHandler;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOutboundHandler;
+import io.netty.channel.ChannelPipeline;
+import io.netty.handler.timeout.IdleStateHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Keys;
+import org.traccar.handler.DefaultDataHandler;
+import org.traccar.handler.events.AlertEventHandler;
+import org.traccar.handler.events.CommandResultEventHandler;
+import org.traccar.handler.events.DriverEventHandler;
+import org.traccar.handler.events.FuelDropEventHandler;
+import org.traccar.handler.events.GeofenceEventHandler;
+import org.traccar.handler.events.IgnitionEventHandler;
+import org.traccar.handler.events.MaintenanceEventHandler;
+import org.traccar.handler.events.MotionEventHandler;
+import org.traccar.handler.events.OverspeedEventHandler;
+import org.traccar.handler.ComputedAttributesHandler;
+import org.traccar.handler.CopyAttributesHandler;
+import org.traccar.handler.DistanceHandler;
+import org.traccar.handler.EngineHoursHandler;
+import org.traccar.handler.FilterHandler;
+import org.traccar.handler.GeocoderHandler;
+import org.traccar.handler.GeolocationHandler;
+import org.traccar.handler.HemisphereHandler;
+import org.traccar.handler.MotionHandler;
+import org.traccar.handler.NetworkMessageHandler;
+import org.traccar.handler.OpenChannelHandler;
+import org.traccar.handler.RemoteAddressHandler;
+import org.traccar.handler.StandardLoggingHandler;
+
+import java.util.Map;
+
+public abstract class BasePipelineFactory extends ChannelInitializer<Channel> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BasePipelineFactory.class);
+
+ private final TrackerServer server;
+ private boolean eventsEnabled;
+ private int timeout;
+
+ public BasePipelineFactory(TrackerServer server, String protocol) {
+ this.server = server;
+ eventsEnabled = Context.getConfig().getBoolean(Keys.EVENT_ENABLE);
+ timeout = Context.getConfig().getInteger(Keys.PROTOCOL_TIMEOUT.withPrefix(protocol));
+ if (timeout == 0) {
+ timeout = Context.getConfig().getInteger(Keys.SERVER_TIMEOUT);
+ }
+ }
+
+ protected abstract void addProtocolHandlers(PipelineBuilder pipeline);
+
+ @SafeVarargs
+ private final void addHandlers(ChannelPipeline pipeline, Class<? extends ChannelHandler>... handlerClasses) {
+ for (Class<? extends ChannelHandler> handlerClass : handlerClasses) {
+ if (handlerClass != null) {
+ pipeline.addLast(Main.getInjector().getInstance(handlerClass));
+ }
+ }
+ }
+
+ public static <T extends ChannelHandler> T getHandler(ChannelPipeline pipeline, Class<T> clazz) {
+ for (Map.Entry<String, ChannelHandler> handlerEntry : pipeline) {
+ ChannelHandler handler = handlerEntry.getValue();
+ if (handler instanceof WrapperInboundHandler) {
+ handler = ((WrapperInboundHandler) handler).getWrappedHandler();
+ } else if (handler instanceof WrapperOutboundHandler) {
+ handler = ((WrapperOutboundHandler) handler).getWrappedHandler();
+ }
+ if (clazz.isAssignableFrom(handler.getClass())) {
+ return (T) handler;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void initChannel(Channel channel) {
+ final ChannelPipeline pipeline = channel.pipeline();
+
+ if (timeout > 0 && !server.isDatagram()) {
+ pipeline.addLast(new IdleStateHandler(timeout, 0, 0));
+ }
+ pipeline.addLast(new OpenChannelHandler(server));
+ pipeline.addLast(new NetworkMessageHandler());
+ pipeline.addLast(new StandardLoggingHandler());
+
+ addProtocolHandlers(handler -> {
+ if (!(handler instanceof BaseProtocolDecoder || handler instanceof BaseProtocolEncoder)) {
+ if (handler instanceof ChannelInboundHandler) {
+ handler = new WrapperInboundHandler((ChannelInboundHandler) handler);
+ } else {
+ handler = new WrapperOutboundHandler((ChannelOutboundHandler) handler);
+ }
+ }
+ pipeline.addLast(handler);
+ });
+
+ addHandlers(
+ pipeline,
+ GeolocationHandler.class,
+ HemisphereHandler.class,
+ DistanceHandler.class,
+ RemoteAddressHandler.class);
+
+ addDynamicHandlers(pipeline);
+
+ addHandlers(
+ pipeline,
+ FilterHandler.class,
+ GeocoderHandler.class,
+ MotionHandler.class,
+ EngineHoursHandler.class,
+ CopyAttributesHandler.class,
+ ComputedAttributesHandler.class,
+ WebDataHandler.class,
+ DefaultDataHandler.class);
+
+ if (eventsEnabled) {
+ addHandlers(
+ pipeline,
+ CommandResultEventHandler.class,
+ OverspeedEventHandler.class,
+ FuelDropEventHandler.class,
+ MotionEventHandler.class,
+ GeofenceEventHandler.class,
+ AlertEventHandler.class,
+ IgnitionEventHandler.class,
+ MaintenanceEventHandler.class,
+ DriverEventHandler.class);
+ }
+
+ pipeline.addLast(new MainEventHandler());
+ }
+
+ private void addDynamicHandlers(ChannelPipeline pipeline) {
+ String handlers = Context.getConfig().getString(Keys.EXTRA_HANDLERS);
+ if (handlers != null) {
+ for (String handler : handlers.split(",")) {
+ try {
+ pipeline.addLast((ChannelHandler) Class.forName(handler).getDeclaredConstructor().newInstance());
+ } catch (ReflectiveOperationException error) {
+ LOGGER.warn("Dynamic handler error", error);
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/BaseProtocol.java b/src/main/java/org/traccar/BaseProtocol.java
new file mode 100644
index 000000000..c0fd1e27f
--- /dev/null
+++ b/src/main/java/org/traccar/BaseProtocol.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.database.ActiveDevice;
+import org.traccar.helper.DataConverter;
+import org.traccar.model.Command;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+public abstract class BaseProtocol implements Protocol {
+
+ private final String name;
+ private final Set<String> supportedDataCommands = new HashSet<>();
+ private final Set<String> supportedTextCommands = new HashSet<>();
+ private final List<TrackerServer> serverList = new LinkedList<>();
+
+ private StringProtocolEncoder textCommandEncoder = null;
+
+ public static String nameFromClass(Class<?> clazz) {
+ String className = clazz.getSimpleName();
+ return className.substring(0, className.length() - 8).toLowerCase();
+ }
+
+ public BaseProtocol() {
+ name = nameFromClass(getClass());
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ protected void addServer(TrackerServer server) {
+ serverList.add(server);
+ }
+
+ @Override
+ public Collection<TrackerServer> getServerList() {
+ return serverList;
+ }
+
+ public void setSupportedDataCommands(String... commands) {
+ supportedDataCommands.addAll(Arrays.asList(commands));
+ }
+
+ public void setSupportedTextCommands(String... commands) {
+ supportedTextCommands.addAll(Arrays.asList(commands));
+ }
+
+ public void setSupportedCommands(String... commands) {
+ supportedDataCommands.addAll(Arrays.asList(commands));
+ supportedTextCommands.addAll(Arrays.asList(commands));
+ }
+
+ @Override
+ public Collection<String> getSupportedDataCommands() {
+ Set<String> commands = new HashSet<>(supportedDataCommands);
+ commands.add(Command.TYPE_CUSTOM);
+ return commands;
+ }
+
+ @Override
+ public Collection<String> getSupportedTextCommands() {
+ Set<String> commands = new HashSet<>(supportedTextCommands);
+ commands.add(Command.TYPE_CUSTOM);
+ return commands;
+ }
+
+ @Override
+ public void sendDataCommand(ActiveDevice activeDevice, Command command) {
+ if (supportedDataCommands.contains(command.getType())) {
+ activeDevice.write(command);
+ } else if (command.getType().equals(Command.TYPE_CUSTOM)) {
+ String data = command.getString(Command.KEY_DATA);
+ if (BasePipelineFactory.getHandler(activeDevice.getChannel().pipeline(), StringEncoder.class) != null) {
+ activeDevice.write(data);
+ } else {
+ activeDevice.write(Unpooled.wrappedBuffer(DataConverter.parseHex(data)));
+ }
+ } else {
+ throw new RuntimeException("Command " + command.getType() + " is not supported in protocol " + getName());
+ }
+ }
+
+ public void setTextCommandEncoder(StringProtocolEncoder textCommandEncoder) {
+ this.textCommandEncoder = textCommandEncoder;
+ }
+
+ @Override
+ public void sendTextCommand(String destAddress, Command command) throws Exception {
+ if (Context.getSmsManager() != null) {
+ if (command.getType().equals(Command.TYPE_CUSTOM)) {
+ Context.getSmsManager().sendMessageSync(destAddress, command.getString(Command.KEY_DATA), true);
+ } else if (supportedTextCommands.contains(command.getType()) && textCommandEncoder != null) {
+ String encodedCommand = (String) textCommandEncoder.encodeCommand(command);
+ if (encodedCommand != null) {
+ Context.getSmsManager().sendMessageSync(destAddress, encodedCommand, true);
+ } else {
+ throw new RuntimeException("Failed to encode command");
+ }
+ } else {
+ throw new RuntimeException(
+ "Command " + command.getType() + " is not supported in protocol " + getName());
+ }
+ } else {
+ throw new RuntimeException("SMS is not enabled");
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/BaseProtocolDecoder.java b/src/main/java/org/traccar/BaseProtocolDecoder.java
new file mode 100644
index 000000000..aa5be612e
--- /dev/null
+++ b/src/main/java/org/traccar/BaseProtocolDecoder.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.Channel;
+import io.netty.channel.socket.DatagramChannel;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.database.ConnectionManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.database.StatisticsManager;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+
+public abstract class BaseProtocolDecoder extends ExtendedObjectDecoder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BaseProtocolDecoder.class);
+
+ private static final String PROTOCOL_UNKNOWN = "unknown";
+
+ private final Config config = Context.getConfig();
+ private final IdentityManager identityManager = Context.getIdentityManager();
+ private final ConnectionManager connectionManager = Context.getConnectionManager();
+ private final StatisticsManager statisticsManager;
+ private final Protocol protocol;
+
+ public BaseProtocolDecoder(Protocol protocol) {
+ this.protocol = protocol;
+ statisticsManager = Main.getInjector() != null ? Main.getInjector().getInstance(StatisticsManager.class) : null;
+ }
+
+ public String getProtocolName() {
+ return protocol != null ? protocol.getName() : PROTOCOL_UNKNOWN;
+ }
+
+ public String getServer(Channel channel, char delimiter) {
+ String server = config.getString(getProtocolName() + ".server");
+ if (server == null && channel != null) {
+ InetSocketAddress address = (InetSocketAddress) channel.localAddress();
+ server = address.getAddress().getHostAddress() + ":" + address.getPort();
+ }
+ return server != null ? server.replace(':', delimiter) : null;
+ }
+
+ protected double convertSpeed(double value, String defaultUnits) {
+ switch (config.getString(getProtocolName() + ".speed", defaultUnits)) {
+ case "kmh":
+ return UnitsConverter.knotsFromKph(value);
+ case "mps":
+ return UnitsConverter.knotsFromMps(value);
+ case "mph":
+ return UnitsConverter.knotsFromMph(value);
+ case "kn":
+ default:
+ return value;
+ }
+ }
+
+ protected TimeZone getTimeZone(long deviceId) {
+ return getTimeZone(deviceId, "UTC");
+ }
+
+ protected TimeZone getTimeZone(long deviceId, String defaultTimeZone) {
+ TimeZone result = TimeZone.getTimeZone(defaultTimeZone);
+ String timeZoneName = identityManager.lookupAttributeString(deviceId, "decoder.timezone", null, true);
+ if (timeZoneName != null) {
+ result = TimeZone.getTimeZone(timeZoneName);
+ } else {
+ int timeZoneOffset = config.getInteger(getProtocolName() + ".timezone", 0);
+ if (timeZoneOffset != 0) {
+ result.setRawOffset(timeZoneOffset * 1000);
+ LOGGER.warn("Config parameter " + getProtocolName() + ".timezone is deprecated");
+ }
+ }
+ return result;
+ }
+
+ private DeviceSession channelDeviceSession; // connection-based protocols
+ private Map<SocketAddress, DeviceSession> addressDeviceSessions = new HashMap<>(); // connectionless protocols
+
+ private long findDeviceId(SocketAddress remoteAddress, String... uniqueIds) {
+ if (uniqueIds.length > 0) {
+ long deviceId = 0;
+ Device device = null;
+ try {
+ for (String uniqueId : uniqueIds) {
+ if (uniqueId != null) {
+ device = identityManager.getByUniqueId(uniqueId);
+ if (device != null) {
+ deviceId = device.getId();
+ break;
+ }
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.warn("Find device error", e);
+ }
+ if (deviceId == 0 && config.getBoolean("database.registerUnknown")) {
+ return identityManager.addUnknownDevice(uniqueIds[0]);
+ }
+ if (device != null && !device.getDisabled() || config.getBoolean("database.storeDisabled")) {
+ return deviceId;
+ }
+ StringBuilder message = new StringBuilder();
+ if (deviceId == 0) {
+ message.append("Unknown device -");
+ } else {
+ message.append("Disabled device -");
+ }
+ for (String uniqueId : uniqueIds) {
+ message.append(" ").append(uniqueId);
+ }
+ if (remoteAddress != null) {
+ message.append(" (").append(((InetSocketAddress) remoteAddress).getHostString()).append(")");
+ }
+ LOGGER.warn(message.toString());
+ }
+ return 0;
+ }
+
+ public DeviceSession getDeviceSession(Channel channel, SocketAddress remoteAddress, String... uniqueIds) {
+ if (channel != null && BasePipelineFactory.getHandler(channel.pipeline(), HttpRequestDecoder.class) != null
+ || config.getBoolean("decoder.ignoreSessionCache")) {
+ long deviceId = findDeviceId(remoteAddress, uniqueIds);
+ if (deviceId != 0) {
+ if (connectionManager != null) {
+ connectionManager.addActiveDevice(deviceId, protocol, channel, remoteAddress);
+ }
+ return new DeviceSession(deviceId);
+ } else {
+ return null;
+ }
+ }
+ if (channel instanceof DatagramChannel) {
+ long deviceId = findDeviceId(remoteAddress, uniqueIds);
+ DeviceSession deviceSession = addressDeviceSessions.get(remoteAddress);
+ if (deviceSession != null && (deviceSession.getDeviceId() == deviceId || uniqueIds.length == 0)) {
+ return deviceSession;
+ } else if (deviceId != 0) {
+ deviceSession = new DeviceSession(deviceId);
+ addressDeviceSessions.put(remoteAddress, deviceSession);
+ if (connectionManager != null) {
+ connectionManager.addActiveDevice(deviceId, protocol, channel, remoteAddress);
+ }
+ return deviceSession;
+ } else {
+ return null;
+ }
+ } else {
+ if (channelDeviceSession == null) {
+ long deviceId = findDeviceId(remoteAddress, uniqueIds);
+ if (deviceId != 0) {
+ channelDeviceSession = new DeviceSession(deviceId);
+ if (connectionManager != null) {
+ connectionManager.addActiveDevice(deviceId, protocol, channel, remoteAddress);
+ }
+ }
+ }
+ return channelDeviceSession;
+ }
+ }
+
+ public void getLastLocation(Position position, Date deviceTime) {
+ if (position.getDeviceId() != 0) {
+ position.setOutdated(true);
+
+ Position last = identityManager.getLastPosition(position.getDeviceId());
+ if (last != null) {
+ position.setFixTime(last.getFixTime());
+ position.setValid(last.getValid());
+ position.setLatitude(last.getLatitude());
+ position.setLongitude(last.getLongitude());
+ position.setAltitude(last.getAltitude());
+ position.setSpeed(last.getSpeed());
+ position.setCourse(last.getCourse());
+ position.setAccuracy(last.getAccuracy());
+ } else {
+ position.setFixTime(new Date(0));
+ }
+
+ if (deviceTime != null) {
+ position.setDeviceTime(deviceTime);
+ } else {
+ position.setDeviceTime(new Date());
+ }
+ }
+ }
+
+ @Override
+ protected void onMessageEvent(
+ Channel channel, SocketAddress remoteAddress, Object originalMessage, Object decodedMessage) {
+ if (statisticsManager != null) {
+ statisticsManager.registerMessageReceived();
+ }
+ Position position = null;
+ if (decodedMessage != null) {
+ if (decodedMessage instanceof Position) {
+ position = (Position) decodedMessage;
+ } else if (decodedMessage instanceof Collection) {
+ Collection positions = (Collection) decodedMessage;
+ if (!positions.isEmpty()) {
+ position = (Position) positions.iterator().next();
+ }
+ }
+ }
+ if (position != null) {
+ connectionManager.updateDevice(
+ position.getDeviceId(), Device.STATUS_ONLINE, new Date());
+ } else {
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession != null) {
+ connectionManager.updateDevice(
+ deviceSession.getDeviceId(), Device.STATUS_ONLINE, new Date());
+ }
+ }
+ }
+
+ @Override
+ protected Object handleEmptyMessage(Channel channel, SocketAddress remoteAddress, Object msg) {
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (config.getBoolean("database.saveEmpty") && deviceSession != null) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ getLastLocation(position, null);
+ return position;
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/BaseProtocolEncoder.java b/src/main/java/org/traccar/BaseProtocolEncoder.java
new file mode 100644
index 000000000..d7625e4b8
--- /dev/null
+++ b/src/main/java/org/traccar/BaseProtocolEncoder.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.model.Command;
+import org.traccar.model.Device;
+
+public abstract class BaseProtocolEncoder extends ChannelOutboundHandlerAdapter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BaseProtocolEncoder.class);
+
+ protected String getUniqueId(long deviceId) {
+ return Context.getIdentityManager().getById(deviceId).getUniqueId();
+ }
+
+ protected void initDevicePassword(Command command, String defaultPassword) {
+ if (!command.getAttributes().containsKey(Command.KEY_DEVICE_PASSWORD)) {
+ Device device = Context.getIdentityManager().getById(command.getDeviceId());
+ String password = device.getString(Command.KEY_DEVICE_PASSWORD);
+ if (password != null) {
+ command.set(Command.KEY_DEVICE_PASSWORD, password);
+ } else {
+ command.set(Command.KEY_DEVICE_PASSWORD, defaultPassword);
+ }
+ }
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+
+ NetworkMessage networkMessage = (NetworkMessage) msg;
+
+ if (networkMessage.getMessage() instanceof Command) {
+
+ Command command = (Command) networkMessage.getMessage();
+ Object encodedCommand = encodeCommand(ctx.channel(), command);
+
+ StringBuilder s = new StringBuilder();
+ s.append("[").append(ctx.channel().id().asShortText()).append("] ");
+ s.append("id: ").append(getUniqueId(command.getDeviceId())).append(", ");
+ s.append("command type: ").append(command.getType()).append(" ");
+ if (encodedCommand != null) {
+ s.append("sent");
+ } else {
+ s.append("not sent");
+ }
+ LOGGER.info(s.toString());
+
+ ctx.write(new NetworkMessage(encodedCommand, networkMessage.getRemoteAddress()), promise);
+
+ } else {
+
+ super.write(ctx, msg, promise);
+
+ }
+ }
+
+ protected Object encodeCommand(Channel channel, Command command) {
+ return encodeCommand(command);
+ }
+
+ protected Object encodeCommand(Command command) {
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/CharacterDelimiterFrameDecoder.java b/src/main/java/org/traccar/CharacterDelimiterFrameDecoder.java
new file mode 100644
index 000000000..eeb8834dc
--- /dev/null
+++ b/src/main/java/org/traccar/CharacterDelimiterFrameDecoder.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.DelimiterBasedFrameDecoder;
+
+public class CharacterDelimiterFrameDecoder extends DelimiterBasedFrameDecoder {
+
+ private static ByteBuf createDelimiter(char delimiter) {
+ byte[] buf = {(byte) delimiter};
+ return Unpooled.wrappedBuffer(buf);
+ }
+
+ private static ByteBuf createDelimiter(String delimiter) {
+ byte[] buf = new byte[delimiter.length()];
+ for (int i = 0; i < delimiter.length(); i++) {
+ buf[i] = (byte) delimiter.charAt(i);
+ }
+ return Unpooled.wrappedBuffer(buf);
+ }
+
+ private static ByteBuf[] convertDelimiters(String[] delimiters) {
+ ByteBuf[] result = new ByteBuf[delimiters.length];
+ for (int i = 0; i < delimiters.length; i++) {
+ result[i] = createDelimiter(delimiters[i]);
+ }
+ return result;
+ }
+
+ public CharacterDelimiterFrameDecoder(int maxFrameLength, char delimiter) {
+ super(maxFrameLength, createDelimiter(delimiter));
+ }
+
+ public CharacterDelimiterFrameDecoder(int maxFrameLength, String delimiter) {
+ super(maxFrameLength, createDelimiter(delimiter));
+ }
+
+ public CharacterDelimiterFrameDecoder(int maxFrameLength, boolean stripDelimiter, String delimiter) {
+ super(maxFrameLength, stripDelimiter, createDelimiter(delimiter));
+ }
+
+ public CharacterDelimiterFrameDecoder(int maxFrameLength, String... delimiters) {
+ super(maxFrameLength, convertDelimiters(delimiters));
+ }
+
+ public CharacterDelimiterFrameDecoder(int maxFrameLength, boolean stripDelimiter, String... delimiters) {
+ super(maxFrameLength, stripDelimiter, convertDelimiters(delimiters));
+ }
+
+}
diff --git a/src/main/java/org/traccar/Context.java b/src/main/java/org/traccar/Context.java
new file mode 100644
index 000000000..9c20db9e4
--- /dev/null
+++ b/src/main/java/org/traccar/Context.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr353.JSR353Module;
+import org.apache.velocity.app.VelocityEngine;
+import org.eclipse.jetty.util.URIUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.database.AttributesManager;
+import org.traccar.database.BaseObjectManager;
+import org.traccar.database.CalendarManager;
+import org.traccar.database.CommandsManager;
+import org.traccar.database.ConnectionManager;
+import org.traccar.database.DataManager;
+import org.traccar.database.DeviceManager;
+import org.traccar.database.DriversManager;
+import org.traccar.database.GeofenceManager;
+import org.traccar.database.GroupsManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.database.LdapProvider;
+import org.traccar.database.MailManager;
+import org.traccar.database.MaintenancesManager;
+import org.traccar.database.MediaManager;
+import org.traccar.database.NotificationManager;
+import org.traccar.database.PermissionsManager;
+import org.traccar.database.UsersManager;
+import org.traccar.geocoder.Geocoder;
+import org.traccar.helper.Log;
+import org.traccar.helper.SanitizerModule;
+import org.traccar.model.Attribute;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Calendar;
+import org.traccar.model.Command;
+import org.traccar.model.Device;
+import org.traccar.model.Driver;
+import org.traccar.model.Geofence;
+import org.traccar.model.Group;
+import org.traccar.model.Maintenance;
+import org.traccar.model.Notification;
+import org.traccar.model.User;
+import org.traccar.notification.EventForwarder;
+import org.traccar.notification.JsonTypeEventForwarder;
+import org.traccar.notification.NotificatorManager;
+import org.traccar.reports.model.TripsConfig;
+import org.traccar.sms.SmsManager;
+import org.traccar.sms.smpp.SmppClient;
+import org.traccar.web.WebServer;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.ext.ContextResolver;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Properties;
+
+public final class Context {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Context.class);
+
+ private Context() {
+ }
+
+ private static Config config;
+
+ public static Config getConfig() {
+ return config;
+ }
+
+ private static ObjectMapper objectMapper;
+
+ public static ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+ private static IdentityManager identityManager;
+
+ public static IdentityManager getIdentityManager() {
+ return identityManager;
+ }
+
+ private static DataManager dataManager;
+
+ public static DataManager getDataManager() {
+ return dataManager;
+ }
+
+ private static LdapProvider ldapProvider;
+
+ public static LdapProvider getLdapProvider() {
+ return ldapProvider;
+ }
+
+ private static MailManager mailManager;
+
+ public static MailManager getMailManager() {
+ return mailManager;
+ }
+
+ private static MediaManager mediaManager;
+
+ public static MediaManager getMediaManager() {
+ return mediaManager;
+ }
+
+ private static UsersManager usersManager;
+
+ public static UsersManager getUsersManager() {
+ return usersManager;
+ }
+
+ private static GroupsManager groupsManager;
+
+ public static GroupsManager getGroupsManager() {
+ return groupsManager;
+ }
+
+ private static DeviceManager deviceManager;
+
+ public static DeviceManager getDeviceManager() {
+ return deviceManager;
+ }
+
+ private static ConnectionManager connectionManager;
+
+ public static ConnectionManager getConnectionManager() {
+ return connectionManager;
+ }
+
+ private static PermissionsManager permissionsManager;
+
+ public static PermissionsManager getPermissionsManager() {
+ return permissionsManager;
+ }
+
+ public static Geocoder getGeocoder() {
+ return Main.getInjector() != null ? Main.getInjector().getInstance(Geocoder.class) : null;
+ }
+
+ private static WebServer webServer;
+
+ public static WebServer getWebServer() {
+ return webServer;
+ }
+
+ private static ServerManager serverManager;
+
+ public static ServerManager getServerManager() {
+ return serverManager;
+ }
+
+ private static GeofenceManager geofenceManager;
+
+ public static GeofenceManager getGeofenceManager() {
+ return geofenceManager;
+ }
+
+ private static CalendarManager calendarManager;
+
+ public static CalendarManager getCalendarManager() {
+ return calendarManager;
+ }
+
+ private static NotificationManager notificationManager;
+
+ public static NotificationManager getNotificationManager() {
+ return notificationManager;
+ }
+
+ private static NotificatorManager notificatorManager;
+
+ public static NotificatorManager getNotificatorManager() {
+ return notificatorManager;
+ }
+
+ private static VelocityEngine velocityEngine;
+
+ public static VelocityEngine getVelocityEngine() {
+ return velocityEngine;
+ }
+
+ private static Client client = ClientBuilder.newClient();
+
+ public static Client getClient() {
+ return client;
+ }
+
+ private static EventForwarder eventForwarder;
+
+ public static EventForwarder getEventForwarder() {
+ return eventForwarder;
+ }
+
+ private static AttributesManager attributesManager;
+
+ public static AttributesManager getAttributesManager() {
+ return attributesManager;
+ }
+
+ private static DriversManager driversManager;
+
+ public static DriversManager getDriversManager() {
+ return driversManager;
+ }
+
+ private static CommandsManager commandsManager;
+
+ public static CommandsManager getCommandsManager() {
+ return commandsManager;
+ }
+
+ private static MaintenancesManager maintenancesManager;
+
+ public static MaintenancesManager getMaintenancesManager() {
+ return maintenancesManager;
+ }
+
+ private static SmsManager smsManager;
+
+ public static SmsManager getSmsManager() {
+ return smsManager;
+ }
+
+ private static TripsConfig tripsConfig;
+
+ public static TripsConfig getTripsConfig() {
+ return tripsConfig;
+ }
+
+ public static TripsConfig initTripsConfig() {
+ return new TripsConfig(
+ config.getLong("report.trip.minimalTripDistance", 500),
+ config.getLong("report.trip.minimalTripDuration", 300) * 1000,
+ config.getLong("report.trip.minimalParkingDuration", 300) * 1000,
+ config.getLong("report.trip.minimalNoDataDuration", 3600) * 1000,
+ config.getBoolean("report.trip.useIgnition"),
+ config.getBoolean("event.motion.processInvalidPositions"),
+ config.getDouble("event.motion.speedThreshold", 0.01));
+ }
+
+ private static class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
+
+ @Override
+ public ObjectMapper getContext(Class<?> clazz) {
+ return objectMapper;
+ }
+
+ }
+
+ public static void init(String configFile) throws Exception {
+
+ try {
+ config = new Config(configFile);
+ } catch (Exception e) {
+ config = new Config();
+ Log.setupDefaultLogger();
+ throw e;
+ }
+
+ if (config.getBoolean("logger.enable")) {
+ Log.setupLogger(config);
+ }
+
+ objectMapper = new ObjectMapper();
+ objectMapper.registerModule(new SanitizerModule());
+ objectMapper.registerModule(new JSR353Module());
+ objectMapper.setConfig(
+ objectMapper.getSerializationConfig().without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS));
+ if (Context.getConfig().getBoolean("mapper.prettyPrintedJson")) {
+ objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
+ }
+
+ client = ClientBuilder.newClient().register(new ObjectMapperContextResolver());
+
+ if (config.hasKey("database.url")) {
+ dataManager = new DataManager(config);
+ }
+
+ if (config.getBoolean("ldap.enable")) {
+ ldapProvider = new LdapProvider(config);
+ }
+
+ mailManager = new MailManager();
+
+ mediaManager = new MediaManager(config.getString("media.path"));
+
+ if (dataManager != null) {
+ usersManager = new UsersManager(dataManager);
+ groupsManager = new GroupsManager(dataManager);
+ deviceManager = new DeviceManager(dataManager);
+ }
+
+ identityManager = deviceManager;
+
+ if (config.getBoolean("web.enable")) {
+ webServer = new WebServer(config);
+ }
+
+ permissionsManager = new PermissionsManager(dataManager, usersManager);
+
+ connectionManager = new ConnectionManager();
+
+ tripsConfig = initTripsConfig();
+
+ if (config.getBoolean("sms.enable")) {
+ final String smsManagerClass = config.getString("sms.manager.class", SmppClient.class.getCanonicalName());
+ try {
+ smsManager = (SmsManager) Class.forName(smsManagerClass).newInstance();
+ } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
+ LOGGER.warn("Error loading SMS Manager class : " + smsManagerClass, e);
+ }
+ }
+
+ if (config.getBoolean("event.enable")) {
+ initEventsModule();
+ }
+
+ serverManager = new ServerManager();
+
+ if (config.getBoolean("event.forward.enable")) {
+ eventForwarder = new JsonTypeEventForwarder();
+ }
+
+ attributesManager = new AttributesManager(dataManager);
+
+ driversManager = new DriversManager(dataManager);
+
+ commandsManager = new CommandsManager(dataManager, config.getBoolean("commands.queueing"));
+
+ }
+
+ private static void initEventsModule() {
+
+ geofenceManager = new GeofenceManager(dataManager);
+ calendarManager = new CalendarManager(dataManager);
+ maintenancesManager = new MaintenancesManager(dataManager);
+ notificationManager = new NotificationManager(dataManager);
+ notificatorManager = new NotificatorManager();
+ Properties velocityProperties = new Properties();
+ velocityProperties.setProperty("file.resource.loader.path",
+ Context.getConfig().getString("templates.rootPath", "templates") + "/");
+ velocityProperties.setProperty("runtime.log.logsystem.class",
+ "org.apache.velocity.runtime.log.NullLogChute");
+
+ String address;
+ try {
+ address = config.getString("web.address", InetAddress.getLocalHost().getHostAddress());
+ } catch (UnknownHostException e) {
+ address = "localhost";
+ }
+
+ String webUrl = URIUtil.newURI("http", address, config.getInteger("web.port", 8082), "", "");
+ webUrl = Context.getConfig().getString("web.url", webUrl);
+ velocityProperties.setProperty("web.url", webUrl);
+
+ velocityEngine = new VelocityEngine();
+ velocityEngine.init(velocityProperties);
+ }
+
+ public static void init(IdentityManager testIdentityManager, MediaManager testMediaManager) {
+ config = new Config();
+ objectMapper = new ObjectMapper();
+ objectMapper.registerModule(new JSR353Module());
+ client = ClientBuilder.newClient().register(new ObjectMapperContextResolver());
+ identityManager = testIdentityManager;
+ mediaManager = testMediaManager;
+ }
+
+ public static <T extends BaseModel> BaseObjectManager<T> getManager(Class<T> clazz) {
+ if (clazz.equals(Device.class)) {
+ return (BaseObjectManager<T>) deviceManager;
+ } else if (clazz.equals(Group.class)) {
+ return (BaseObjectManager<T>) groupsManager;
+ } else if (clazz.equals(User.class)) {
+ return (BaseObjectManager<T>) usersManager;
+ } else if (clazz.equals(Calendar.class)) {
+ return (BaseObjectManager<T>) calendarManager;
+ } else if (clazz.equals(Attribute.class)) {
+ return (BaseObjectManager<T>) attributesManager;
+ } else if (clazz.equals(Geofence.class)) {
+ return (BaseObjectManager<T>) geofenceManager;
+ } else if (clazz.equals(Driver.class)) {
+ return (BaseObjectManager<T>) driversManager;
+ } else if (clazz.equals(Command.class)) {
+ return (BaseObjectManager<T>) commandsManager;
+ } else if (clazz.equals(Maintenance.class)) {
+ return (BaseObjectManager<T>) maintenancesManager;
+ } else if (clazz.equals(Notification.class)) {
+ return (BaseObjectManager<T>) notificationManager;
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/DeviceSession.java b/src/main/java/org/traccar/DeviceSession.java
new file mode 100644
index 000000000..322381807
--- /dev/null
+++ b/src/main/java/org/traccar/DeviceSession.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import java.util.TimeZone;
+
+public class DeviceSession {
+
+ private final long deviceId;
+
+ public DeviceSession(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ private TimeZone timeZone;
+
+ public void setTimeZone(TimeZone timeZone) {
+ this.timeZone = timeZone;
+ }
+
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+}
diff --git a/src/main/java/org/traccar/EventLoopGroupFactory.java b/src/main/java/org/traccar/EventLoopGroupFactory.java
new file mode 100644
index 000000000..482559253
--- /dev/null
+++ b/src/main/java/org/traccar/EventLoopGroupFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+
+public final class EventLoopGroupFactory {
+
+ private static EventLoopGroup bossGroup = new NioEventLoopGroup();
+ private static EventLoopGroup workerGroup = new NioEventLoopGroup();
+
+ private EventLoopGroupFactory() {
+ }
+
+ public static EventLoopGroup getBossGroup() {
+ return bossGroup;
+ }
+
+ public static EventLoopGroup getWorkerGroup() {
+ return workerGroup;
+ }
+
+}
diff --git a/src/main/java/org/traccar/ExtendedObjectDecoder.java b/src/main/java/org/traccar/ExtendedObjectDecoder.java
new file mode 100644
index 000000000..681924e87
--- /dev/null
+++ b/src/main/java/org/traccar/ExtendedObjectDecoder.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.util.ReferenceCountUtil;
+import org.traccar.helper.DataConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+
+public abstract class ExtendedObjectDecoder extends ChannelInboundHandlerAdapter {
+
+ private void saveOriginal(Object decodedMessage, Object originalMessage) {
+ if (Context.getConfig().getBoolean("database.saveOriginal") && decodedMessage instanceof Position) {
+ Position position = (Position) decodedMessage;
+ if (originalMessage instanceof ByteBuf) {
+ ByteBuf buf = (ByteBuf) originalMessage;
+ position.set(Position.KEY_ORIGINAL, ByteBufUtil.hexDump(buf));
+ } else if (originalMessage instanceof String) {
+ position.set(Position.KEY_ORIGINAL, DataConverter.printHex(
+ ((String) originalMessage).getBytes(StandardCharsets.US_ASCII)));
+ }
+ }
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ NetworkMessage networkMessage = (NetworkMessage) msg;
+ Object originalMessage = networkMessage.getMessage();
+ try {
+ Object decodedMessage = decode(ctx.channel(), networkMessage.getRemoteAddress(), originalMessage);
+ onMessageEvent(ctx.channel(), networkMessage.getRemoteAddress(), originalMessage, decodedMessage);
+ if (decodedMessage == null) {
+ decodedMessage = handleEmptyMessage(ctx.channel(), networkMessage.getRemoteAddress(), originalMessage);
+ }
+ if (decodedMessage != null) {
+ if (decodedMessage instanceof Collection) {
+ for (Object o : (Collection) decodedMessage) {
+ saveOriginal(o, originalMessage);
+ ctx.fireChannelRead(o);
+ }
+ } else {
+ saveOriginal(decodedMessage, originalMessage);
+ ctx.fireChannelRead(decodedMessage);
+ }
+ }
+ } finally {
+ ReferenceCountUtil.release(originalMessage);
+ }
+ }
+
+ protected void onMessageEvent(
+ Channel channel, SocketAddress remoteAddress, Object originalMessage, Object decodedMessage) {
+ }
+
+ protected Object handleEmptyMessage(Channel channel, SocketAddress remoteAddress, Object msg) {
+ return null;
+ }
+
+ protected abstract Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception;
+
+}
diff --git a/src/main/java/org/traccar/GlobalTimer.java b/src/main/java/org/traccar/GlobalTimer.java
new file mode 100644
index 000000000..a97321ba2
--- /dev/null
+++ b/src/main/java/org/traccar/GlobalTimer.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2012 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.util.HashedWheelTimer;
+import io.netty.util.Timer;
+
+public final class GlobalTimer {
+
+ private static Timer instance = null;
+
+ private GlobalTimer() {
+ }
+
+ public static void release() {
+ if (instance != null) {
+ instance.stop();
+ }
+ instance = null;
+ }
+
+ public static Timer getTimer() {
+ if (instance == null) {
+ instance = new HashedWheelTimer();
+ }
+ return instance;
+ }
+
+}
diff --git a/src/main/java/org/traccar/Main.java b/src/main/java/org/traccar/Main.java
new file mode 100644
index 000000000..6ebd1d399
--- /dev/null
+++ b/src/main/java/org/traccar/Main.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.management.OperatingSystemMXBean;
+import java.lang.management.RuntimeMXBean;
+import java.nio.charset.Charset;
+import java.sql.SQLException;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.Locale;
+
+public final class Main {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
+
+ private static final long CLEAN_PERIOD = 24 * 60 * 60 * 1000;
+
+ private static Injector injector;
+
+ public static Injector getInjector() {
+ return injector;
+ }
+
+ private Main() {
+ }
+
+ public static void logSystemInfo() {
+ try {
+ OperatingSystemMXBean operatingSystemBean = ManagementFactory.getOperatingSystemMXBean();
+ LOGGER.info("Operating system"
+ + " name: " + operatingSystemBean.getName()
+ + " version: " + operatingSystemBean.getVersion()
+ + " architecture: " + operatingSystemBean.getArch());
+
+ RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
+ LOGGER.info("Java runtime"
+ + " name: " + runtimeBean.getVmName()
+ + " vendor: " + runtimeBean.getVmVendor()
+ + " version: " + runtimeBean.getVmVersion());
+
+ MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
+ LOGGER.info("Memory limit"
+ + " heap: " + memoryBean.getHeapMemoryUsage().getMax() / (1024 * 1024) + "mb"
+ + " non-heap: " + memoryBean.getNonHeapMemoryUsage().getMax() / (1024 * 1024) + "mb");
+
+ LOGGER.info("Character encoding: "
+ + System.getProperty("file.encoding") + " charset: " + Charset.defaultCharset());
+
+ } catch (Exception error) {
+ LOGGER.warn("Failed to get system info");
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ Locale.setDefault(Locale.ENGLISH);
+
+ if (args.length <= 0) {
+ throw new RuntimeException("Configuration file is not provided");
+ }
+
+ final String configFile = args[args.length - 1];
+
+ if (args[0].startsWith("--")) {
+ WindowsService windowsService = new WindowsService("traccar") {
+ @Override
+ public void run() {
+ Main.run(configFile);
+ }
+ };
+ switch (args[0]) {
+ case "--install":
+ windowsService.install("traccar", null, null, null, null, configFile);
+ return;
+ case "--uninstall":
+ windowsService.uninstall();
+ return;
+ case "--service":
+ default:
+ windowsService.init();
+ break;
+ }
+ } else {
+ run(configFile);
+ }
+ }
+
+ public static void run(String configFile) {
+ try {
+ Context.init(configFile);
+ injector = Guice.createInjector(new MainModule());
+ logSystemInfo();
+ LOGGER.info("Version: " + Main.class.getPackage().getImplementationVersion());
+ LOGGER.info("Starting server...");
+
+ Context.getServerManager().start();
+ if (Context.getWebServer() != null) {
+ Context.getWebServer().start();
+ }
+
+ new Timer().scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ try {
+ Context.getDataManager().clearHistory();
+ } catch (SQLException error) {
+ LOGGER.warn("Clear history error", error);
+ }
+ }
+ }, 0, CLEAN_PERIOD);
+
+ Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread t, Throwable e) {
+ LOGGER.error("Thread exception", e);
+ }
+ });
+
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ LOGGER.info("Shutting down server...");
+
+ if (Context.getWebServer() != null) {
+ Context.getWebServer().stop();
+ }
+ Context.getServerManager().stop();
+ }
+ });
+ } catch (Exception e) {
+ LOGGER.error("Main method error", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/MainEventHandler.java b/src/main/java/org/traccar/MainEventHandler.java
new file mode 100644
index 000000000..a8b53ff60
--- /dev/null
+++ b/src/main/java/org/traccar/MainEventHandler.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.socket.DatagramChannel;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.timeout.IdleStateEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.database.StatisticsManager;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Position;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+public class MainEventHandler extends ChannelInboundHandlerAdapter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MainEventHandler.class);
+
+ private static final String DEFAULT_LOGGER_ATTRIBUTES = "time,position,speed,course,accuracy,result";
+
+ private final Set<String> connectionlessProtocols = new HashSet<>();
+ private final Set<String> logAttributes = new LinkedHashSet<>();
+
+ public MainEventHandler() {
+ String connectionlessProtocolList = Context.getConfig().getString("status.ignoreOffline");
+ if (connectionlessProtocolList != null) {
+ connectionlessProtocols.addAll(Arrays.asList(connectionlessProtocolList.split(",")));
+ }
+ logAttributes.addAll(Arrays.asList(
+ Context.getConfig().getString("logger.attributes", DEFAULT_LOGGER_ATTRIBUTES).split(",")));
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ if (msg instanceof Position) {
+
+ Position position = (Position) msg;
+ try {
+ Context.getDeviceManager().updateLatestPosition(position);
+ } catch (SQLException error) {
+ LOGGER.warn("Failed to update device", error);
+ }
+
+ String uniqueId = Context.getIdentityManager().getById(position.getDeviceId()).getUniqueId();
+
+ StringBuilder builder = new StringBuilder();
+ builder.append(formatChannel(ctx.channel())).append(" ");
+ builder.append("id: ").append(uniqueId);
+ for (String attribute : logAttributes) {
+ switch (attribute) {
+ case "time":
+ builder.append(", time: ").append(DateUtil.formatDate(position.getFixTime(), false));
+ break;
+ case "position":
+ builder.append(", lat: ").append(String.format("%.5f", position.getLatitude()));
+ builder.append(", lon: ").append(String.format("%.5f", position.getLongitude()));
+ break;
+ case "speed":
+ if (position.getSpeed() > 0) {
+ builder.append(", speed: ").append(String.format("%.1f", position.getSpeed()));
+ }
+ break;
+ case "course":
+ builder.append(", course: ").append(String.format("%.1f", position.getCourse()));
+ break;
+ case "accuracy":
+ if (position.getAccuracy() > 0) {
+ builder.append(", accuracy: ").append(String.format("%.1f", position.getAccuracy()));
+ }
+ break;
+ case "outdated":
+ if (position.getOutdated()) {
+ builder.append(", outdated");
+ }
+ break;
+ case "invalid":
+ if (!position.getValid()) {
+ builder.append(", invalid");
+ }
+ break;
+ default:
+ Object value = position.getAttributes().get(attribute);
+ if (value != null) {
+ builder.append(", ").append(attribute).append(": ").append(value);
+ }
+ break;
+ }
+ }
+ LOGGER.info(builder.toString());
+
+ Main.getInjector().getInstance(StatisticsManager.class).registerMessageStored(position.getDeviceId());
+ }
+ }
+
+ private static String formatChannel(Channel channel) {
+ return String.format("[%s]", channel.id().asShortText());
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ if (!(ctx.channel() instanceof DatagramChannel)) {
+ LOGGER.info(formatChannel(ctx.channel()) + " connected");
+ }
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) {
+ LOGGER.info(formatChannel(ctx.channel()) + " disconnected");
+ closeChannel(ctx.channel());
+
+ if (BasePipelineFactory.getHandler(ctx.pipeline(), HttpRequestDecoder.class) == null
+ && !connectionlessProtocols.contains(ctx.pipeline().get(BaseProtocolDecoder.class).getProtocolName())) {
+ Context.getConnectionManager().removeActiveDevice(ctx.channel());
+ }
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ while (cause.getCause() != null && cause.getCause() != cause) {
+ cause = cause.getCause();
+ }
+ LOGGER.warn(formatChannel(ctx.channel()) + " error", cause);
+ closeChannel(ctx.channel());
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+ if (evt instanceof IdleStateEvent) {
+ LOGGER.info(formatChannel(ctx.channel()) + " timed out");
+ closeChannel(ctx.channel());
+ }
+ }
+
+ private void closeChannel(Channel channel) {
+ if (!(channel instanceof DatagramChannel)) {
+ channel.close();
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/MainModule.java b/src/main/java/org/traccar/MainModule.java
new file mode 100644
index 000000000..6fe8bad1c
--- /dev/null
+++ b/src/main/java/org/traccar/MainModule.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2018 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.database.AttributesManager;
+import org.traccar.database.CalendarManager;
+import org.traccar.database.DataManager;
+import org.traccar.database.DeviceManager;
+import org.traccar.database.GeofenceManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.database.MaintenancesManager;
+import org.traccar.database.StatisticsManager;
+import org.traccar.geocoder.AddressFormat;
+import org.traccar.geocoder.BanGeocoder;
+import org.traccar.geocoder.BingMapsGeocoder;
+import org.traccar.geocoder.FactualGeocoder;
+import org.traccar.geocoder.GeocodeFarmGeocoder;
+import org.traccar.geocoder.GeocodeXyzGeocoder;
+import org.traccar.geocoder.Geocoder;
+import org.traccar.geocoder.GisgraphyGeocoder;
+import org.traccar.geocoder.GoogleGeocoder;
+import org.traccar.geocoder.HereGeocoder;
+import org.traccar.geocoder.MapQuestGeocoder;
+import org.traccar.geocoder.MapmyIndiaGeocoder;
+import org.traccar.geocoder.NominatimGeocoder;
+import org.traccar.geocoder.OpenCageGeocoder;
+import org.traccar.geolocation.GeolocationProvider;
+import org.traccar.geolocation.GoogleGeolocationProvider;
+import org.traccar.geolocation.MozillaGeolocationProvider;
+import org.traccar.geolocation.OpenCellIdGeolocationProvider;
+import org.traccar.geolocation.UnwiredGeolocationProvider;
+import org.traccar.handler.ComputedAttributesHandler;
+import org.traccar.handler.CopyAttributesHandler;
+import org.traccar.handler.DefaultDataHandler;
+import org.traccar.handler.DistanceHandler;
+import org.traccar.handler.EngineHoursHandler;
+import org.traccar.handler.FilterHandler;
+import org.traccar.handler.GeocoderHandler;
+import org.traccar.handler.GeolocationHandler;
+import org.traccar.handler.HemisphereHandler;
+import org.traccar.handler.MotionHandler;
+import org.traccar.handler.RemoteAddressHandler;
+import org.traccar.handler.events.AlertEventHandler;
+import org.traccar.handler.events.CommandResultEventHandler;
+import org.traccar.handler.events.DriverEventHandler;
+import org.traccar.handler.events.FuelDropEventHandler;
+import org.traccar.handler.events.GeofenceEventHandler;
+import org.traccar.handler.events.IgnitionEventHandler;
+import org.traccar.handler.events.MaintenanceEventHandler;
+import org.traccar.handler.events.MotionEventHandler;
+import org.traccar.handler.events.OverspeedEventHandler;
+import org.traccar.reports.model.TripsConfig;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.client.Client;
+
+public class MainModule extends AbstractModule {
+
+ @Provides
+ public static ObjectMapper provideObjectMapper() {
+ return Context.getObjectMapper();
+ }
+
+ @Provides
+ public static Config provideConfig() {
+ return Context.getConfig();
+ }
+
+ @Provides
+ public static DataManager provideDataManager() {
+ return Context.getDataManager();
+ }
+
+ @Provides
+ public static IdentityManager provideIdentityManager() {
+ return Context.getIdentityManager();
+ }
+
+ @Provides
+ public static Client provideClient() {
+ return Context.getClient();
+ }
+
+ @Provides
+ public static TripsConfig provideTripsConfig() {
+ return Context.getTripsConfig();
+ }
+
+ @Provides
+ public static DeviceManager provideDeviceManager() {
+ return Context.getDeviceManager();
+ }
+
+ @Provides
+ public static GeofenceManager provideGeofenceManager() {
+ return Context.getGeofenceManager();
+ }
+
+ @Provides
+ public static CalendarManager provideCalendarManager() {
+ return Context.getCalendarManager();
+ }
+
+ @Provides
+ public static AttributesManager provideAttributesManager() {
+ return Context.getAttributesManager();
+ }
+
+ @Provides
+ public static MaintenancesManager provideMaintenancesManager() {
+ return Context.getMaintenancesManager();
+ }
+
+ @Singleton
+ @Provides
+ public static StatisticsManager provideStatisticsManager(Config config, DataManager dataManager, Client client) {
+ return new StatisticsManager(config, dataManager, client);
+ }
+
+ @Singleton
+ @Provides
+ public static Geocoder provideGeocoder(Config config) {
+ if (config.getBoolean(Keys.GEOCODER_ENABLE)) {
+ String type = config.getString(Keys.GEOCODER_TYPE, "google");
+ String url = config.getString(Keys.GEOCODER_URL);
+ String id = config.getString(Keys.GEOCODER_ID);
+ String key = config.getString(Keys.GEOCODER_KEY);
+ String language = config.getString(Keys.GEOCODER_LANGUAGE);
+ String formatString = config.getString(Keys.GEOCODER_FORMAT);
+ AddressFormat addressFormat = formatString != null ? new AddressFormat(formatString) : new AddressFormat();
+
+ int cacheSize = config.getInteger(Keys.GEOCODER_CACHE_SIZE);
+ switch (type) {
+ case "nominatim":
+ return new NominatimGeocoder(url, key, language, cacheSize, addressFormat);
+ case "gisgraphy":
+ return new GisgraphyGeocoder(url, cacheSize, addressFormat);
+ case "mapquest":
+ return new MapQuestGeocoder(url, key, cacheSize, addressFormat);
+ case "opencage":
+ return new OpenCageGeocoder(url, key, cacheSize, addressFormat);
+ case "bingmaps":
+ return new BingMapsGeocoder(url, key, cacheSize, addressFormat);
+ case "factual":
+ return new FactualGeocoder(url, key, cacheSize, addressFormat);
+ case "geocodefarm":
+ return new GeocodeFarmGeocoder(key, language, cacheSize, addressFormat);
+ case "geocodexyz":
+ return new GeocodeXyzGeocoder(key, cacheSize, addressFormat);
+ case "ban":
+ return new BanGeocoder(cacheSize, addressFormat);
+ case "here":
+ return new HereGeocoder(id, key, language, cacheSize, addressFormat);
+ case "mapmyindia":
+ return new MapmyIndiaGeocoder(url, key, cacheSize, addressFormat);
+ default:
+ return new GoogleGeocoder(key, language, cacheSize, addressFormat);
+ }
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static GeolocationProvider provideGeolocationProvider(Config config) {
+ if (config.getBoolean(Keys.GEOLOCATION_ENABLE)) {
+ String type = config.getString(Keys.GEOLOCATION_TYPE, "mozilla");
+ String url = config.getString(Keys.GEOLOCATION_URL);
+ String key = config.getString(Keys.GEOLOCATION_KEY);
+ switch (type) {
+ case "google":
+ return new GoogleGeolocationProvider(key);
+ case "opencellid":
+ return new OpenCellIdGeolocationProvider(key);
+ case "unwired":
+ return new UnwiredGeolocationProvider(url, key);
+ default:
+ return new MozillaGeolocationProvider(key);
+ }
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static DistanceHandler provideDistanceHandler(Config config, IdentityManager identityManager) {
+ return new DistanceHandler(config, identityManager);
+ }
+
+ @Singleton
+ @Provides
+ public static FilterHandler provideFilterHandler(Config config) {
+ if (config.getBoolean(Keys.FILTER_ENABLE)) {
+ return new FilterHandler(config);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static HemisphereHandler provideHemisphereHandler(Config config) {
+ if (config.hasKey(Keys.LOCATION_LATITUDE_HEMISPHERE) || config.hasKey(Keys.LOCATION_LONGITUDE_HEMISPHERE)) {
+ return new HemisphereHandler(config);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static RemoteAddressHandler provideRemoteAddressHandler(Config config) {
+ if (config.getBoolean(Keys.PROCESSING_REMOTE_ADDRESS_ENABLE)) {
+ return new RemoteAddressHandler();
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static WebDataHandler provideWebDataHandler(
+ Config config, IdentityManager identityManager, ObjectMapper objectMapper, Client client) {
+ if (config.getBoolean(Keys.FORWARD_ENABLE)) {
+ return new WebDataHandler(config, identityManager, objectMapper, client);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static GeolocationHandler provideGeolocationHandler(
+ Config config, @Nullable GeolocationProvider geolocationProvider, StatisticsManager statisticsManager) {
+ if (geolocationProvider != null) {
+ return new GeolocationHandler(config, geolocationProvider, statisticsManager);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static GeocoderHandler provideGeocoderHandler(
+ Config config, @Nullable Geocoder geocoder, IdentityManager identityManager,
+ StatisticsManager statisticsManager) {
+ if (geocoder != null) {
+ return new GeocoderHandler(config, geocoder, identityManager, statisticsManager);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static MotionHandler provideMotionHandler(TripsConfig tripsConfig) {
+ return new MotionHandler(tripsConfig.getSpeedThreshold());
+ }
+
+ @Singleton
+ @Provides
+ public static EngineHoursHandler provideEngineHoursHandler(Config config, IdentityManager identityManager) {
+ if (config.getBoolean(Keys.PROCESSING_ENGINE_HOURS_ENABLE)) {
+ return new EngineHoursHandler(identityManager);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static CopyAttributesHandler provideCopyAttributesHandler(Config config, IdentityManager identityManager) {
+ if (config.getBoolean(Keys.PROCESSING_COPY_ATTRIBUTES_ENABLE)) {
+ return new CopyAttributesHandler(identityManager);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static ComputedAttributesHandler provideComputedAttributesHandler(
+ Config config, IdentityManager identityManager, AttributesManager attributesManager) {
+ if (config.getBoolean(Keys.PROCESSING_COMPUTED_ATTRIBUTES_ENABLE)) {
+ return new ComputedAttributesHandler(config, identityManager, attributesManager);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static DefaultDataHandler provideDefaultDataHandler(@Nullable DataManager dataManager) {
+ if (dataManager != null) {
+ return new DefaultDataHandler(dataManager);
+ }
+ return null;
+ }
+
+ @Singleton
+ @Provides
+ public static CommandResultEventHandler provideCommandResultEventHandler() {
+ return new CommandResultEventHandler();
+ }
+
+ @Singleton
+ @Provides
+ public static OverspeedEventHandler provideOverspeedEventHandler(
+ Config config, DeviceManager deviceManager, GeofenceManager geofenceManager) {
+ return new OverspeedEventHandler(config, deviceManager, geofenceManager);
+ }
+
+ @Singleton
+ @Provides
+ public static FuelDropEventHandler provideFuelDropEventHandler(IdentityManager identityManager) {
+ return new FuelDropEventHandler(identityManager);
+ }
+
+ @Singleton
+ @Provides
+ public static MotionEventHandler provideMotionEventHandler(
+ IdentityManager identityManager, DeviceManager deviceManager, TripsConfig tripsConfig) {
+ return new MotionEventHandler(identityManager, deviceManager, tripsConfig);
+ }
+
+ @Singleton
+ @Provides
+ public static GeofenceEventHandler provideGeofenceEventHandler(
+ IdentityManager identityManager, GeofenceManager geofenceManager, CalendarManager calendarManager) {
+ return new GeofenceEventHandler(identityManager, geofenceManager, calendarManager);
+ }
+
+ @Singleton
+ @Provides
+ public static AlertEventHandler provideAlertEventHandler(Config config, IdentityManager identityManager) {
+ return new AlertEventHandler(config, identityManager);
+ }
+
+ @Singleton
+ @Provides
+ public static IgnitionEventHandler provideIgnitionEventHandler(IdentityManager identityManager) {
+ return new IgnitionEventHandler(identityManager);
+ }
+
+ @Singleton
+ @Provides
+ public static MaintenanceEventHandler provideMaintenanceEventHandler(
+ IdentityManager identityManager, MaintenancesManager maintenancesManager) {
+ return new MaintenanceEventHandler(identityManager, maintenancesManager);
+ }
+
+ @Singleton
+ @Provides
+ public static DriverEventHandler provideDriverEventHandler(IdentityManager identityManager) {
+ return new DriverEventHandler(identityManager);
+ }
+
+ @Override
+ protected void configure() {
+ binder().requireExplicitBindings();
+ }
+
+}
diff --git a/src/main/java/org/traccar/NetworkMessage.java b/src/main/java/org/traccar/NetworkMessage.java
new file mode 100644
index 000000000..14a397e69
--- /dev/null
+++ b/src/main/java/org/traccar/NetworkMessage.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import java.net.SocketAddress;
+
+public class NetworkMessage {
+
+ private final SocketAddress remoteAddress;
+ private final Object message;
+
+ public NetworkMessage(Object message, SocketAddress remoteAddress) {
+ this.message = message;
+ this.remoteAddress = remoteAddress;
+ }
+
+ public SocketAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public Object getMessage() {
+ return message;
+ }
+
+}
diff --git a/src/main/java/org/traccar/PipelineBuilder.java b/src/main/java/org/traccar/PipelineBuilder.java
new file mode 100644
index 000000000..3334040b1
--- /dev/null
+++ b/src/main/java/org/traccar/PipelineBuilder.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.ChannelHandler;
+
+public interface PipelineBuilder {
+
+ void addLast(ChannelHandler handler);
+
+}
diff --git a/src/main/java/org/traccar/Protocol.java b/src/main/java/org/traccar/Protocol.java
new file mode 100644
index 000000000..3b66f2598
--- /dev/null
+++ b/src/main/java/org/traccar/Protocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import org.traccar.database.ActiveDevice;
+import org.traccar.model.Command;
+
+import java.util.Collection;
+
+public interface Protocol {
+
+ String getName();
+
+ Collection<TrackerServer> getServerList();
+
+ Collection<String> getSupportedDataCommands();
+
+ void sendDataCommand(ActiveDevice activeDevice, Command command);
+
+ Collection<String> getSupportedTextCommands();
+
+ void sendTextCommand(String destAddress, Command command) throws Exception;
+
+}
diff --git a/src/main/java/org/traccar/ServerManager.java b/src/main/java/org/traccar/ServerManager.java
new file mode 100644
index 000000000..6a3273402
--- /dev/null
+++ b/src/main/java/org/traccar/ServerManager.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.BindException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+public class ServerManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ServerManager.class);
+
+ private final List<TrackerServer> serverList = new LinkedList<>();
+ private final Map<String, BaseProtocol> protocolList = new ConcurrentHashMap<>();
+
+ public ServerManager() throws Exception {
+
+ List<String> names = new LinkedList<>();
+ String packageName = "org.traccar.protocol";
+ String packagePath = packageName.replace('.', '/');
+ URL packageUrl = getClass().getClassLoader().getResource(packagePath);
+
+ if (packageUrl.getProtocol().equals("jar")) {
+ String jarFileName = URLDecoder.decode(packageUrl.getFile(), StandardCharsets.UTF_8.name());
+ try (JarFile jf = new JarFile(jarFileName.substring(5, jarFileName.indexOf("!")))) {
+ Enumeration<JarEntry> jarEntries = jf.entries();
+ while (jarEntries.hasMoreElements()) {
+ String entryName = jarEntries.nextElement().getName();
+ if (entryName.startsWith(packagePath) && entryName.length() > packagePath.length() + 5) {
+ names.add(entryName.substring(packagePath.length() + 1, entryName.lastIndexOf('.')));
+ }
+ }
+ }
+ } else {
+ File folder = new File(new URI(packageUrl.toString()));
+ File[] files = folder.listFiles();
+ if (files != null) {
+ for (File actual: files) {
+ String entryName = actual.getName();
+ names.add(entryName.substring(0, entryName.lastIndexOf('.')));
+ }
+ }
+ }
+
+ for (String name : names) {
+ Class protocolClass = Class.forName(packageName + '.' + name);
+ if (BaseProtocol.class.isAssignableFrom(protocolClass)
+ && Context.getConfig().hasKey(BaseProtocol.nameFromClass(protocolClass) + ".port")) {
+ BaseProtocol protocol = (BaseProtocol) protocolClass.newInstance();
+ serverList.addAll(protocol.getServerList());
+ protocolList.put(protocol.getName(), protocol);
+ }
+ }
+ }
+
+ public BaseProtocol getProtocol(String name) {
+ return protocolList.get(name);
+ }
+
+ public void start() throws Exception {
+ for (TrackerServer server: serverList) {
+ try {
+ server.start();
+ } catch (BindException e) {
+ LOGGER.warn("Port {} is disabled due to conflict", server.getPort());
+ }
+ }
+ }
+
+ public void stop() {
+ for (TrackerServer server: serverList) {
+ server.stop();
+ }
+ GlobalTimer.release();
+ }
+
+}
diff --git a/src/main/java/org/traccar/StringProtocolEncoder.java b/src/main/java/org/traccar/StringProtocolEncoder.java
new file mode 100644
index 000000000..1945ae174
--- /dev/null
+++ b/src/main/java/org/traccar/StringProtocolEncoder.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import org.traccar.model.Command;
+
+import java.util.Map;
+
+public abstract class StringProtocolEncoder extends BaseProtocolEncoder {
+
+ public interface ValueFormatter {
+ String formatValue(String key, Object value);
+ }
+
+ protected String formatCommand(Command command, String format, ValueFormatter valueFormatter, String... keys) {
+
+ String result = String.format(format, (Object[]) keys);
+
+ result = result.replaceAll("\\{" + Command.KEY_UNIQUE_ID + "}", getUniqueId(command.getDeviceId()));
+ for (Map.Entry<String, Object> entry : command.getAttributes().entrySet()) {
+ String value = null;
+ if (valueFormatter != null) {
+ value = valueFormatter.formatValue(entry.getKey(), entry.getValue());
+ }
+ if (value == null) {
+ value = entry.getValue().toString();
+ }
+ result = result.replaceAll("\\{" + entry.getKey() + "}", value);
+ }
+
+ return result;
+ }
+
+ protected String formatCommand(Command command, String format, String... keys) {
+ return formatCommand(command, format, null, keys);
+ }
+
+}
diff --git a/src/main/java/org/traccar/TrackerServer.java b/src/main/java/org/traccar/TrackerServer.java
new file mode 100644
index 000000000..3a1e1c4e8
--- /dev/null
+++ b/src/main/java/org/traccar/TrackerServer.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.bootstrap.AbstractBootstrap;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.channel.socket.nio.NioDatagramChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.util.concurrent.GlobalEventExecutor;
+
+import java.net.InetSocketAddress;
+
+public abstract class TrackerServer {
+
+ private final boolean datagram;
+ private final AbstractBootstrap bootstrap;
+
+ public boolean isDatagram() {
+ return datagram;
+ }
+
+ public TrackerServer(boolean datagram, String protocol) {
+ this.datagram = datagram;
+
+ address = Context.getConfig().getString(protocol + ".address");
+ port = Context.getConfig().getInteger(protocol + ".port");
+
+ BasePipelineFactory pipelineFactory = new BasePipelineFactory(this, protocol) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ TrackerServer.this.addProtocolHandlers(pipeline);
+ }
+ };
+
+ if (datagram) {
+
+ this.bootstrap = new Bootstrap()
+ .group(EventLoopGroupFactory.getWorkerGroup())
+ .channel(NioDatagramChannel.class)
+ .handler(pipelineFactory);
+
+ } else {
+
+ this.bootstrap = new ServerBootstrap()
+ .group(EventLoopGroupFactory.getBossGroup(), EventLoopGroupFactory.getWorkerGroup())
+ .channel(NioServerSocketChannel.class)
+ .childHandler(pipelineFactory);
+
+ }
+ }
+
+ protected abstract void addProtocolHandlers(PipelineBuilder pipeline);
+
+ private int port;
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ private String address;
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ private final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
+
+ public ChannelGroup getChannelGroup() {
+ return channelGroup;
+ }
+
+ public void start() throws Exception {
+ InetSocketAddress endpoint;
+ if (address == null) {
+ endpoint = new InetSocketAddress(port);
+ } else {
+ endpoint = new InetSocketAddress(address, port);
+ }
+
+ Channel channel = bootstrap.bind(endpoint).sync().channel();
+ if (channel != null) {
+ getChannelGroup().add(channel);
+ }
+ }
+
+ public void stop() {
+ channelGroup.close().awaitUninterruptibly();
+ }
+
+}
diff --git a/src/main/java/org/traccar/WebDataHandler.java b/src/main/java/org/traccar/WebDataHandler.java
new file mode 100644
index 000000000..64396de03
--- /dev/null
+++ b/src/main/java/org/traccar/WebDataHandler.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.netty.channel.ChannelHandler;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.database.IdentityManager;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+import org.traccar.model.Group;
+
+import javax.inject.Inject;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation;
+import java.util.HashMap;
+import java.util.Map;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Calendar;
+import java.util.Formatter;
+import java.util.Locale;
+import java.util.TimeZone;
+
+@ChannelHandler.Sharable
+public class WebDataHandler extends BaseDataHandler {
+
+ private static final String KEY_POSITION = "position";
+ private static final String KEY_DEVICE = "device";
+
+ private final IdentityManager identityManager;
+ private final ObjectMapper objectMapper;
+ private final Client client;
+
+ private final String url;
+ private final String header;
+ private final boolean json;
+
+ @Inject
+ public WebDataHandler(
+ Config config, IdentityManager identityManager, ObjectMapper objectMapper, Client client) {
+ this.identityManager = identityManager;
+ this.objectMapper = objectMapper;
+ this.client = client;
+ this.url = config.getString(Keys.FORWARD_URL);
+ this.header = config.getString(Keys.FORWARD_HEADER);
+ this.json = config.getBoolean(Keys.FORWARD_JSON);
+ }
+
+ private static String formatSentence(Position position) {
+
+ StringBuilder s = new StringBuilder("$GPRMC,");
+
+ try (Formatter f = new Formatter(s, Locale.ENGLISH)) {
+
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ENGLISH);
+ calendar.setTimeInMillis(position.getFixTime().getTime());
+
+ f.format("%1$tH%1$tM%1$tS.%1$tL,A,", calendar);
+
+ double lat = position.getLatitude();
+ double lon = position.getLongitude();
+
+ f.format("%02d%07.4f,%c,", (int) Math.abs(lat), Math.abs(lat) % 1 * 60, lat < 0 ? 'S' : 'N');
+ f.format("%03d%07.4f,%c,", (int) Math.abs(lon), Math.abs(lon) % 1 * 60, lon < 0 ? 'W' : 'E');
+
+ f.format("%.2f,%.2f,", position.getSpeed(), position.getCourse());
+ f.format("%1$td%1$tm%1$ty,,", calendar);
+ }
+
+ s.append(Checksum.nmea(s.toString()));
+
+ return s.toString();
+ }
+
+ private String calculateStatus(Position position) {
+ if (position.getAttributes().containsKey(Position.KEY_ALARM)) {
+ return "0xF841"; // STATUS_PANIC_ON
+ } else if (position.getSpeed() < 1.0) {
+ return "0xF020"; // STATUS_LOCATION
+ } else {
+ return "0xF11C"; // STATUS_MOTION_MOVING
+ }
+ }
+
+ public String formatRequest(Position position) throws UnsupportedEncodingException, JsonProcessingException {
+
+ Device device = identityManager.getById(position.getDeviceId());
+
+ String request = url
+ .replace("{name}", URLEncoder.encode(device.getName(), StandardCharsets.UTF_8.name()))
+ .replace("{uniqueId}", device.getUniqueId())
+ .replace("{status}", device.getStatus())
+ .replace("{deviceId}", String.valueOf(position.getDeviceId()))
+ .replace("{protocol}", String.valueOf(position.getProtocol()))
+ .replace("{deviceTime}", String.valueOf(position.getDeviceTime().getTime()))
+ .replace("{fixTime}", String.valueOf(position.getFixTime().getTime()))
+ .replace("{valid}", String.valueOf(position.getValid()))
+ .replace("{latitude}", String.valueOf(position.getLatitude()))
+ .replace("{longitude}", String.valueOf(position.getLongitude()))
+ .replace("{altitude}", String.valueOf(position.getAltitude()))
+ .replace("{speed}", String.valueOf(position.getSpeed()))
+ .replace("{course}", String.valueOf(position.getCourse()))
+ .replace("{accuracy}", String.valueOf(position.getAccuracy()))
+ .replace("{statusCode}", calculateStatus(position));
+
+ if (position.getAddress() != null) {
+ request = request.replace(
+ "{address}", URLEncoder.encode(position.getAddress(), StandardCharsets.UTF_8.name()));
+ }
+
+ if (request.contains("{attributes}")) {
+ String attributes = objectMapper.writeValueAsString(position.getAttributes());
+ request = request.replace(
+ "{attributes}", URLEncoder.encode(attributes, StandardCharsets.UTF_8.name()));
+ }
+
+ if (request.contains("{gprmc}")) {
+ request = request.replace("{gprmc}", formatSentence(position));
+ }
+
+ if (request.contains("{group}")) {
+ String deviceGroupName = "";
+ if (device.getGroupId() != 0) {
+ Group group = Context.getGroupsManager().getById(device.getGroupId());
+ if (group != null) {
+ deviceGroupName = group.getName();
+ }
+ }
+
+ request = request.replace("{group}", URLEncoder.encode(deviceGroupName, StandardCharsets.UTF_8.name()));
+ }
+
+ return request;
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+
+ String url;
+ if (json) {
+ url = this.url;
+ } else {
+ try {
+ url = formatRequest(position);
+ } catch (UnsupportedEncodingException | JsonProcessingException e) {
+ throw new RuntimeException("Forwarding formatting error", e);
+ }
+ }
+
+ Invocation.Builder requestBuilder = client.target(url).request();
+
+ if (header != null && !header.isEmpty()) {
+ for (String line: header.split("\\r?\\n")) {
+ String[] values = line.split(":", 2);
+ requestBuilder.header(values[0].trim(), values[1].trim());
+ }
+ }
+
+ if (json) {
+ requestBuilder.async().post(Entity.json(prepareJsonPayload(position)));
+ } else {
+ requestBuilder.async().get();
+ }
+
+ return position;
+ }
+
+ private Map<String, Object> prepareJsonPayload(Position position) {
+
+ Map<String, Object> data = new HashMap<>();
+ Device device = identityManager.getById(position.getDeviceId());
+
+ data.put(KEY_POSITION, position);
+
+ if (device != null) {
+ data.put(KEY_DEVICE, device);
+ }
+
+ return data;
+ }
+
+}
diff --git a/src/main/java/org/traccar/WindowsService.java b/src/main/java/org/traccar/WindowsService.java
new file mode 100644
index 000000000..4a8955608
--- /dev/null
+++ b/src/main/java/org/traccar/WindowsService.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import com.sun.jna.Pointer;
+import com.sun.jna.platform.win32.Advapi32;
+import com.sun.jna.platform.win32.WinError;
+import com.sun.jna.platform.win32.WinNT;
+import com.sun.jna.platform.win32.Winsvc;
+import com.sun.jna.platform.win32.Winsvc.HandlerEx;
+import com.sun.jna.platform.win32.Winsvc.SC_HANDLE;
+import com.sun.jna.platform.win32.Winsvc.SERVICE_DESCRIPTION;
+import com.sun.jna.platform.win32.Winsvc.SERVICE_MAIN_FUNCTION;
+import com.sun.jna.platform.win32.Winsvc.SERVICE_STATUS;
+import com.sun.jna.platform.win32.Winsvc.SERVICE_STATUS_HANDLE;
+import com.sun.jna.platform.win32.Winsvc.SERVICE_TABLE_ENTRY;
+import jnr.posix.POSIXFactory;
+
+import java.io.File;
+import java.net.URISyntaxException;
+
+public abstract class WindowsService {
+
+ private static final Advapi32 ADVAPI_32 = Advapi32.INSTANCE;
+
+ private final Object waitObject = new Object();
+
+ private String serviceName;
+ private ServiceMain serviceMain;
+ private ServiceControl serviceControl;
+ private SERVICE_STATUS_HANDLE serviceStatusHandle;
+
+ public WindowsService(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ public boolean install(
+ String displayName, String description, String[] dependencies,
+ String account, String password, String config) throws URISyntaxException {
+
+ String javaHome = System.getProperty("java.home");
+ String javaBinary = javaHome + "\\bin\\java.exe";
+
+ File jar = new File(WindowsService.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ String command = javaBinary
+ + " -Duser.dir=\"" + jar.getParentFile().getAbsolutePath() + "\""
+ + " -jar \"" + jar.getAbsolutePath() + "\""
+ + " --service \"" + config + "\"";
+
+ boolean success = false;
+ StringBuilder dep = new StringBuilder();
+
+ if (dependencies != null) {
+ for (String s : dependencies) {
+ dep.append(s);
+ dep.append("\0");
+ }
+ }
+ dep.append("\0");
+
+ SERVICE_DESCRIPTION desc = new SERVICE_DESCRIPTION();
+ desc.lpDescription = description;
+
+ SC_HANDLE serviceManager = openServiceControlManager(null, Winsvc.SC_MANAGER_ALL_ACCESS);
+
+ if (serviceManager != null) {
+ SC_HANDLE service = ADVAPI_32.CreateService(serviceManager, serviceName, displayName,
+ Winsvc.SERVICE_ALL_ACCESS, WinNT.SERVICE_WIN32_OWN_PROCESS, WinNT.SERVICE_AUTO_START,
+ WinNT.SERVICE_ERROR_NORMAL,
+ command,
+ null, null, dep.toString(), account, password);
+
+ if (service != null) {
+ success = ADVAPI_32.ChangeServiceConfig2(service, Winsvc.SERVICE_CONFIG_DESCRIPTION, desc);
+ ADVAPI_32.CloseServiceHandle(service);
+ }
+ ADVAPI_32.CloseServiceHandle(serviceManager);
+ }
+ return success;
+ }
+
+ public boolean uninstall() {
+ boolean success = false;
+
+ SC_HANDLE serviceManager = openServiceControlManager(null, Winsvc.SC_MANAGER_ALL_ACCESS);
+
+ if (serviceManager != null) {
+ SC_HANDLE service = ADVAPI_32.OpenService(serviceManager, serviceName, Winsvc.SERVICE_ALL_ACCESS);
+
+ if (service != null) {
+ success = ADVAPI_32.DeleteService(service);
+ ADVAPI_32.CloseServiceHandle(service);
+ }
+ ADVAPI_32.CloseServiceHandle(serviceManager);
+ }
+ return success;
+ }
+
+ public boolean start() {
+ boolean success = false;
+
+ SC_HANDLE serviceManager = openServiceControlManager(null, WinNT.GENERIC_EXECUTE);
+
+ if (serviceManager != null) {
+ SC_HANDLE service = ADVAPI_32.OpenService(serviceManager, serviceName, WinNT.GENERIC_EXECUTE);
+
+ if (service != null) {
+ success = ADVAPI_32.StartService(service, 0, null);
+ ADVAPI_32.CloseServiceHandle(service);
+ }
+ ADVAPI_32.CloseServiceHandle(serviceManager);
+ }
+
+ return success;
+ }
+
+ public boolean stop() {
+ boolean success = false;
+
+ SC_HANDLE serviceManager = openServiceControlManager(null, WinNT.GENERIC_EXECUTE);
+
+ if (serviceManager != null) {
+ SC_HANDLE service = Advapi32.INSTANCE.OpenService(serviceManager, serviceName, WinNT.GENERIC_EXECUTE);
+
+ if (service != null) {
+ SERVICE_STATUS serviceStatus = new SERVICE_STATUS();
+ success = Advapi32.INSTANCE.ControlService(service, Winsvc.SERVICE_CONTROL_STOP, serviceStatus);
+ Advapi32.INSTANCE.CloseServiceHandle(service);
+ }
+ Advapi32.INSTANCE.CloseServiceHandle(serviceManager);
+ }
+
+ return success;
+ }
+
+ public void init() throws URISyntaxException {
+ String path = new File(
+ WindowsService.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent();
+
+ POSIXFactory.getPOSIX().chdir(path);
+
+ serviceMain = new ServiceMain();
+ SERVICE_TABLE_ENTRY entry = new SERVICE_TABLE_ENTRY();
+ entry.lpServiceName = serviceName;
+ entry.lpServiceProc = serviceMain;
+
+ Advapi32.INSTANCE.StartServiceCtrlDispatcher((SERVICE_TABLE_ENTRY[]) entry.toArray(2));
+ }
+
+ private SC_HANDLE openServiceControlManager(String machine, int access) {
+ return ADVAPI_32.OpenSCManager(machine, null, access);
+ }
+
+ private void reportStatus(int status, int win32ExitCode, int waitHint) {
+ SERVICE_STATUS serviceStatus = new SERVICE_STATUS();
+ serviceStatus.dwServiceType = WinNT.SERVICE_WIN32_OWN_PROCESS;
+ serviceStatus.dwControlsAccepted = Winsvc.SERVICE_ACCEPT_STOP | Winsvc.SERVICE_ACCEPT_SHUTDOWN;
+ serviceStatus.dwWin32ExitCode = win32ExitCode;
+ serviceStatus.dwWaitHint = waitHint;
+ serviceStatus.dwCurrentState = status;
+
+ ADVAPI_32.SetServiceStatus(serviceStatusHandle, serviceStatus);
+ }
+
+ public abstract void run();
+
+ private class ServiceMain implements SERVICE_MAIN_FUNCTION {
+
+ public void callback(int dwArgc, Pointer lpszArgv) {
+ serviceControl = new ServiceControl();
+ serviceStatusHandle = ADVAPI_32.RegisterServiceCtrlHandlerEx(serviceName, serviceControl, null);
+
+ reportStatus(Winsvc.SERVICE_START_PENDING, WinError.NO_ERROR, 3000);
+ reportStatus(Winsvc.SERVICE_RUNNING, WinError.NO_ERROR, 0);
+
+ Thread.currentThread().setContextClassLoader(WindowsService.class.getClassLoader());
+
+ run();
+
+ try {
+ synchronized (waitObject) {
+ waitObject.wait();
+ }
+ } catch (InterruptedException ex) {
+ }
+
+ reportStatus(Winsvc.SERVICE_STOPPED, WinError.NO_ERROR, 0);
+
+ // Avoid returning from ServiceMain, which will cause a crash
+ // See http://support.microsoft.com/kb/201349, which recommends
+ // having init() wait for this thread.
+ // Waiting on this thread in init() won't fix the crash, though.
+
+ System.exit(0);
+ }
+
+ }
+
+ private class ServiceControl implements HandlerEx {
+
+ public int callback(int dwControl, int dwEventType, Pointer lpEventData, Pointer lpContext) {
+ switch (dwControl) {
+ case Winsvc.SERVICE_CONTROL_STOP:
+ case Winsvc.SERVICE_CONTROL_SHUTDOWN:
+ reportStatus(Winsvc.SERVICE_STOP_PENDING, WinError.NO_ERROR, 5000);
+ synchronized (waitObject) {
+ waitObject.notifyAll();
+ }
+ break;
+ default:
+ break;
+ }
+ return WinError.NO_ERROR;
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/traccar/WrapperContext.java b/src/main/java/org/traccar/WrapperContext.java
new file mode 100644
index 000000000..372d3c60d
--- /dev/null
+++ b/src/main/java/org/traccar/WrapperContext.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.ChannelProgressivePromise;
+import io.netty.channel.ChannelPromise;
+import io.netty.util.Attribute;
+import io.netty.util.AttributeKey;
+import io.netty.util.concurrent.EventExecutor;
+
+import java.net.SocketAddress;
+
+public class WrapperContext implements ChannelHandlerContext {
+
+ private ChannelHandlerContext context;
+ private SocketAddress remoteAddress;
+
+ public WrapperContext(ChannelHandlerContext context, SocketAddress remoteAddress) {
+ this.context = context;
+ this.remoteAddress = remoteAddress;
+ }
+
+ @Override
+ public Channel channel() {
+ return context.channel();
+ }
+
+ @Override
+ public EventExecutor executor() {
+ return context.executor();
+ }
+
+ @Override
+ public String name() {
+ return context.name();
+ }
+
+ @Override
+ public ChannelHandler handler() {
+ return context.handler();
+ }
+
+ @Override
+ public boolean isRemoved() {
+ return context.isRemoved();
+ }
+
+ @Override
+ public ChannelHandlerContext fireChannelRegistered() {
+ return context.fireChannelRegistered();
+ }
+
+ @Override
+ public ChannelHandlerContext fireChannelUnregistered() {
+ return context.fireChannelUnregistered();
+ }
+
+ @Override
+ public ChannelHandlerContext fireChannelActive() {
+ return context.fireChannelActive();
+ }
+
+ @Override
+ public ChannelHandlerContext fireChannelInactive() {
+ return context.fireChannelInactive();
+ }
+
+ @Override
+ public ChannelHandlerContext fireExceptionCaught(Throwable cause) {
+ return context.fireExceptionCaught(cause);
+ }
+
+ @Override
+ public ChannelHandlerContext fireUserEventTriggered(Object evt) {
+ return context.fireUserEventTriggered(evt);
+ }
+
+ @Override
+ public ChannelHandlerContext fireChannelRead(Object msg) {
+ if (!(msg instanceof NetworkMessage)) {
+ msg = new NetworkMessage(msg, remoteAddress);
+ }
+ return context.fireChannelRead(msg);
+ }
+
+ @Override
+ public ChannelHandlerContext fireChannelReadComplete() {
+ return context.fireChannelReadComplete();
+ }
+
+ @Override
+ public ChannelHandlerContext fireChannelWritabilityChanged() {
+ return context.fireChannelWritabilityChanged();
+ }
+
+ @Override
+ public ChannelFuture bind(SocketAddress localAddress) {
+ return context.bind(localAddress);
+ }
+
+ @Override
+ public ChannelFuture connect(SocketAddress remoteAddress) {
+ return context.connect(remoteAddress);
+ }
+
+ @Override
+ public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
+ return context.connect(remoteAddress, localAddress);
+ }
+
+ @Override
+ public ChannelFuture disconnect() {
+ return context.disconnect();
+ }
+
+ @Override
+ public ChannelFuture close() {
+ return context.close();
+ }
+
+ @Override
+ public ChannelFuture deregister() {
+ return context.deregister();
+ }
+
+ @Override
+ public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
+ return context.bind(localAddress, promise);
+ }
+
+ @Override
+ public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
+ return context.connect(remoteAddress, promise);
+ }
+
+ @Override
+ public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
+ return context.connect(remoteAddress, localAddress, promise);
+ }
+
+ @Override
+ public ChannelFuture disconnect(ChannelPromise promise) {
+ return context.disconnect(promise);
+ }
+
+ @Override
+ public ChannelFuture close(ChannelPromise promise) {
+ return context.close(promise);
+ }
+
+ @Override
+ public ChannelFuture deregister(ChannelPromise promise) {
+ return context.deregister(promise);
+ }
+
+ @Override
+ public ChannelHandlerContext read() {
+ return context.read();
+ }
+
+ @Override
+ public ChannelFuture write(Object msg) {
+ return context.write(msg);
+ }
+
+ @Override
+ public ChannelFuture write(Object msg, ChannelPromise promise) {
+ if (!(msg instanceof NetworkMessage)) {
+ msg = new NetworkMessage(msg, remoteAddress);
+ }
+ return context.write(msg, promise);
+ }
+
+ @Override
+ public ChannelHandlerContext flush() {
+ return context.flush();
+ }
+
+ @Override
+ public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
+ return context.writeAndFlush(msg, promise);
+ }
+
+ @Override
+ public ChannelFuture writeAndFlush(Object msg) {
+ return context.writeAndFlush(msg);
+ }
+
+ @Override
+ public ChannelPromise newPromise() {
+ return context.newPromise();
+ }
+
+ @Override
+ public ChannelProgressivePromise newProgressivePromise() {
+ return context.newProgressivePromise();
+ }
+
+ @Override
+ public ChannelFuture newSucceededFuture() {
+ return context.newSucceededFuture();
+ }
+
+ @Override
+ public ChannelFuture newFailedFuture(Throwable cause) {
+ return context.newFailedFuture(cause);
+ }
+
+ @Override
+ public ChannelPromise voidPromise() {
+ return context.voidPromise();
+ }
+
+ @Override
+ public ChannelPipeline pipeline() {
+ return context.pipeline();
+ }
+
+ @Override
+ public ByteBufAllocator alloc() {
+ return context.alloc();
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public <T> Attribute<T> attr(AttributeKey<T> key) {
+ return context.attr(key);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public <T> boolean hasAttr(AttributeKey<T> key) {
+ return context.hasAttr(key);
+ }
+
+}
diff --git a/src/main/java/org/traccar/WrapperInboundHandler.java b/src/main/java/org/traccar/WrapperInboundHandler.java
new file mode 100644
index 000000000..ca33d021f
--- /dev/null
+++ b/src/main/java/org/traccar/WrapperInboundHandler.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandler;
+
+public class WrapperInboundHandler implements ChannelInboundHandler {
+
+ private ChannelInboundHandler handler;
+
+ public ChannelInboundHandler getWrappedHandler() {
+ return handler;
+ }
+
+ public WrapperInboundHandler(ChannelInboundHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
+ handler.channelRegistered(ctx);
+ }
+
+ @Override
+ public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
+ handler.channelUnregistered(ctx);
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+ handler.channelActive(ctx);
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ handler.channelInactive(ctx);
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ if (msg instanceof NetworkMessage) {
+ NetworkMessage nm = (NetworkMessage) msg;
+ handler.channelRead(new WrapperContext(ctx, nm.getRemoteAddress()), nm.getMessage());
+ } else {
+ handler.channelRead(ctx, msg);
+ }
+ }
+
+ @Override
+ public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+ handler.channelReadComplete(ctx);
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ handler.userEventTriggered(ctx, evt);
+ }
+
+ @Override
+ public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
+ handler.channelWritabilityChanged(ctx);
+ }
+
+ @Override
+ public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+ handler.handlerAdded(ctx);
+ }
+
+ @Override
+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+ handler.handlerRemoved(ctx);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ handler.exceptionCaught(ctx, cause);
+ }
+
+}
diff --git a/src/main/java/org/traccar/WrapperOutboundHandler.java b/src/main/java/org/traccar/WrapperOutboundHandler.java
new file mode 100644
index 000000000..0136c5b22
--- /dev/null
+++ b/src/main/java/org/traccar/WrapperOutboundHandler.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandler;
+import io.netty.channel.ChannelPromise;
+
+import java.net.SocketAddress;
+
+public class WrapperOutboundHandler implements ChannelOutboundHandler {
+
+ private ChannelOutboundHandler handler;
+
+ public ChannelOutboundHandler getWrappedHandler() {
+ return handler;
+ }
+
+ public WrapperOutboundHandler(ChannelOutboundHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
+ handler.bind(ctx, localAddress, promise);
+ }
+
+ @Override
+ public void connect(
+ ChannelHandlerContext ctx, SocketAddress remoteAddress,
+ SocketAddress localAddress, ChannelPromise promise) throws Exception {
+ handler.connect(ctx, remoteAddress, localAddress, promise);
+ }
+
+ @Override
+ public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+ handler.disconnect(ctx, promise);
+ }
+
+ @Override
+ public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+ handler.close(ctx, promise);
+ }
+
+ @Override
+ public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+ handler.deregister(ctx, promise);
+ }
+
+ @Override
+ public void read(ChannelHandlerContext ctx) throws Exception {
+ handler.read(ctx);
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+ if (msg instanceof NetworkMessage) {
+ NetworkMessage nm = (NetworkMessage) msg;
+ handler.write(new WrapperContext(ctx, nm.getRemoteAddress()), nm.getMessage(), promise);
+ } else {
+ handler.write(ctx, msg, promise);
+ }
+ }
+
+ @Override
+ public void flush(ChannelHandlerContext ctx) throws Exception {
+ handler.flush(ctx);
+ }
+
+ @Override
+ public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+ handler.handlerAdded(ctx);
+ }
+
+ @Override
+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+ handler.handlerRemoved(ctx);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ handler.exceptionCaught(ctx, cause);
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/AsyncSocket.java b/src/main/java/org/traccar/api/AsyncSocket.java
new file mode 100644
index 000000000..906d16b5b
--- /dev/null
+++ b/src/main/java/org/traccar/api/AsyncSocket.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.database.ConnectionManager;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.UpdateListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AsyncSocket.class);
+
+ private static final String KEY_DEVICES = "devices";
+ private static final String KEY_POSITIONS = "positions";
+ private static final String KEY_EVENTS = "events";
+
+ private long userId;
+
+ public AsyncSocket(long userId) {
+ this.userId = userId;
+ }
+
+ @Override
+ public void onWebSocketConnect(Session session) {
+ super.onWebSocketConnect(session);
+
+ Map<String, Collection<?>> data = new HashMap<>();
+ data.put(KEY_POSITIONS, Context.getDeviceManager().getInitialState(userId));
+ sendData(data);
+
+ Context.getConnectionManager().addListener(userId, this);
+ }
+
+ @Override
+ public void onWebSocketClose(int statusCode, String reason) {
+ super.onWebSocketClose(statusCode, reason);
+
+ Context.getConnectionManager().removeListener(userId, this);
+ }
+
+ @Override
+ public void onUpdateDevice(Device device) {
+ Map<String, Collection<?>> data = new HashMap<>();
+ data.put(KEY_DEVICES, Collections.singletonList(device));
+ sendData(data);
+ }
+
+ @Override
+ public void onUpdatePosition(Position position) {
+ Map<String, Collection<?>> data = new HashMap<>();
+ data.put(KEY_POSITIONS, Collections.singletonList(position));
+ sendData(data);
+ }
+
+ @Override
+ public void onUpdateEvent(Event event) {
+ Map<String, Collection<?>> data = new HashMap<>();
+ data.put(KEY_EVENTS, Collections.singletonList(event));
+ sendData(data);
+ }
+
+ private void sendData(Map<String, Collection<?>> data) {
+ if (!data.isEmpty() && isConnected()) {
+ try {
+ getRemote().sendString(Context.getObjectMapper().writeValueAsString(data), null);
+ } catch (JsonProcessingException e) {
+ LOGGER.warn("Socket JSON formatting error", e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/api/AsyncSocketServlet.java b/src/main/java/org/traccar/api/AsyncSocketServlet.java
new file mode 100644
index 000000000..9318b6fc6
--- /dev/null
+++ b/src/main/java/org/traccar/api/AsyncSocketServlet.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
+import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+import org.traccar.Context;
+import org.traccar.api.resource.SessionResource;
+
+public class AsyncSocketServlet extends WebSocketServlet {
+
+ private static final long ASYNC_TIMEOUT = 10 * 60 * 1000;
+
+ @Override
+ public void configure(WebSocketServletFactory factory) {
+ factory.getPolicy().setIdleTimeout(Context.getConfig().getLong("web.timeout", ASYNC_TIMEOUT));
+ factory.setCreator(new WebSocketCreator() {
+ @Override
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) {
+ if (req.getSession() != null) {
+ long userId = (Long) req.getSession().getAttribute(SessionResource.USER_ID_KEY);
+ return new AsyncSocket(userId);
+ } else {
+ return null;
+ }
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/BaseObjectResource.java b/src/main/java/org/traccar/api/BaseObjectResource.java
new file mode 100644
index 000000000..7de6a3877
--- /dev/null
+++ b/src/main/java/org/traccar/api/BaseObjectResource.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import java.sql.SQLException;
+import java.util.Set;
+
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Response;
+
+import org.traccar.Context;
+import org.traccar.database.BaseObjectManager;
+import org.traccar.database.ExtendedObjectManager;
+import org.traccar.database.ManagableObjects;
+import org.traccar.database.SimpleObjectManager;
+import org.traccar.helper.LogAction;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Calendar;
+import org.traccar.model.Command;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.model.GroupedModel;
+import org.traccar.model.ScheduledModel;
+import org.traccar.model.User;
+
+public abstract class BaseObjectResource<T extends BaseModel> extends BaseResource {
+
+ private Class<T> baseClass;
+
+ public BaseObjectResource(Class<T> baseClass) {
+ this.baseClass = baseClass;
+ }
+
+ protected final Class<T> getBaseClass() {
+ return baseClass;
+ }
+
+ protected final Set<Long> getSimpleManagerItems(BaseObjectManager<T> manager, boolean all, long userId) {
+ Set<Long> result = null;
+ if (all) {
+ if (Context.getPermissionsManager().getUserAdmin(getUserId())) {
+ result = manager.getAllItems();
+ } else {
+ Context.getPermissionsManager().checkManager(getUserId());
+ result = ((ManagableObjects) manager).getManagedItems(getUserId());
+ }
+ } else {
+ if (userId == 0) {
+ userId = getUserId();
+ }
+ Context.getPermissionsManager().checkUser(getUserId(), userId);
+ result = ((ManagableObjects) manager).getUserItems(userId);
+ }
+ return result;
+ }
+
+ @POST
+ public Response add(T entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ if (baseClass.equals(Device.class)) {
+ Context.getPermissionsManager().checkDeviceReadonly(getUserId());
+ Context.getPermissionsManager().checkDeviceLimit(getUserId());
+ } else if (baseClass.equals(Command.class)) {
+ Context.getPermissionsManager().checkLimitCommands(getUserId());
+ } else if (entity instanceof GroupedModel && ((GroupedModel) entity).getGroupId() != 0) {
+ Context.getPermissionsManager().checkPermission(
+ Group.class, getUserId(), ((GroupedModel) entity).getGroupId());
+ } else if (entity instanceof ScheduledModel && ((ScheduledModel) entity).getCalendarId() != 0) {
+ Context.getPermissionsManager().checkPermission(
+ Calendar.class, getUserId(), ((ScheduledModel) entity).getCalendarId());
+ }
+
+ BaseObjectManager<T> manager = Context.getManager(baseClass);
+ manager.addItem(entity);
+ LogAction.create(getUserId(), entity);
+
+ Context.getDataManager().linkObject(User.class, getUserId(), baseClass, entity.getId(), true);
+ LogAction.link(getUserId(), User.class, getUserId(), baseClass, entity.getId());
+
+ if (manager instanceof SimpleObjectManager) {
+ ((SimpleObjectManager<T>) manager).refreshUserItems();
+ } else if (baseClass.equals(Group.class) || baseClass.equals(Device.class)) {
+ Context.getPermissionsManager().refreshDeviceAndGroupPermissions();
+ Context.getPermissionsManager().refreshAllExtendedPermissions();
+ }
+ return Response.ok(entity).build();
+ }
+
+ @Path("{id}")
+ @PUT
+ public Response update(T entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ if (baseClass.equals(Device.class)) {
+ Context.getPermissionsManager().checkDeviceReadonly(getUserId());
+ } else if (baseClass.equals(User.class)) {
+ User before = Context.getPermissionsManager().getUser(entity.getId());
+ Context.getPermissionsManager().checkUserUpdate(getUserId(), before, (User) entity);
+ } else if (baseClass.equals(Command.class)) {
+ Context.getPermissionsManager().checkLimitCommands(getUserId());
+ } else if (entity instanceof GroupedModel && ((GroupedModel) entity).getGroupId() != 0) {
+ Context.getPermissionsManager().checkPermission(
+ Group.class, getUserId(), ((GroupedModel) entity).getGroupId());
+ } else if (entity instanceof ScheduledModel && ((ScheduledModel) entity).getCalendarId() != 0) {
+ Context.getPermissionsManager().checkPermission(
+ Calendar.class, getUserId(), ((ScheduledModel) entity).getCalendarId());
+ }
+ Context.getPermissionsManager().checkPermission(baseClass, getUserId(), entity.getId());
+
+ Context.getManager(baseClass).updateItem(entity);
+ LogAction.edit(getUserId(), entity);
+
+ if (baseClass.equals(Group.class) || baseClass.equals(Device.class)) {
+ Context.getPermissionsManager().refreshDeviceAndGroupPermissions();
+ Context.getPermissionsManager().refreshAllExtendedPermissions();
+ }
+ return Response.ok(entity).build();
+ }
+
+ @Path("{id}")
+ @DELETE
+ public Response remove(@PathParam("id") long id) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ if (baseClass.equals(Device.class)) {
+ Context.getPermissionsManager().checkDeviceReadonly(getUserId());
+ } else if (baseClass.equals(Command.class)) {
+ Context.getPermissionsManager().checkLimitCommands(getUserId());
+ }
+ Context.getPermissionsManager().checkPermission(baseClass, getUserId(), id);
+
+ BaseObjectManager<T> manager = Context.getManager(baseClass);
+ manager.removeItem(id);
+ LogAction.remove(getUserId(), baseClass, id);
+
+ if (manager instanceof SimpleObjectManager) {
+ ((SimpleObjectManager<T>) manager).refreshUserItems();
+ if (manager instanceof ExtendedObjectManager) {
+ ((ExtendedObjectManager<T>) manager).refreshExtendedPermissions();
+ }
+ }
+ if (baseClass.equals(Group.class) || baseClass.equals(Device.class) || baseClass.equals(User.class)) {
+ if (baseClass.equals(Group.class)) {
+ Context.getGroupsManager().updateGroupCache(true);
+ Context.getDeviceManager().updateDeviceCache(true);
+ }
+ Context.getPermissionsManager().refreshDeviceAndGroupPermissions();
+ if (baseClass.equals(User.class)) {
+ Context.getPermissionsManager().refreshAllUsersPermissions();
+ } else {
+ Context.getPermissionsManager().refreshAllExtendedPermissions();
+ }
+ } else if (baseClass.equals(Calendar.class)) {
+ Context.getGeofenceManager().refreshItems();
+ Context.getNotificationManager().refreshItems();
+ }
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/BaseResource.java b/src/main/java/org/traccar/api/BaseResource.java
new file mode 100644
index 000000000..cc272df9c
--- /dev/null
+++ b/src/main/java/org/traccar/api/BaseResource.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import javax.ws.rs.core.SecurityContext;
+
+public class BaseResource {
+
+ @javax.ws.rs.core.Context
+ private SecurityContext securityContext;
+
+ protected long getUserId() {
+ UserPrincipal principal = (UserPrincipal) securityContext.getUserPrincipal();
+ if (principal != null) {
+ return principal.getUserId();
+ }
+ return 0;
+ }
+}
diff --git a/src/main/java/org/traccar/api/CorsResponseFilter.java b/src/main/java/org/traccar/api/CorsResponseFilter.java
new file mode 100644
index 000000000..227f80609
--- /dev/null
+++ b/src/main/java/org/traccar/api/CorsResponseFilter.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import io.netty.handler.codec.http.HttpHeaderNames;
+import org.traccar.Context;
+
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
+import java.io.IOException;
+
+public class CorsResponseFilter implements ContainerResponseFilter {
+
+ private static final String ORIGIN_ALL = "*";
+ private static final String HEADERS_ALL = "origin, content-type, accept, authorization";
+ private static final String METHODS_ALL = "GET, POST, PUT, DELETE, OPTIONS";
+
+ @Override
+ public void filter(ContainerRequestContext request, ContainerResponseContext response) throws IOException {
+ if (!response.getHeaders().containsKey(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS.toString())) {
+ response.getHeaders().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS.toString(), HEADERS_ALL);
+ }
+
+ if (!response.getHeaders().containsKey(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS.toString())) {
+ response.getHeaders().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS.toString(), true);
+ }
+
+ if (!response.getHeaders().containsKey(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS.toString())) {
+ response.getHeaders().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS.toString(), METHODS_ALL);
+ }
+
+ if (!response.getHeaders().containsKey(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.toString())) {
+ String origin = request.getHeaderString(HttpHeaderNames.ORIGIN.toString());
+ String allowed = Context.getConfig().getString("web.origin");
+
+ if (origin == null) {
+ response.getHeaders().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.toString(), ORIGIN_ALL);
+ } else if (allowed == null || allowed.equals(ORIGIN_ALL) || allowed.contains(origin)) {
+ response.getHeaders().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.toString(), origin);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/ExtendedObjectResource.java b/src/main/java/org/traccar/api/ExtendedObjectResource.java
new file mode 100644
index 000000000..007a7b1bd
--- /dev/null
+++ b/src/main/java/org/traccar/api/ExtendedObjectResource.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.QueryParam;
+
+import org.traccar.Context;
+import org.traccar.database.ExtendedObjectManager;
+import org.traccar.model.BaseModel;
+
+public class ExtendedObjectResource<T extends BaseModel> extends BaseObjectResource<T> {
+
+ public ExtendedObjectResource(Class<T> baseClass) {
+ super(baseClass);
+ }
+
+ @GET
+ public Collection<T> get(
+ @QueryParam("all") boolean all, @QueryParam("userId") long userId, @QueryParam("groupId") long groupId,
+ @QueryParam("deviceId") long deviceId, @QueryParam("refresh") boolean refresh) throws SQLException {
+
+ ExtendedObjectManager<T> manager = (ExtendedObjectManager<T>) Context.getManager(getBaseClass());
+ if (refresh) {
+ manager.refreshItems();
+ }
+
+ Set<Long> result = new HashSet<>(getSimpleManagerItems(manager, all, userId));
+
+ if (groupId != 0) {
+ Context.getPermissionsManager().checkGroup(getUserId(), groupId);
+ result.retainAll(manager.getGroupItems(groupId));
+ }
+
+ if (deviceId != 0) {
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ result.retainAll(manager.getDeviceItems(deviceId));
+ }
+ return manager.getItems(result);
+
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/MediaFilter.java b/src/main/java/org/traccar/api/MediaFilter.java
new file mode 100644
index 000000000..53539770f
--- /dev/null
+++ b/src/main/java/org/traccar/api/MediaFilter.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import java.io.IOException;
+import java.sql.SQLException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.traccar.Context;
+import org.traccar.Main;
+import org.traccar.api.resource.SessionResource;
+import org.traccar.database.StatisticsManager;
+import org.traccar.helper.Log;
+import org.traccar.model.Device;
+
+public class MediaFilter implements Filter {
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ try {
+ HttpSession session = ((HttpServletRequest) request).getSession(false);
+ Long userId = null;
+ if (session != null) {
+ userId = (Long) session.getAttribute(SessionResource.USER_ID_KEY);
+ if (userId != null) {
+ Context.getPermissionsManager().checkUserEnabled(userId);
+ Main.getInjector().getInstance(StatisticsManager.class).registerRequest(userId);
+ }
+ }
+ if (userId == null) {
+ httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+
+ String path = ((HttpServletRequest) request).getPathInfo();
+ String[] parts = path.split("/");
+ if (parts.length < 2 || parts.length == 2 && !path.endsWith("/")) {
+ Context.getPermissionsManager().checkAdmin(userId);
+ } else {
+ Device device = Context.getDeviceManager().getByUniqueId(parts[1]);
+ if (device != null) {
+ Context.getPermissionsManager().checkDevice(userId, device.getId());
+ } else {
+ httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ }
+
+ chain.doFilter(request, response);
+ } catch (SecurityException e) {
+ httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ httpResponse.getWriter().println(Log.exceptionStack(e));
+ } catch (SQLException e) {
+ httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ httpResponse.getWriter().println(Log.exceptionStack(e));
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/ObjectMapperProvider.java b/src/main/java/org/traccar/api/ObjectMapperProvider.java
new file mode 100644
index 000000000..f81b20917
--- /dev/null
+++ b/src/main/java/org/traccar/api/ObjectMapperProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.traccar.Context;
+
+import javax.ws.rs.ext.ContextResolver;
+import javax.ws.rs.ext.Provider;
+
+@Provider
+public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
+
+ @Override
+ public ObjectMapper getContext(Class<?> type) {
+ return Context.getObjectMapper();
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/ResourceErrorHandler.java b/src/main/java/org/traccar/api/ResourceErrorHandler.java
new file mode 100644
index 000000000..1d618b08d
--- /dev/null
+++ b/src/main/java/org/traccar/api/ResourceErrorHandler.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import org.traccar.helper.Log;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+public class ResourceErrorHandler implements ExceptionMapper<Exception> {
+
+ @Override
+ public Response toResponse(Exception e) {
+ if (e instanceof WebApplicationException) {
+ WebApplicationException exception = (WebApplicationException) e;
+ String message;
+ if (exception.getCause() != null) {
+ message = Log.exceptionStack(exception.getCause());
+ } else {
+ message = Log.exceptionStack(exception);
+ }
+ return Response.fromResponse(exception.getResponse()).entity(message).build();
+ } else {
+ return Response.status(Response.Status.BAD_REQUEST).entity(Log.exceptionStack(e)).build();
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/SecurityRequestFilter.java b/src/main/java/org/traccar/api/SecurityRequestFilter.java
new file mode 100644
index 000000000..33b6b37df
--- /dev/null
+++ b/src/main/java/org/traccar/api/SecurityRequestFilter.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.Main;
+import org.traccar.api.resource.SessionResource;
+import org.traccar.database.StatisticsManager;
+import org.traccar.helper.DataConverter;
+import org.traccar.model.User;
+
+import javax.annotation.security.PermitAll;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.ResourceInfo;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+
+public class SecurityRequestFilter implements ContainerRequestFilter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SecurityRequestFilter.class);
+
+ public static final String AUTHORIZATION_HEADER = "Authorization";
+ public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+ public static final String BASIC_REALM = "Basic realm=\"api\"";
+ public static final String X_REQUESTED_WITH = "X-Requested-With";
+ public static final String XML_HTTP_REQUEST = "XMLHttpRequest";
+
+ public static String[] decodeBasicAuth(String auth) {
+ auth = auth.replaceFirst("[B|b]asic ", "");
+ byte[] decodedBytes = DataConverter.parseBase64(auth);
+ if (decodedBytes != null && decodedBytes.length > 0) {
+ return new String(decodedBytes, StandardCharsets.US_ASCII).split(":", 2);
+ }
+ return null;
+ }
+
+ @javax.ws.rs.core.Context
+ private HttpServletRequest request;
+
+ @javax.ws.rs.core.Context
+ private ResourceInfo resourceInfo;
+
+ @Override
+ public void filter(ContainerRequestContext requestContext) {
+
+ if (requestContext.getMethod().equals("OPTIONS")) {
+ return;
+ }
+
+ SecurityContext securityContext = null;
+
+ try {
+
+ String authHeader = requestContext.getHeaderString(AUTHORIZATION_HEADER);
+ if (authHeader != null) {
+
+ try {
+ String[] auth = decodeBasicAuth(authHeader);
+ User user = Context.getPermissionsManager().login(auth[0], auth[1]);
+ if (user != null) {
+ Main.getInjector().getInstance(StatisticsManager.class).registerRequest(user.getId());
+ securityContext = new UserSecurityContext(new UserPrincipal(user.getId()));
+ }
+ } catch (SQLException e) {
+ throw new WebApplicationException(e);
+ }
+
+ } else if (request.getSession() != null) {
+
+ Long userId = (Long) request.getSession().getAttribute(SessionResource.USER_ID_KEY);
+ if (userId != null) {
+ Context.getPermissionsManager().checkUserEnabled(userId);
+ Main.getInjector().getInstance(StatisticsManager.class).registerRequest(userId);
+ securityContext = new UserSecurityContext(new UserPrincipal(userId));
+ }
+
+ }
+
+ } catch (SecurityException e) {
+ LOGGER.warn("Authentication error", e);
+ }
+
+ if (securityContext != null) {
+ requestContext.setSecurityContext(securityContext);
+ } else {
+ Method method = resourceInfo.getResourceMethod();
+ if (!method.isAnnotationPresent(PermitAll.class)) {
+ Response.ResponseBuilder responseBuilder = Response.status(Response.Status.UNAUTHORIZED);
+ if (!XML_HTTP_REQUEST.equals(request.getHeader(X_REQUESTED_WITH))) {
+ responseBuilder.header(WWW_AUTHENTICATE, BASIC_REALM);
+ }
+ throw new WebApplicationException(responseBuilder.build());
+ }
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/SimpleObjectResource.java b/src/main/java/org/traccar/api/SimpleObjectResource.java
new file mode 100644
index 000000000..a7fcae0e7
--- /dev/null
+++ b/src/main/java/org/traccar/api/SimpleObjectResource.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import java.sql.SQLException;
+import java.util.Collection;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.QueryParam;
+
+import org.traccar.Context;
+import org.traccar.database.BaseObjectManager;
+import org.traccar.model.BaseModel;
+
+public class SimpleObjectResource<T extends BaseModel> extends BaseObjectResource<T> {
+
+ public SimpleObjectResource(Class<T> baseClass) {
+ super(baseClass);
+ }
+
+ @GET
+ public Collection<T> get(
+ @QueryParam("all") boolean all, @QueryParam("userId") long userId) throws SQLException {
+
+ BaseObjectManager<T> manager = Context.getManager(getBaseClass());
+ return manager.getItems(getSimpleManagerItems(manager, all, userId));
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/UserPrincipal.java b/src/main/java/org/traccar/api/UserPrincipal.java
new file mode 100644
index 000000000..80e92c2dd
--- /dev/null
+++ b/src/main/java/org/traccar/api/UserPrincipal.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import java.security.Principal;
+
+public class UserPrincipal implements Principal {
+
+ private long userId;
+
+ public UserPrincipal(long userId) {
+ this.userId = userId;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ @Override
+ public String getName() {
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/UserSecurityContext.java b/src/main/java/org/traccar/api/UserSecurityContext.java
new file mode 100644
index 000000000..55c0621bc
--- /dev/null
+++ b/src/main/java/org/traccar/api/UserSecurityContext.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api;
+
+import javax.ws.rs.core.SecurityContext;
+import java.security.Principal;
+
+public class UserSecurityContext implements SecurityContext {
+
+ private UserPrincipal principal;
+
+ public UserSecurityContext(UserPrincipal principal) {
+ this.principal = principal;
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return principal;
+ }
+
+ @Override
+ public boolean isUserInRole(String role) {
+ return true;
+ }
+
+ @Override
+ public boolean isSecure() {
+ return false;
+ }
+
+ @Override
+ public String getAuthenticationScheme() {
+ return BASIC_AUTH;
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/AttributeResource.java b/src/main/java/org/traccar/api/resource/AttributeResource.java
new file mode 100644
index 000000000..de69d871c
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/AttributeResource.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import java.sql.SQLException;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.traccar.Context;
+import org.traccar.api.ExtendedObjectResource;
+import org.traccar.model.Attribute;
+import org.traccar.model.Position;
+import org.traccar.handler.ComputedAttributesHandler;
+
+@Path("attributes/computed")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class AttributeResource extends ExtendedObjectResource<Attribute> {
+
+ public AttributeResource() {
+ super(Attribute.class);
+ }
+
+ @POST
+ @Path("test")
+ public Response test(@QueryParam("deviceId") long deviceId, Attribute entity) {
+ Context.getPermissionsManager().checkAdmin(getUserId());
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ Position last = Context.getIdentityManager().getLastPosition(deviceId);
+ if (last != null) {
+ Object result = new ComputedAttributesHandler(
+ Context.getConfig(),
+ Context.getIdentityManager(),
+ Context.getAttributesManager()).computeAttribute(entity, last);
+ if (result != null) {
+ switch (entity.getType()) {
+ case "number":
+ Number numberValue = (Number) result;
+ return Response.ok(numberValue).build();
+ case "boolean":
+ Boolean booleanValue = (Boolean) result;
+ return Response.ok(booleanValue).build();
+ default:
+ return Response.ok(result.toString()).build();
+ }
+ } else {
+ return Response.noContent().build();
+ }
+ } else {
+ throw new IllegalArgumentException("Device has no last position");
+ }
+ }
+
+ @POST
+ public Response add(Attribute entity) throws SQLException {
+ Context.getPermissionsManager().checkAdmin(getUserId());
+ return super.add(entity);
+ }
+
+ @Path("{id}")
+ @PUT
+ public Response update(Attribute entity) throws SQLException {
+ Context.getPermissionsManager().checkAdmin(getUserId());
+ return super.update(entity);
+ }
+
+ @Path("{id}")
+ @DELETE
+ public Response remove(@PathParam("id") long id) throws SQLException {
+ Context.getPermissionsManager().checkAdmin(getUserId());
+ return super.remove(id);
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/CalendarResource.java b/src/main/java/org/traccar/api/resource/CalendarResource.java
new file mode 100644
index 000000000..9399c34a5
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/CalendarResource.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.traccar.api.SimpleObjectResource;
+import org.traccar.model.Calendar;
+
+@Path("calendars")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class CalendarResource extends SimpleObjectResource<Calendar> {
+
+ public CalendarResource() {
+ super(Calendar.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/CommandResource.java b/src/main/java/org/traccar/api/resource/CommandResource.java
new file mode 100644
index 000000000..703638701
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/CommandResource.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Gabor Somogyi (gabor.g.somogyi@gmail.com)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.ExtendedObjectResource;
+import org.traccar.database.CommandsManager;
+import org.traccar.model.Command;
+import org.traccar.model.Typed;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("commands")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class CommandResource extends ExtendedObjectResource<Command> {
+
+ public CommandResource() {
+ super(Command.class);
+ }
+
+ @GET
+ @Path("send")
+ public Collection<Command> get(@QueryParam("deviceId") long deviceId) throws SQLException {
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ CommandsManager commandsManager = Context.getCommandsManager();
+ Set<Long> result = new HashSet<>(commandsManager.getUserItems(getUserId()));
+ result.retainAll(commandsManager.getSupportedCommands(deviceId));
+ return commandsManager.getItems(result);
+ }
+
+ @POST
+ @Path("send")
+ public Response send(Command entity) throws Exception {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ long deviceId = entity.getDeviceId();
+ long id = entity.getId();
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ if (id != 0) {
+ Context.getPermissionsManager().checkPermission(Command.class, getUserId(), id);
+ Context.getPermissionsManager().checkUserDeviceCommand(getUserId(), deviceId, id);
+ } else {
+ Context.getPermissionsManager().checkLimitCommands(getUserId());
+ }
+ if (!Context.getCommandsManager().sendCommand(entity)) {
+ return Response.accepted(entity).build();
+ }
+ return Response.ok(entity).build();
+ }
+
+ @GET
+ @Path("types")
+ public Collection<Typed> get(@QueryParam("deviceId") long deviceId,
+ @QueryParam("textChannel") boolean textChannel) {
+ if (deviceId != 0) {
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ return Context.getCommandsManager().getCommandTypes(deviceId, textChannel);
+ } else {
+ return Context.getCommandsManager().getAllCommandTypes();
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/api/resource/DeviceResource.java b/src/main/java/org/traccar/api/resource/DeviceResource.java
new file mode 100644
index 000000000..f9c9a139d
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/DeviceResource.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseObjectResource;
+import org.traccar.database.DeviceManager;
+import org.traccar.helper.LogAction;
+import org.traccar.model.Device;
+import org.traccar.model.DeviceAccumulators;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Path("devices")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class DeviceResource extends BaseObjectResource<Device> {
+
+ public DeviceResource() {
+ super(Device.class);
+ }
+
+ @GET
+ public Collection<Device> get(
+ @QueryParam("all") boolean all, @QueryParam("userId") long userId,
+ @QueryParam("uniqueId") List<String> uniqueIds,
+ @QueryParam("id") List<Long> deviceIds) throws SQLException {
+ DeviceManager deviceManager = Context.getDeviceManager();
+ Set<Long> result = null;
+ if (all) {
+ if (Context.getPermissionsManager().getUserAdmin(getUserId())) {
+ result = deviceManager.getAllItems();
+ } else {
+ Context.getPermissionsManager().checkManager(getUserId());
+ result = deviceManager.getManagedItems(getUserId());
+ }
+ } else if (uniqueIds.isEmpty() && deviceIds.isEmpty()) {
+ if (userId == 0) {
+ userId = getUserId();
+ }
+ Context.getPermissionsManager().checkUser(getUserId(), userId);
+ if (Context.getPermissionsManager().getUserAdmin(getUserId())) {
+ result = deviceManager.getAllUserItems(userId);
+ } else {
+ result = deviceManager.getUserItems(userId);
+ }
+ } else {
+ result = new HashSet<>();
+ for (String uniqueId : uniqueIds) {
+ Device device = deviceManager.getByUniqueId(uniqueId);
+ Context.getPermissionsManager().checkDevice(getUserId(), device.getId());
+ result.add(device.getId());
+ }
+ for (Long deviceId : deviceIds) {
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ result.add(deviceId);
+ }
+ }
+ return deviceManager.getItems(result);
+ }
+
+ @Path("{id}/accumulators")
+ @PUT
+ public Response updateAccumulators(DeviceAccumulators entity) throws SQLException {
+ if (!Context.getPermissionsManager().getUserAdmin(getUserId())) {
+ Context.getPermissionsManager().checkManager(getUserId());
+ Context.getPermissionsManager().checkPermission(Device.class, getUserId(), entity.getDeviceId());
+ }
+ Context.getDeviceManager().resetDeviceAccumulators(entity);
+ LogAction.resetDeviceAccumulators(getUserId(), entity.getDeviceId());
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/DriverResource.java b/src/main/java/org/traccar/api/resource/DriverResource.java
new file mode 100644
index 000000000..91aa54c5e
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/DriverResource.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.traccar.api.ExtendedObjectResource;
+import org.traccar.model.Driver;
+
+@Path("drivers")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class DriverResource extends ExtendedObjectResource<Driver> {
+
+ public DriverResource() {
+ super(Driver.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/EventResource.java b/src/main/java/org/traccar/api/resource/EventResource.java
new file mode 100644
index 000000000..e0ccf7020
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/EventResource.java
@@ -0,0 +1,38 @@
+package org.traccar.api.resource;
+
+import java.sql.SQLException;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.model.Event;
+import org.traccar.model.Geofence;
+import org.traccar.model.Maintenance;
+
+@Path("events")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+
+public class EventResource extends BaseResource {
+
+ @Path("{id}")
+ @GET
+ public Event get(@PathParam("id") long id) throws SQLException {
+ Event event = Context.getDataManager().getObject(Event.class, id);
+ Context.getPermissionsManager().checkDevice(getUserId(), event.getDeviceId());
+ if (event.getGeofenceId() != 0) {
+ Context.getPermissionsManager().checkPermission(Geofence.class, getUserId(), event.getGeofenceId());
+ }
+ if (event.getMaintenanceId() != 0) {
+ Context.getPermissionsManager().checkPermission(Maintenance.class, getUserId(), event.getMaintenanceId());
+ }
+ return event;
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/GeofenceResource.java b/src/main/java/org/traccar/api/resource/GeofenceResource.java
new file mode 100644
index 000000000..58f2c188c
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/GeofenceResource.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.api.ExtendedObjectResource;
+import org.traccar.model.Geofence;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("geofences")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class GeofenceResource extends ExtendedObjectResource<Geofence> {
+
+ public GeofenceResource() {
+ super(Geofence.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/GroupResource.java b/src/main/java/org/traccar/api/resource/GroupResource.java
new file mode 100644
index 000000000..fcea15d0a
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/GroupResource.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.api.SimpleObjectResource;
+import org.traccar.model.Group;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("groups")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class GroupResource extends SimpleObjectResource<Group> {
+
+ public GroupResource() {
+ super(Group.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/MaintenanceResource.java b/src/main/java/org/traccar/api/resource/MaintenanceResource.java
new file mode 100644
index 000000000..fa1b359ce
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/MaintenanceResource.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.traccar.api.ExtendedObjectResource;
+import org.traccar.model.Maintenance;
+
+@Path("maintenance")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class MaintenanceResource extends ExtendedObjectResource<Maintenance> {
+
+ public MaintenanceResource() {
+ super(Maintenance.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/NotificationResource.java b/src/main/java/org/traccar/api/resource/NotificationResource.java
new file mode 100644
index 000000000..9631a52b7
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/NotificationResource.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import java.util.Collection;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.traccar.Context;
+import org.traccar.api.ExtendedObjectResource;
+import org.traccar.model.Event;
+import org.traccar.model.Notification;
+import org.traccar.model.Typed;
+import org.traccar.notification.MessageException;
+
+
+@Path("notifications")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class NotificationResource extends ExtendedObjectResource<Notification> {
+
+ public NotificationResource() {
+ super(Notification.class);
+ }
+
+ @GET
+ @Path("types")
+ public Collection<Typed> get() {
+ return Context.getNotificationManager().getAllNotificationTypes();
+ }
+
+ @GET
+ @Path("notificators")
+ public Collection<Typed> getNotificators() {
+ return Context.getNotificatorManager().getAllNotificatorTypes();
+ }
+
+ @POST
+ @Path("test")
+ public Response testMessage() throws MessageException, InterruptedException {
+ for (Typed method : Context.getNotificatorManager().getAllNotificatorTypes()) {
+ Context.getNotificatorManager()
+ .getNotificator(method.getType()).sendSync(getUserId(), new Event("test", 0), null);
+ }
+ return Response.noContent().build();
+ }
+
+ @POST
+ @Path("test/{notificator}")
+ public Response testMessage(@PathParam("notificator") String notificator)
+ throws MessageException, InterruptedException {
+ Context.getNotificatorManager().getNotificator(notificator).sendSync(getUserId(), new Event("test", 0), null);
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/PermissionsResource.java b/src/main/java/org/traccar/api/resource/PermissionsResource.java
new file mode 100644
index 000000000..b89d9d376
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/PermissionsResource.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import java.sql.SQLException;
+import java.util.LinkedHashMap;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.helper.LogAction;
+import org.traccar.model.Device;
+import org.traccar.model.Permission;
+import org.traccar.model.User;
+
+@Path("permissions")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class PermissionsResource extends BaseResource {
+
+ private void checkPermission(Permission permission, boolean link) {
+ if (!link && permission.getOwnerClass().equals(User.class)
+ && permission.getPropertyClass().equals(Device.class)) {
+ if (getUserId() != permission.getOwnerId()) {
+ Context.getPermissionsManager().checkUser(getUserId(), permission.getOwnerId());
+ } else {
+ Context.getPermissionsManager().checkAdmin(getUserId());
+ }
+ } else {
+ Context.getPermissionsManager().checkPermission(
+ permission.getOwnerClass(), getUserId(), permission.getOwnerId());
+ }
+ Context.getPermissionsManager().checkPermission(
+ permission.getPropertyClass(), getUserId(), permission.getPropertyId());
+ }
+
+ @POST
+ public Response add(LinkedHashMap<String, Long> entity) throws SQLException, ClassNotFoundException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Permission permission = new Permission(entity);
+ checkPermission(permission, true);
+ Context.getDataManager().linkObject(permission.getOwnerClass(), permission.getOwnerId(),
+ permission.getPropertyClass(), permission.getPropertyId(), true);
+ LogAction.link(getUserId(), permission.getOwnerClass(), permission.getOwnerId(),
+ permission.getPropertyClass(), permission.getPropertyId());
+ Context.getPermissionsManager().refreshPermissions(permission);
+ return Response.noContent().build();
+ }
+
+ @DELETE
+ public Response remove(LinkedHashMap<String, Long> entity) throws SQLException, ClassNotFoundException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Permission permission = new Permission(entity);
+ checkPermission(permission, false);
+ Context.getDataManager().linkObject(permission.getOwnerClass(), permission.getOwnerId(),
+ permission.getPropertyClass(), permission.getPropertyId(), false);
+ LogAction.unlink(getUserId(), permission.getOwnerClass(), permission.getOwnerId(),
+ permission.getPropertyClass(), permission.getPropertyId());
+ Context.getPermissionsManager().refreshPermissions(permission);
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/PositionResource.java b/src/main/java/org/traccar/api/resource/PositionResource.java
new file mode 100644
index 000000000..c031b842f
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/PositionResource.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Position;
+import org.traccar.web.CsvBuilder;
+import org.traccar.web.GpxBuilder;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@Path("positions")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class PositionResource extends BaseResource {
+
+ public static final String TEXT_CSV = "text/csv";
+ public static final String CONTENT_DISPOSITION_VALUE_CSV = "attachment; filename=positions.csv";
+ public static final String GPX = "application/gpx+xml";
+ public static final String CONTENT_DISPOSITION_VALUE_GPX = "attachment; filename=positions.gpx";
+
+ @GET
+ public Collection<Position> getJson(
+ @QueryParam("deviceId") long deviceId, @QueryParam("id") List<Long> positionIds,
+ @QueryParam("from") String from, @QueryParam("to") String to)
+ throws SQLException {
+ if (!positionIds.isEmpty()) {
+ ArrayList<Position> positions = new ArrayList<>();
+ for (Long positionId : positionIds) {
+ Position position = Context.getDataManager().getObject(Position.class, positionId);
+ Context.getPermissionsManager().checkDevice(getUserId(), position.getDeviceId());
+ positions.add(position);
+ }
+ return positions;
+ } else if (deviceId == 0) {
+ return Context.getDeviceManager().getInitialState(getUserId());
+ } else {
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ return Context.getDataManager().getPositions(
+ deviceId, DateUtil.parseDate(from), DateUtil.parseDate(to));
+ }
+ }
+
+ @GET
+ @Produces(TEXT_CSV)
+ public Response getCsv(
+ @QueryParam("deviceId") long deviceId, @QueryParam("from") String from, @QueryParam("to") String to)
+ throws SQLException {
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ CsvBuilder csv = new CsvBuilder();
+ csv.addHeaderLine(new Position());
+ csv.addArray(Context.getDataManager().getPositions(
+ deviceId, DateUtil.parseDate(from), DateUtil.parseDate(to)));
+ return Response.ok(csv.build()).header(HttpHeaders.CONTENT_DISPOSITION, CONTENT_DISPOSITION_VALUE_CSV).build();
+ }
+
+ @GET
+ @Produces(GPX)
+ public Response getGpx(
+ @QueryParam("deviceId") long deviceId, @QueryParam("from") String from, @QueryParam("to") String to)
+ throws SQLException {
+ Context.getPermissionsManager().checkDevice(getUserId(), deviceId);
+ GpxBuilder gpx = new GpxBuilder(Context.getIdentityManager().getById(deviceId).getName());
+ gpx.addPositions(Context.getDataManager().getPositions(
+ deviceId, DateUtil.parseDate(from), DateUtil.parseDate(to)));
+ return Response.ok(gpx.build()).header(HttpHeaders.CONTENT_DISPOSITION, CONTENT_DISPOSITION_VALUE_GPX).build();
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/ReportResource.java b/src/main/java/org/traccar/api/resource/ReportResource.java
new file mode 100644
index 000000000..d371cf987
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/ReportResource.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.List;
+
+import javax.activation.DataHandler;
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.util.ByteArrayDataSource;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.reports.Events;
+import org.traccar.reports.Summary;
+import org.traccar.reports.Trips;
+import org.traccar.reports.model.StopReport;
+import org.traccar.reports.model.SummaryReport;
+import org.traccar.reports.model.TripReport;
+import org.traccar.reports.Route;
+import org.traccar.reports.Stops;
+
+@Path("reports")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class ReportResource extends BaseResource {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ReportResource.class);
+
+ private static final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+ private static final String CONTENT_DISPOSITION_VALUE_XLSX = "attachment; filename=report.xlsx";
+
+ private interface ReportExecutor {
+ void execute(ByteArrayOutputStream stream) throws SQLException, IOException;
+ }
+
+ private Response executeReport(
+ long userId, boolean mail, ReportExecutor executor) throws SQLException, IOException {
+ final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ if (mail) {
+ new Thread(() -> {
+ try {
+ executor.execute(stream);
+
+ MimeBodyPart attachment = new MimeBodyPart();
+
+ attachment.setFileName("report.xlsx");
+ attachment.setDataHandler(new DataHandler(new ByteArrayDataSource(
+ stream.toByteArray(), "application/octet-stream")));
+
+ Context.getMailManager().sendMessage(
+ userId, "Report", "The report is in the attachment.", attachment);
+ } catch (SQLException | IOException | MessagingException e) {
+ LOGGER.warn("Report failed", e);
+ }
+ }).start();
+ return Response.noContent().build();
+ } else {
+ executor.execute(stream);
+ return Response.ok(stream.toByteArray())
+ .header(HttpHeaders.CONTENT_DISPOSITION, CONTENT_DISPOSITION_VALUE_XLSX).build();
+ }
+ }
+
+ @Path("route")
+ @GET
+ public Collection<Position> getRoute(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("from") String from, @QueryParam("to") String to) throws SQLException {
+ return Route.getObjects(getUserId(), deviceIds, groupIds,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ }
+
+ @Path("route")
+ @GET
+ @Produces(XLSX)
+ public Response getRouteExcel(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("from") String from, @QueryParam("to") String to, @QueryParam("mail") boolean mail)
+ throws SQLException, IOException {
+ return executeReport(getUserId(), mail, stream -> {
+ Route.getExcel(stream, getUserId(), deviceIds, groupIds,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ });
+ }
+
+ @Path("events")
+ @GET
+ public Collection<Event> getEvents(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("type") final List<String> types,
+ @QueryParam("from") String from, @QueryParam("to") String to) throws SQLException {
+ return Events.getObjects(getUserId(), deviceIds, groupIds, types,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ }
+
+ @Path("events")
+ @GET
+ @Produces(XLSX)
+ public Response getEventsExcel(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("type") final List<String> types,
+ @QueryParam("from") String from, @QueryParam("to") String to, @QueryParam("mail") boolean mail)
+ throws SQLException, IOException {
+ return executeReport(getUserId(), mail, stream -> {
+ Events.getExcel(stream, getUserId(), deviceIds, groupIds, types,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ });
+ }
+
+ @Path("summary")
+ @GET
+ public Collection<SummaryReport> getSummary(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("from") String from, @QueryParam("to") String to) throws SQLException {
+ return Summary.getObjects(getUserId(), deviceIds, groupIds,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ }
+
+ @Path("summary")
+ @GET
+ @Produces(XLSX)
+ public Response getSummaryExcel(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("from") String from, @QueryParam("to") String to, @QueryParam("mail") boolean mail)
+ throws SQLException, IOException {
+ return executeReport(getUserId(), mail, stream -> {
+ Summary.getExcel(stream, getUserId(), deviceIds, groupIds,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ });
+ }
+
+ @Path("trips")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public Collection<TripReport> getTrips(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("from") String from, @QueryParam("to") String to) throws SQLException {
+ return Trips.getObjects(getUserId(), deviceIds, groupIds,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ }
+
+ @Path("trips")
+ @GET
+ @Produces(XLSX)
+ public Response getTripsExcel(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("from") String from, @QueryParam("to") String to, @QueryParam("mail") boolean mail)
+ throws SQLException, IOException {
+ return executeReport(getUserId(), mail, stream -> {
+ Trips.getExcel(stream, getUserId(), deviceIds, groupIds,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ });
+ }
+
+ @Path("stops")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public Collection<StopReport> getStops(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("from") String from, @QueryParam("to") String to) throws SQLException {
+ return Stops.getObjects(getUserId(), deviceIds, groupIds,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ }
+
+ @Path("stops")
+ @GET
+ @Produces(XLSX)
+ public Response getStopsExcel(
+ @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds,
+ @QueryParam("from") String from, @QueryParam("to") String to, @QueryParam("mail") boolean mail)
+ throws SQLException, IOException {
+ return executeReport(getUserId(), mail, stream -> {
+ Stops.getExcel(stream, getUserId(), deviceIds, groupIds,
+ DateUtil.parseDate(from), DateUtil.parseDate(to));
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/ServerResource.java b/src/main/java/org/traccar/api/resource/ServerResource.java
new file mode 100644
index 000000000..e7cad2a0c
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/ServerResource.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.helper.LogAction;
+import org.traccar.model.Server;
+
+import javax.annotation.security.PermitAll;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.sql.SQLException;
+
+@Path("server")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class ServerResource extends BaseResource {
+
+ @PermitAll
+ @GET
+ public Server get() throws SQLException {
+ return Context.getPermissionsManager().getServer();
+ }
+
+ @PUT
+ public Response update(Server entity) throws SQLException {
+ Context.getPermissionsManager().checkAdmin(getUserId());
+ Context.getPermissionsManager().updateServer(entity);
+ LogAction.edit(getUserId(), entity);
+ return Response.ok(entity).build();
+ }
+
+ @Path("geocode")
+ @GET
+ public String geocode(@QueryParam("latitude") double latitude, @QueryParam("longitude") double longitude) {
+ if (Context.getGeocoder() != null) {
+ return Context.getGeocoder().getAddress(latitude, longitude, null);
+ } else {
+ throw new RuntimeException("Reverse geocoding is not enabled");
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java
new file mode 100644
index 000000000..fd331c766
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/SessionResource.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.helper.DataConverter;
+import org.traccar.helper.LogAction;
+import org.traccar.model.User;
+
+import javax.annotation.security.PermitAll;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+
+@Path("session")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+public class SessionResource extends BaseResource {
+
+ public static final String USER_ID_KEY = "userId";
+ public static final String USER_COOKIE_KEY = "user";
+ public static final String PASS_COOKIE_KEY = "password";
+
+ @javax.ws.rs.core.Context
+ private HttpServletRequest request;
+
+ @PermitAll
+ @GET
+ public User get(@QueryParam("token") String token) throws SQLException, UnsupportedEncodingException {
+ Long userId = (Long) request.getSession().getAttribute(USER_ID_KEY);
+ if (userId == null) {
+ Cookie[] cookies = request.getCookies();
+ String email = null, password = null;
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(USER_COOKIE_KEY)) {
+ byte[] emailBytes = DataConverter.parseBase64(
+ URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII.name()));
+ email = new String(emailBytes, StandardCharsets.UTF_8);
+ } else if (cookie.getName().equals(PASS_COOKIE_KEY)) {
+ byte[] passwordBytes = DataConverter.parseBase64(
+ URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII.name()));
+ password = new String(passwordBytes, StandardCharsets.UTF_8);
+ }
+ }
+ }
+ if (email != null && password != null) {
+ User user = Context.getPermissionsManager().login(email, password);
+ if (user != null) {
+ userId = user.getId();
+ request.getSession().setAttribute(USER_ID_KEY, userId);
+ }
+ } else if (token != null) {
+ User user = Context.getUsersManager().getUserByToken(token);
+ if (user != null) {
+ userId = user.getId();
+ request.getSession().setAttribute(USER_ID_KEY, userId);
+ }
+ }
+ }
+
+ if (userId != null) {
+ Context.getPermissionsManager().checkUserEnabled(userId);
+ return Context.getPermissionsManager().getUser(userId);
+ } else {
+ throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
+ }
+ }
+
+ @PermitAll
+ @POST
+ public User add(
+ @FormParam("email") String email, @FormParam("password") String password) throws SQLException {
+ User user = Context.getPermissionsManager().login(email, password);
+ if (user != null) {
+ request.getSession().setAttribute(USER_ID_KEY, user.getId());
+ LogAction.login(user.getId());
+ return user;
+ } else {
+ throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).build());
+ }
+ }
+
+ @DELETE
+ public Response remove() {
+ LogAction.logout(getUserId());
+ request.getSession().removeAttribute(USER_ID_KEY);
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/StatisticsResource.java b/src/main/java/org/traccar/api/resource/StatisticsResource.java
new file mode 100644
index 000000000..e801d4ff3
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/StatisticsResource.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Statistics;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import java.sql.SQLException;
+import java.util.Collection;
+
+@Path("statistics")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class StatisticsResource extends BaseResource {
+
+ @GET
+ public Collection<Statistics> get(
+ @QueryParam("from") String from, @QueryParam("to") String to) throws SQLException {
+ Context.getPermissionsManager().checkAdmin(getUserId());
+ return Context.getDataManager().getStatistics(DateUtil.parseDate(from), DateUtil.parseDate(to));
+ }
+
+}
diff --git a/src/main/java/org/traccar/api/resource/UserResource.java b/src/main/java/org/traccar/api/resource/UserResource.java
new file mode 100644
index 000000000..0b42d8d92
--- /dev/null
+++ b/src/main/java/org/traccar/api/resource/UserResource.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseObjectResource;
+import org.traccar.database.UsersManager;
+import org.traccar.helper.LogAction;
+import org.traccar.model.ManagedUser;
+import org.traccar.model.User;
+
+import javax.annotation.security.PermitAll;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Set;
+
+@Path("users")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class UserResource extends BaseObjectResource<User> {
+
+ public UserResource() {
+ super(User.class);
+ }
+
+ @GET
+ public Collection<User> get(@QueryParam("userId") long userId) throws SQLException {
+ UsersManager usersManager = Context.getUsersManager();
+ Set<Long> result = null;
+ if (Context.getPermissionsManager().getUserAdmin(getUserId())) {
+ if (userId != 0) {
+ result = usersManager.getUserItems(userId);
+ } else {
+ result = usersManager.getAllItems();
+ }
+ } else if (Context.getPermissionsManager().getUserManager(getUserId())) {
+ result = usersManager.getManagedItems(getUserId());
+ } else {
+ throw new SecurityException("Admin or manager access required");
+ }
+ return usersManager.getItems(result);
+ }
+
+ @Override
+ @PermitAll
+ @POST
+ public Response add(User entity) throws SQLException {
+ if (!Context.getPermissionsManager().getUserAdmin(getUserId())) {
+ Context.getPermissionsManager().checkUserUpdate(getUserId(), new User(), entity);
+ if (Context.getPermissionsManager().getUserManager(getUserId())) {
+ Context.getPermissionsManager().checkUserLimit(getUserId());
+ } else {
+ Context.getPermissionsManager().checkRegistration(getUserId());
+ entity.setDeviceLimit(Context.getConfig().getInteger("users.defaultDeviceLimit", -1));
+ int expirationDays = Context.getConfig().getInteger("users.defaultExpirationDays");
+ if (expirationDays > 0) {
+ entity.setExpirationTime(
+ new Date(System.currentTimeMillis() + (long) expirationDays * 24 * 3600 * 1000));
+ }
+ }
+ }
+ Context.getUsersManager().addItem(entity);
+ LogAction.create(getUserId(), entity);
+ if (Context.getPermissionsManager().getUserManager(getUserId())) {
+ Context.getDataManager().linkObject(User.class, getUserId(), ManagedUser.class, entity.getId(), true);
+ LogAction.link(getUserId(), User.class, getUserId(), ManagedUser.class, entity.getId());
+ }
+ Context.getUsersManager().refreshUserItems();
+ return Response.ok(entity).build();
+ }
+
+}
diff --git a/src/main/java/org/traccar/config/Config.java b/src/main/java/org/traccar/config/Config.java
new file mode 100644
index 000000000..d8f2a0e99
--- /dev/null
+++ b/src/main/java/org/traccar/config/Config.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.config;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.InvalidPropertiesFormatException;
+import java.util.Properties;
+
+public class Config {
+
+ private final Properties properties = new Properties();
+
+ private boolean useEnvironmentVariables;
+
+ public Config() {
+ }
+
+ public Config(String file) throws IOException {
+ try {
+ Properties mainProperties = new Properties();
+ try (InputStream inputStream = new FileInputStream(file)) {
+ mainProperties.loadFromXML(inputStream);
+ }
+
+ String defaultConfigFile = mainProperties.getProperty("config.default");
+ if (defaultConfigFile != null) {
+ try (InputStream inputStream = new FileInputStream(defaultConfigFile)) {
+ properties.loadFromXML(inputStream);
+ }
+ }
+
+ properties.putAll(mainProperties); // override defaults
+
+ useEnvironmentVariables = Boolean.parseBoolean(System.getenv("CONFIG_USE_ENVIRONMENT_VARIABLES"))
+ || Boolean.parseBoolean(properties.getProperty("config.useEnvironmentVariables"));
+ } catch (InvalidPropertiesFormatException e) {
+ throw new RuntimeException("Configuration file is not a valid XML document", e);
+ }
+ }
+
+ public boolean hasKey(ConfigKey key) {
+ return hasKey(key.getKey());
+ }
+
+ @Deprecated
+ public boolean hasKey(String key) {
+ return useEnvironmentVariables && System.getenv().containsKey(getEnvironmentVariableName(key))
+ || properties.containsKey(key);
+ }
+
+ public String getString(ConfigKey key) {
+ return getString(key.getKey());
+ }
+
+ @Deprecated
+ public String getString(String key) {
+ if (useEnvironmentVariables) {
+ String value = System.getenv(getEnvironmentVariableName(key));
+ if (value != null && !value.isEmpty()) {
+ return value;
+ }
+ }
+ return properties.getProperty(key);
+ }
+
+ public String getString(ConfigKey key, String defaultValue) {
+ return getString(key.getKey(), defaultValue);
+ }
+
+ @Deprecated
+ public String getString(String key, String defaultValue) {
+ return hasKey(key) ? getString(key) : defaultValue;
+ }
+
+ public boolean getBoolean(ConfigKey key) {
+ return getBoolean(key.getKey());
+ }
+
+ @Deprecated
+ public boolean getBoolean(String key) {
+ return Boolean.parseBoolean(getString(key));
+ }
+
+ public int getInteger(ConfigKey key) {
+ return getInteger(key.getKey());
+ }
+
+ @Deprecated
+ public int getInteger(String key) {
+ return getInteger(key, 0);
+ }
+
+ public int getInteger(ConfigKey key, int defaultValue) {
+ return getInteger(key.getKey(), defaultValue);
+ }
+
+ @Deprecated
+ public int getInteger(String key, int defaultValue) {
+ return hasKey(key) ? Integer.parseInt(getString(key)) : defaultValue;
+ }
+
+ public long getLong(ConfigKey key) {
+ return getLong(key.getKey());
+ }
+
+ @Deprecated
+ public long getLong(String key) {
+ return getLong(key, 0);
+ }
+
+ public long getLong(ConfigKey key, long defaultValue) {
+ return getLong(key.getKey(), defaultValue);
+ }
+
+ @Deprecated
+ public long getLong(String key, long defaultValue) {
+ return hasKey(key) ? Long.parseLong(getString(key)) : defaultValue;
+ }
+
+ public double getDouble(ConfigKey key) {
+ return getDouble(key.getKey());
+ }
+
+ @Deprecated
+ public double getDouble(String key) {
+ return getDouble(key, 0.0);
+ }
+
+ public double getDouble(ConfigKey key, double defaultValue) {
+ return getDouble(key.getKey(), defaultValue);
+ }
+
+ @Deprecated
+ public double getDouble(String key, double defaultValue) {
+ return hasKey(key) ? Double.parseDouble(getString(key)) : defaultValue;
+ }
+
+ public void setString(ConfigKey key, String value) {
+ setString(key.getKey(), value);
+ }
+
+ @Deprecated
+ public void setString(String key, String value) {
+ properties.put(key, value);
+ }
+
+ static String getEnvironmentVariableName(String key) {
+ return key.replaceAll("\\.", "_").replaceAll("(\\p{Lu})", "_$1").toUpperCase();
+ }
+
+}
diff --git a/src/main/java/org/traccar/config/ConfigKey.java b/src/main/java/org/traccar/config/ConfigKey.java
new file mode 100644
index 000000000..2e54ad392
--- /dev/null
+++ b/src/main/java/org/traccar/config/ConfigKey.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.config;
+
+public class ConfigKey {
+
+ private final String key;
+ private final Class clazz;
+
+ ConfigKey(String key, Class clazz) {
+ this.key = key;
+ this.clazz = clazz;
+ }
+
+ String getKey() {
+ return key;
+ }
+
+ Class getValueClass() {
+ return clazz;
+ }
+
+}
diff --git a/src/main/java/org/traccar/config/ConfigSuffix.java b/src/main/java/org/traccar/config/ConfigSuffix.java
new file mode 100644
index 000000000..149b2cd00
--- /dev/null
+++ b/src/main/java/org/traccar/config/ConfigSuffix.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.config;
+
+public class ConfigSuffix extends ConfigKey {
+
+ ConfigSuffix(String key, Class clazz) {
+ super(key, clazz);
+ }
+
+ public ConfigKey withPrefix(String prefix) {
+ return new ConfigKey(prefix + getKey(), getValueClass());
+ }
+
+}
diff --git a/src/main/java/org/traccar/config/Keys.java b/src/main/java/org/traccar/config/Keys.java
new file mode 100644
index 000000000..48cf3e558
--- /dev/null
+++ b/src/main/java/org/traccar/config/Keys.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.config;
+
+public final class Keys {
+
+ /**
+ * Connection timeout value in seconds. Because sometimes there is no way to detect lost TCP connection old
+ * connections stay in open state. On most systems there is a limit on number of open connection, so this leads to
+ * problems with establishing new connections when number of devices is high or devices data connections are
+ * unstable.
+ */
+ public static final ConfigSuffix PROTOCOL_TIMEOUT = new ConfigSuffix(
+ ".timeout", Integer.class);
+
+ /**
+ * Server wide connection timeout value in seconds. See protocol timeout for more information.
+ */
+ public static final ConfigKey SERVER_TIMEOUT = new ConfigKey(
+ "server.timeout", Integer.class);
+
+ /**
+ * Address for uploading aggregated anonymous usage statistics. Uploaded information is the same you can see on the
+ * statistics screen in the web app. It does not include any sensitive (e.g. locations).
+ */
+ public static final ConfigKey SERVER_STATISTICS = new ConfigKey(
+ "server.statistics", Boolean.class);
+
+ /**
+ * Enable events subsystem. Flag to enable all events handlers.
+ */
+ public static final ConfigKey EVENT_ENABLE = new ConfigKey(
+ "event.enable", Boolean.class);
+
+ /**
+ * If true, the event is generated once at the beginning of overspeeding period.
+ */
+ public static final ConfigKey EVENT_OVERSPEED_NOT_REPEAT = new ConfigKey(
+ "event.overspeed.notRepeat", Boolean.class);
+
+ /**
+ * Minimal over speed duration to trigger the event. Value in seconds.
+ */
+ public static final ConfigKey EVENT_OVERSPEED_MINIMAL_DURATION = new ConfigKey(
+ "event.overspeed.minimalDuration", Long.class);
+
+ /**
+ * Relevant only for geofence speed limits. Use lowest speed limits from all geofences.
+ */
+ public static final ConfigKey EVENT_OVERSPEED_PREFER_LOWEST = new ConfigKey(
+ "event.overspeed.preferLowest", Boolean.class);
+
+ /**
+ * Do not generate alert event if same alert was present in last known location.
+ */
+ public static final ConfigKey EVENT_IGNORE_DUPLICATE_ALERTS = new ConfigKey(
+ "event.ignoreDuplicateAlerts", Boolean.class);
+
+ /**
+ * List of external handler classes to use in Netty pipeline.
+ */
+ public static final ConfigKey EXTRA_HANDLERS = new ConfigKey(
+ "extra.handlers", String.class);
+
+ /**
+ * Enable positions forwarding to other web server.
+ */
+ public static final ConfigKey FORWARD_ENABLE = new ConfigKey(
+ "forward.enable", Boolean.class);
+
+ /**
+ * URL to forward positions. Data is passed through URL parameters. For example, {uniqueId} for device identifier,
+ * {latitude} and {longitude} for coordinates.
+ */
+ public static final ConfigKey FORWARD_URL = new ConfigKey(
+ "forward.url", String.class);
+
+ /**
+ * Additional HTTP header, can be used for authorization.
+ */
+ public static final ConfigKey FORWARD_HEADER = new ConfigKey(
+ "forward.header", String.class);
+
+ /**
+ * Boolean value to enable forwarding in JSON format.
+ */
+ public static final ConfigKey FORWARD_JSON = new ConfigKey(
+ "forward.json", Boolean.class);
+
+ /**
+ * Boolean flag to enable or disable position filtering.
+ */
+ public static final ConfigKey FILTER_ENABLE = new ConfigKey(
+ "filter.enable", Boolean.class);
+
+ /**
+ * Filter invalid (valid field is set to false) positions.
+ */
+ public static final ConfigKey FILTER_INVALID = new ConfigKey(
+ "filter.invalid", Boolean.class);
+
+ /**
+ * Filter zero coordinates. Zero latitude and longitude are theoretically valid values, but it practice it usually
+ * indicates invalid GPS data.
+ */
+ public static final ConfigKey FILTER_ZERO = new ConfigKey(
+ "filter.zero", Boolean.class);
+
+ /**
+ * Filter duplicate records (duplicates are detected by time value).
+ */
+ public static final ConfigKey FILTER_DUPLICATE = new ConfigKey(
+ "filter.duplicate", Boolean.class);
+
+ /**
+ * Filter records with fix time in future. The values is specified in seconds. Records that have fix time more than
+ * specified number of seconds later than current server time would be filtered out.
+ */
+ public static final ConfigKey FILTER_FUTURE = new ConfigKey(
+ "filter.future", Long.class);
+
+ /**
+ * Filter positions with accuracy less than specified value in meters.
+ */
+ public static final ConfigKey FILTER_ACCURACY = new ConfigKey(
+ "filter.accuracy", Integer.class);
+
+ /**
+ * Filter cell and wifi locations that are coming from geolocation provider.
+ */
+ public static final ConfigKey FILTER_APPROXIMATE = new ConfigKey(
+ "filter.approximate", Boolean.class);
+
+ /**
+ * Filter positions with exactly zero speed values.
+ */
+ public static final ConfigKey FILTER_STATIC = new ConfigKey(
+ "filter.static", Boolean.class);
+
+ /**
+ * Filter records by distance. The values is specified in meters. If the new position is less far than this value
+ * from the last one it gets filtered out.
+ */
+ public static final ConfigKey FILTER_DISTANCE = new ConfigKey(
+ "filter.distance", Integer.class);
+
+ /**
+ * Filter records by Maximum Speed value in knots. Can be used to filter jumps to far locations even if they're
+ * marked as valid. Shouldn't be too low. Start testing with values at about 25000.
+ */
+ public static final ConfigKey FILTER_MAX_SPEED = new ConfigKey(
+ "filter.maxSpeed", Integer.class);
+
+ /**
+ * Filter position if time from previous position is less than specified value in seconds.
+ */
+ public static final ConfigKey FILTER_MIN_PERIOD = new ConfigKey(
+ "filter.minPeriod", Integer.class);
+
+ /**
+ * Time limit for the filtering in seconds. If the time difference between last position and a new one is more than
+ * this limit, the new position will not be filtered out.
+ */
+ public static final ConfigKey FILTER_SKIP_LIMIT = new ConfigKey(
+ "filter.skipLimit", Long.class);
+
+ /**
+ * Enable attributes skipping. Attribute skipping can be enabled in the config or device attributes.
+ */
+ public static final ConfigKey FILTER_SKIP_ATTRIBUTES_ENABLE = new ConfigKey(
+ "filter.skipAttributes.enable", Boolean.class);
+
+ /**
+ * Replaces coordinates with last known if change is less than a 'coordinates.error' meters. Helps to avoid
+ * coordinates jumps during parking period.
+ */
+ public static final ConfigKey COORDINATES_FILTER = new ConfigKey(
+ "coordinates.filter", Boolean.class);
+
+ /**
+ * Distance in meters. Distances below this value gets handled like explained in 'coordinates.filter'.
+ */
+ public static final ConfigKey COORDINATES_MIN_ERROR = new ConfigKey(
+ "coordinates.minError", Integer.class);
+
+ /**
+ * Distance in meters. Distances above this value gets handled like explained in 'coordinates.filter', but only if
+ * Position is also marked as 'invalid'.
+ */
+ public static final ConfigKey COORDINATES_MAX_ERROR = new ConfigKey(
+ "filter.maxError", Integer.class);
+
+ /**
+ * Enable to save device IP addresses information. Disabled by default.
+ */
+ public static final ConfigKey PROCESSING_REMOTE_ADDRESS_ENABLE = new ConfigKey(
+ "processing.remoteAddress.enable", Boolean.class);
+
+ /**
+ * Enable engine hours calculation on the server side. It uses ignition value to determine engine state.
+ */
+ public static final ConfigKey PROCESSING_ENGINE_HOURS_ENABLE = new ConfigKey(
+ "processing.engineHours.enable", Boolean.class);
+
+ /**
+ * Enable copying of missing attributes from last position to the current one. Might be useful if device doesn't
+ * send some values in every message.
+ */
+ public static final ConfigKey PROCESSING_COPY_ATTRIBUTES_ENABLE = new ConfigKey(
+ "processing.copyAttributes.enable", Boolean.class);
+
+ /**
+ * Enable computed attributes processing.
+ */
+ public static final ConfigKey PROCESSING_COMPUTED_ATTRIBUTES_ENABLE = new ConfigKey(
+ "processing.computedAttributes.enable", Boolean.class);
+
+ /**
+ * Enable computed attributes processing.
+ */
+ public static final ConfigKey PROCESSING_COMPUTED_ATTRIBUTES_DEVICE_ATTRIBUTES = new ConfigKey(
+ "processing.computedAttributes.deviceAttributes", Boolean.class);
+
+ /**
+ * Boolean flag to enable or disable reverse geocoder.
+ */
+ public static final ConfigKey GEOCODER_ENABLE = new ConfigKey(
+ "geocoder.enable", Boolean.class);
+
+ /**
+ * Reverse geocoder type. Check reverse geocoding documentation for more info. By default (if the value is not
+ * specified) server uses Google API.
+ */
+ public static final ConfigKey GEOCODER_TYPE = new ConfigKey(
+ "geocoder.type", String.class);
+
+ /**
+ * Geocoder server URL. Applicable only to Nominatim and Gisgraphy providers.
+ */
+ public static final ConfigKey GEOCODER_URL = new ConfigKey(
+ "geocoder.url", String.class);
+
+ /**
+ * App id for use with Here provider.
+ */
+ public static final ConfigKey GEOCODER_ID = new ConfigKey(
+ "geocoder.id", String.class);
+
+ /**
+ * Provider API key. Most providers require API keys.
+ */
+ public static final ConfigKey GEOCODER_KEY = new ConfigKey(
+ "geocoder.key", String.class);
+
+ /**
+ * Language parameter for providers that support localization (e.g. Google and Nominatim).
+ */
+ public static final ConfigKey GEOCODER_LANGUAGE = new ConfigKey(
+ "geocoder.language", String.class);
+
+ /**
+ * Address format string. Default value is %h %r, %t, %s, %c. See AddressFormat for more info.
+ */
+ public static final ConfigKey GEOCODER_FORMAT = new ConfigKey(
+ "geocoder.format", String.class);
+
+ /**
+ * Cache size for geocoding results.
+ */
+ public static final ConfigKey GEOCODER_CACHE_SIZE = new ConfigKey(
+ "geocoder.cacheSize", Integer.class);
+
+ /**
+ * Disable automatic reverse geocoding requests for all positions.
+ */
+ public static final ConfigKey GEOCODER_IGNORE_POSITIONS = new ConfigKey(
+ "geocoder.ignorePositions", Boolean.class);
+
+ /**
+ * Boolean flag to apply reverse geocoding to invalid positions.
+ */
+ public static final ConfigKey GEOCODER_PROCESS_INVALID_POSITIONS = new ConfigKey(
+ "geocoder.processInvalidPositions", Boolean.class);
+
+ /**
+ * Optional parameter to specify minimum distance for new reverse geocoding request. If distance is less than
+ * specified value (in meters), then Traccar will reuse last known address.
+ */
+ public static final ConfigKey GEOCODER_REUSE_DISTANCE = new ConfigKey(
+ "geocoder.reuseDistance", Integer.class);
+
+ /**
+ * Boolean flag to enable LBS location resolution. Some devices send cell towers information and WiFi point when GPS
+ * location is not available. Traccar can determine coordinates based on that information using third party
+ * services. Default value is false.
+ */
+ public static final ConfigKey GEOLOCATION_ENABLE = new ConfigKey(
+ "geolocation.enable", Boolean.class);
+
+ /**
+ * Provider to use for LBS location. Available options: google, mozilla and opencellid. By default opencellid is
+ * used. You have to supply a key that you get from corresponding provider. For more information see LBS geolocation
+ * documentation.
+ */
+ public static final ConfigKey GEOLOCATION_TYPE = new ConfigKey(
+ "geolocation.type", String.class);
+
+ /**
+ * Geolocation provider API URL address. Not required for most providers.
+ */
+ public static final ConfigKey GEOLOCATION_URL = new ConfigKey(
+ "geolocation.url", String.class);
+
+ /**
+ * Provider API key. OpenCellID service requires API key.
+ */
+ public static final ConfigKey GEOLOCATION_KEY = new ConfigKey(
+ "geolocation.key", String.class);
+
+ /**
+ * Boolean flag to apply geolocation to invalid positions.
+ */
+ public static final ConfigKey GEOLOCATION_PROCESS_INVALID_POSITIONS = new ConfigKey(
+ "geolocation.processInvalidPositions", Boolean.class);
+
+ /**
+ * Override latitude sign / hemisphere. Useful in cases where value is incorrect because of device bug. Value can be
+ * N for North or S for South.
+ */
+ public static final ConfigKey LOCATION_LATITUDE_HEMISPHERE = new ConfigKey(
+ "location.latitudeHemisphere", Boolean.class);
+
+ /**
+ * Override longitude sign / hemisphere. Useful in cases where value is incorrect because of device bug. Value can
+ * be E for East or W for West.
+ */
+ public static final ConfigKey LOCATION_LONGITUDE_HEMISPHERE = new ConfigKey(
+ "location.longitudeHemisphere", Boolean.class);
+
+ private Keys() {
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/ActiveDevice.java b/src/main/java/org/traccar/database/ActiveDevice.java
new file mode 100644
index 000000000..207fc454b
--- /dev/null
+++ b/src/main/java/org/traccar/database/ActiveDevice.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import io.netty.channel.Channel;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.model.Command;
+
+import java.net.SocketAddress;
+
+public class ActiveDevice {
+
+ private final long deviceId;
+ private final Protocol protocol;
+ private final Channel channel;
+ private final SocketAddress remoteAddress;
+
+ public ActiveDevice(long deviceId, Protocol protocol, Channel channel, SocketAddress remoteAddress) {
+ this.deviceId = deviceId;
+ this.protocol = protocol;
+ this.channel = channel;
+ this.remoteAddress = remoteAddress;
+ }
+
+ public Channel getChannel() {
+ return channel;
+ }
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void sendCommand(Command command) {
+ protocol.sendDataCommand(this, command);
+ }
+
+ public void write(Object message) {
+ channel.writeAndFlush(new NetworkMessage(message, remoteAddress));
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/AttributesManager.java b/src/main/java/org/traccar/database/AttributesManager.java
new file mode 100644
index 000000000..28816645a
--- /dev/null
+++ b/src/main/java/org/traccar/database/AttributesManager.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.traccar.model.Attribute;
+
+public class AttributesManager extends ExtendedObjectManager<Attribute> {
+
+ public AttributesManager(DataManager dataManager) {
+ super(dataManager, Attribute.class);
+ }
+
+ @Override
+ public void updateCachedItem(Attribute attribute) {
+ Attribute cachedAttribute = getById(attribute.getId());
+ cachedAttribute.setDescription(attribute.getDescription());
+ cachedAttribute.setAttribute(attribute.getAttribute());
+ cachedAttribute.setExpression(attribute.getExpression());
+ cachedAttribute.setType(attribute.getType());
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/BaseObjectManager.java b/src/main/java/org/traccar/database/BaseObjectManager.java
new file mode 100644
index 000000000..8bf9ef860
--- /dev/null
+++ b/src/main/java/org/traccar/database/BaseObjectManager.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.model.BaseModel;
+
+public class BaseObjectManager<T extends BaseModel> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BaseObjectManager.class);
+
+ private final DataManager dataManager;
+
+ private Map<Long, T> items;
+ private Class<T> baseClass;
+
+ protected BaseObjectManager(DataManager dataManager, Class<T> baseClass) {
+ this.dataManager = dataManager;
+ this.baseClass = baseClass;
+ refreshItems();
+ }
+
+ protected final DataManager getDataManager() {
+ return dataManager;
+ }
+
+ protected final Class<T> getBaseClass() {
+ return baseClass;
+ }
+
+ public T getById(long itemId) {
+ return items.get(itemId);
+ }
+
+ public void refreshItems() {
+ if (dataManager != null) {
+ try {
+ Collection<T> databaseItems = dataManager.getObjects(baseClass);
+ if (items == null) {
+ items = new ConcurrentHashMap<>(databaseItems.size());
+ }
+ Set<Long> databaseItemIds = new HashSet<>();
+ for (T item : databaseItems) {
+ databaseItemIds.add(item.getId());
+ if (items.containsKey(item.getId())) {
+ updateCachedItem(item);
+ } else {
+ addNewItem(item);
+ }
+ }
+ for (Long cachedItemId : items.keySet()) {
+ if (!databaseItemIds.contains(cachedItemId)) {
+ removeCachedItem(cachedItemId);
+ }
+ }
+ } catch (SQLException error) {
+ LOGGER.warn("Error refreshing items", error);
+ }
+ }
+ }
+
+ protected void addNewItem(T item) {
+ items.put(item.getId(), item);
+ }
+
+ public void addItem(T item) throws SQLException {
+ dataManager.addObject(item);
+ addNewItem(item);
+ }
+
+ protected void updateCachedItem(T item) {
+ items.put(item.getId(), item);
+ }
+
+ public void updateItem(T item) throws SQLException {
+ dataManager.updateObject(item);
+ updateCachedItem(item);
+ }
+
+ protected void removeCachedItem(long itemId) {
+ items.remove(itemId);
+ }
+
+ public void removeItem(long itemId) throws SQLException {
+ BaseModel item = getById(itemId);
+ if (item != null) {
+ dataManager.removeObject(baseClass, itemId);
+ removeCachedItem(itemId);
+ }
+ }
+
+ public final Collection<T> getItems(Set<Long> itemIds) {
+ Collection<T> result = new LinkedList<>();
+ for (long itemId : itemIds) {
+ result.add(getById(itemId));
+ }
+ return result;
+ }
+
+ public Set<Long> getAllItems() {
+ return items.keySet();
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/CalendarManager.java b/src/main/java/org/traccar/database/CalendarManager.java
new file mode 100644
index 000000000..44ced1082
--- /dev/null
+++ b/src/main/java/org/traccar/database/CalendarManager.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.traccar.model.Calendar;
+
+public class CalendarManager extends SimpleObjectManager<Calendar> {
+
+ public CalendarManager(DataManager dataManager) {
+ super(dataManager, Calendar.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/CommandsManager.java b/src/main/java/org/traccar/database/CommandsManager.java
new file mode 100644
index 000000000..d6fdd66ca
--- /dev/null
+++ b/src/main/java/org/traccar/database/CommandsManager.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseProtocol;
+import org.traccar.Context;
+import org.traccar.model.Command;
+import org.traccar.model.Typed;
+import org.traccar.model.Position;
+
+public class CommandsManager extends ExtendedObjectManager<Command> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CommandsManager.class);
+
+ private final Map<Long, Queue<Command>> deviceQueues = new ConcurrentHashMap<>();
+
+ private boolean queueing;
+
+ public CommandsManager(DataManager dataManager, boolean queueing) {
+ super(dataManager, Command.class);
+ this.queueing = queueing;
+ }
+
+ public boolean checkDeviceCommand(long deviceId, long commandId) {
+ return !getAllDeviceItems(deviceId).contains(commandId);
+ }
+
+ public boolean sendCommand(Command command) throws Exception {
+ long deviceId = command.getDeviceId();
+ if (command.getId() != 0) {
+ command = getById(command.getId()).clone();
+ command.setDeviceId(deviceId);
+ }
+ if (command.getTextChannel()) {
+ Position lastPosition = Context.getIdentityManager().getLastPosition(deviceId);
+ String phone = Context.getIdentityManager().getById(deviceId).getPhone();
+ if (lastPosition != null) {
+ BaseProtocol protocol = Context.getServerManager().getProtocol(lastPosition.getProtocol());
+ protocol.sendTextCommand(phone, command);
+ } else if (command.getType().equals(Command.TYPE_CUSTOM)) {
+ if (Context.getSmsManager() != null) {
+ Context.getSmsManager().sendMessageSync(phone, command.getString(Command.KEY_DATA), true);
+ } else {
+ throw new RuntimeException("SMS is not enabled");
+ }
+ } else {
+ throw new RuntimeException("Command " + command.getType() + " is not supported");
+ }
+ } else {
+ ActiveDevice activeDevice = Context.getConnectionManager().getActiveDevice(deviceId);
+ if (activeDevice != null) {
+ activeDevice.sendCommand(command);
+ } else if (!queueing) {
+ throw new RuntimeException("Device is not online");
+ } else {
+ getDeviceQueue(deviceId).add(command);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public Collection<Long> getSupportedCommands(long deviceId) {
+ List<Long> result = new ArrayList<>();
+ Position lastPosition = Context.getIdentityManager().getLastPosition(deviceId);
+ for (long commandId : getAllDeviceItems(deviceId)) {
+ Command command = getById(commandId);
+ if (lastPosition != null) {
+ BaseProtocol protocol = Context.getServerManager().getProtocol(lastPosition.getProtocol());
+ if (command.getTextChannel() && protocol.getSupportedTextCommands().contains(command.getType())
+ || !command.getTextChannel()
+ && protocol.getSupportedDataCommands().contains(command.getType())) {
+ result.add(commandId);
+ }
+ } else if (command.getType().equals(Command.TYPE_CUSTOM)) {
+ result.add(commandId);
+ }
+ }
+ return result;
+ }
+
+ public Collection<Typed> getCommandTypes(long deviceId, boolean textChannel) {
+ List<Typed> result = new ArrayList<>();
+ Position lastPosition = Context.getIdentityManager().getLastPosition(deviceId);
+ if (lastPosition != null) {
+ BaseProtocol protocol = Context.getServerManager().getProtocol(lastPosition.getProtocol());
+ Collection<String> commands;
+ commands = textChannel ? protocol.getSupportedTextCommands() : protocol.getSupportedDataCommands();
+ for (String commandKey : commands) {
+ result.add(new Typed(commandKey));
+ }
+ } else {
+ result.add(new Typed(Command.TYPE_CUSTOM));
+ }
+ return result;
+ }
+
+ public Collection<Typed> getAllCommandTypes() {
+ List<Typed> result = new ArrayList<>();
+ Field[] fields = Command.class.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers()) && field.getName().startsWith("TYPE_")) {
+ try {
+ result.add(new Typed(field.get(null).toString()));
+ } catch (IllegalArgumentException | IllegalAccessException error) {
+ LOGGER.warn("Get command types error", error);
+ }
+ }
+ }
+ return result;
+ }
+
+ private Queue<Command> getDeviceQueue(long deviceId) {
+ if (!deviceQueues.containsKey(deviceId)) {
+ deviceQueues.put(deviceId, new ConcurrentLinkedQueue<Command>());
+ }
+ return deviceQueues.get(deviceId);
+ }
+
+ public void sendQueuedCommands(ActiveDevice activeDevice) {
+ Queue<Command> deviceQueue = deviceQueues.get(activeDevice.getDeviceId());
+ if (deviceQueue != null) {
+ Command command = deviceQueue.poll();
+ while (command != null) {
+ activeDevice.sendCommand(command);
+ command = deviceQueue.poll();
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/ConnectionManager.java b/src/main/java/org/traccar/database/ConnectionManager.java
new file mode 100644
index 000000000..8bae1ea93
--- /dev/null
+++ b/src/main/java/org/traccar/database/ConnectionManager.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import io.netty.channel.Channel;
+import io.netty.util.Timeout;
+import io.netty.util.TimerTask;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.GlobalTimer;
+import org.traccar.Main;
+import org.traccar.Protocol;
+import org.traccar.handler.events.MotionEventHandler;
+import org.traccar.handler.events.OverspeedEventHandler;
+import org.traccar.model.Device;
+import org.traccar.model.DeviceState;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+public class ConnectionManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class);
+
+ private static final long DEFAULT_TIMEOUT = 600;
+
+ private final long deviceTimeout;
+ private final boolean enableStatusEvents;
+ private final boolean updateDeviceState;
+
+ private final Map<Long, ActiveDevice> activeDevices = new ConcurrentHashMap<>();
+ private final Map<Long, Set<UpdateListener>> listeners = new ConcurrentHashMap<>();
+ private final Map<Long, Timeout> timeouts = new ConcurrentHashMap<>();
+
+ public ConnectionManager() {
+ deviceTimeout = Context.getConfig().getLong("status.timeout", DEFAULT_TIMEOUT) * 1000;
+ enableStatusEvents = Context.getConfig().getBoolean("event.enable");
+ updateDeviceState = Context.getConfig().getBoolean("status.updateDeviceState");
+ }
+
+ public void addActiveDevice(long deviceId, Protocol protocol, Channel channel, SocketAddress remoteAddress) {
+ activeDevices.put(deviceId, new ActiveDevice(deviceId, protocol, channel, remoteAddress));
+ }
+
+ public void removeActiveDevice(Channel channel) {
+ for (ActiveDevice activeDevice : activeDevices.values()) {
+ if (activeDevice.getChannel() == channel) {
+ updateDevice(activeDevice.getDeviceId(), Device.STATUS_OFFLINE, null);
+ activeDevices.remove(activeDevice.getDeviceId());
+ break;
+ }
+ }
+ }
+
+ public ActiveDevice getActiveDevice(long deviceId) {
+ return activeDevices.get(deviceId);
+ }
+
+ public void updateDevice(final long deviceId, String status, Date time) {
+ Device device = Context.getIdentityManager().getById(deviceId);
+ if (device == null) {
+ return;
+ }
+
+ String oldStatus = device.getStatus();
+ device.setStatus(status);
+
+ if (enableStatusEvents && !status.equals(oldStatus)) {
+ String eventType;
+ Map<Event, Position> events = new HashMap<>();
+ switch (status) {
+ case Device.STATUS_ONLINE:
+ eventType = Event.TYPE_DEVICE_ONLINE;
+ break;
+ case Device.STATUS_UNKNOWN:
+ eventType = Event.TYPE_DEVICE_UNKNOWN;
+ if (updateDeviceState) {
+ events.putAll(updateDeviceState(deviceId));
+ }
+ break;
+ default:
+ eventType = Event.TYPE_DEVICE_OFFLINE;
+ if (updateDeviceState) {
+ events.putAll(updateDeviceState(deviceId));
+ }
+ break;
+ }
+ events.put(new Event(eventType, deviceId), null);
+ Context.getNotificationManager().updateEvents(events);
+ }
+
+ Timeout timeout = timeouts.remove(deviceId);
+ if (timeout != null) {
+ timeout.cancel();
+ }
+
+ if (time != null) {
+ device.setLastUpdate(time);
+ }
+
+ if (status.equals(Device.STATUS_ONLINE)) {
+ timeouts.put(deviceId, GlobalTimer.getTimer().newTimeout(new TimerTask() {
+ @Override
+ public void run(Timeout timeout) {
+ if (!timeout.isCancelled()) {
+ updateDevice(deviceId, Device.STATUS_UNKNOWN, null);
+ }
+ }
+ }, deviceTimeout, TimeUnit.MILLISECONDS));
+ }
+
+ try {
+ Context.getDeviceManager().updateDeviceStatus(device);
+ } catch (SQLException error) {
+ LOGGER.warn("Update device status error", error);
+ }
+
+ updateDevice(device);
+
+ if (status.equals(Device.STATUS_ONLINE) && !oldStatus.equals(Device.STATUS_ONLINE)) {
+ Context.getCommandsManager().sendQueuedCommands(getActiveDevice(deviceId));
+ }
+ }
+
+ public Map<Event, Position> updateDeviceState(long deviceId) {
+ DeviceState deviceState = Context.getDeviceManager().getDeviceState(deviceId);
+ Map<Event, Position> result = new HashMap<>();
+
+ Map<Event, Position> event = Main.getInjector()
+ .getInstance(MotionEventHandler.class).updateMotionState(deviceState);
+ if (event != null) {
+ result.putAll(event);
+ }
+
+ event = Main.getInjector().getInstance(OverspeedEventHandler.class)
+ .updateOverspeedState(deviceState, Context.getDeviceManager().
+ lookupAttributeDouble(deviceId, OverspeedEventHandler.ATTRIBUTE_SPEED_LIMIT, 0, false));
+ if (event != null) {
+ result.putAll(event);
+ }
+
+ return result;
+ }
+
+ public synchronized void updateDevice(Device device) {
+ for (long userId : Context.getPermissionsManager().getDeviceUsers(device.getId())) {
+ if (listeners.containsKey(userId)) {
+ for (UpdateListener listener : listeners.get(userId)) {
+ listener.onUpdateDevice(device);
+ }
+ }
+ }
+ }
+
+ public synchronized void updatePosition(Position position) {
+ long deviceId = position.getDeviceId();
+
+ for (long userId : Context.getPermissionsManager().getDeviceUsers(deviceId)) {
+ if (listeners.containsKey(userId)) {
+ for (UpdateListener listener : listeners.get(userId)) {
+ listener.onUpdatePosition(position);
+ }
+ }
+ }
+ }
+
+ public synchronized void updateEvent(long userId, Event event) {
+ if (listeners.containsKey(userId)) {
+ for (UpdateListener listener : listeners.get(userId)) {
+ listener.onUpdateEvent(event);
+ }
+ }
+ }
+
+ public interface UpdateListener {
+ void onUpdateDevice(Device device);
+ void onUpdatePosition(Position position);
+ void onUpdateEvent(Event event);
+ }
+
+ public synchronized void addListener(long userId, UpdateListener listener) {
+ if (!listeners.containsKey(userId)) {
+ listeners.put(userId, new HashSet<UpdateListener>());
+ }
+ listeners.get(userId).add(listener);
+ }
+
+ public synchronized void removeListener(long userId, UpdateListener listener) {
+ if (!listeners.containsKey(userId)) {
+ listeners.put(userId, new HashSet<UpdateListener>());
+ }
+ listeners.get(userId).remove(listener);
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/DataManager.java b/src/main/java/org/traccar/database/DataManager.java
new file mode 100644
index 000000000..8e9071736
--- /dev/null
+++ b/src/main/java/org/traccar/database/DataManager.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright 2012 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.beans.Introspector;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+
+import liquibase.Contexts;
+import liquibase.Liquibase;
+import liquibase.database.Database;
+import liquibase.database.DatabaseFactory;
+import liquibase.exception.LiquibaseException;
+import liquibase.resource.FileSystemResourceAccessor;
+import liquibase.resource.ResourceAccessor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.Context;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Attribute;
+import org.traccar.model.Device;
+import org.traccar.model.Driver;
+import org.traccar.model.Event;
+import org.traccar.model.Geofence;
+import org.traccar.model.Group;
+import org.traccar.model.Maintenance;
+import org.traccar.model.ManagedUser;
+import org.traccar.model.Notification;
+import org.traccar.model.Permission;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Calendar;
+import org.traccar.model.Command;
+import org.traccar.model.Position;
+import org.traccar.model.Server;
+import org.traccar.model.Statistics;
+import org.traccar.model.User;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+
+public class DataManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DataManager.class);
+
+ public static final String ACTION_SELECT_ALL = "selectAll";
+ public static final String ACTION_SELECT = "select";
+ public static final String ACTION_INSERT = "insert";
+ public static final String ACTION_UPDATE = "update";
+ public static final String ACTION_DELETE = "delete";
+
+ private final Config config;
+
+ private DataSource dataSource;
+
+ private boolean generateQueries;
+
+ private boolean forceLdap;
+
+ public DataManager(Config config) throws Exception {
+ this.config = config;
+
+ forceLdap = config.getBoolean("ldap.force");
+
+ initDatabase();
+ initDatabaseSchema();
+ }
+
+ private void initDatabase() throws Exception {
+
+ String jndiName = config.getString("database.jndi");
+
+ if (jndiName != null) {
+
+ dataSource = (DataSource) new InitialContext().lookup(jndiName);
+
+ } else {
+
+ String driverFile = config.getString("database.driverFile");
+ if (driverFile != null) {
+ ClassLoader classLoader = ClassLoader.getSystemClassLoader();
+ try {
+ Method method = classLoader.getClass().getDeclaredMethod("addURL", URL.class);
+ method.setAccessible(true);
+ method.invoke(classLoader, new File(driverFile).toURI().toURL());
+ } catch (NoSuchMethodException e) {
+ Method method = classLoader.getClass()
+ .getDeclaredMethod("appendToClassPathForInstrumentation", String.class);
+ method.setAccessible(true);
+ method.invoke(classLoader, driverFile);
+ }
+ }
+
+ String driver = config.getString("database.driver");
+ if (driver != null) {
+ Class.forName(driver);
+ }
+
+ HikariConfig hikariConfig = new HikariConfig();
+ hikariConfig.setDriverClassName(config.getString("database.driver"));
+ hikariConfig.setJdbcUrl(config.getString("database.url"));
+ hikariConfig.setUsername(config.getString("database.user"));
+ hikariConfig.setPassword(config.getString("database.password"));
+ hikariConfig.setConnectionInitSql(config.getString("database.checkConnection", "SELECT 1"));
+ hikariConfig.setIdleTimeout(600000);
+
+ int maxPoolSize = config.getInteger("database.maxPoolSize");
+
+ if (maxPoolSize != 0) {
+ hikariConfig.setMaximumPoolSize(maxPoolSize);
+ }
+
+ generateQueries = config.getBoolean("database.generateQueries");
+
+ dataSource = new HikariDataSource(hikariConfig);
+
+ }
+ }
+
+ public static String constructObjectQuery(String action, Class<?> clazz, boolean extended) {
+ switch (action) {
+ case ACTION_INSERT:
+ case ACTION_UPDATE:
+ StringBuilder result = new StringBuilder();
+ StringBuilder fields = new StringBuilder();
+ StringBuilder values = new StringBuilder();
+
+ Set<Method> methods = new HashSet<>(Arrays.asList(clazz.getMethods()));
+ methods.removeAll(Arrays.asList(Object.class.getMethods()));
+ methods.removeAll(Arrays.asList(BaseModel.class.getMethods()));
+ for (Method method : methods) {
+ boolean skip;
+ if (extended) {
+ skip = !method.isAnnotationPresent(QueryExtended.class);
+ } else {
+ skip = method.isAnnotationPresent(QueryIgnore.class)
+ || method.isAnnotationPresent(QueryExtended.class) && !action.equals(ACTION_INSERT);
+ }
+ if (!skip && method.getName().startsWith("get") && method.getParameterTypes().length == 0) {
+ String name = Introspector.decapitalize(method.getName().substring(3));
+ if (action.equals(ACTION_INSERT)) {
+ fields.append(name).append(", ");
+ values.append(":").append(name).append(", ");
+ } else {
+ fields.append(name).append(" = :").append(name).append(", ");
+ }
+ }
+ }
+ fields.setLength(fields.length() - 2);
+ if (action.equals(ACTION_INSERT)) {
+ values.setLength(values.length() - 2);
+ result.append("INSERT INTO ").append(getObjectsTableName(clazz)).append(" (");
+ result.append(fields).append(") ");
+ result.append("VALUES (").append(values).append(")");
+ } else {
+ result.append("UPDATE ").append(getObjectsTableName(clazz)).append(" SET ");
+ result.append(fields);
+ result.append(" WHERE id = :id");
+ }
+ return result.toString();
+ case ACTION_SELECT_ALL:
+ return "SELECT * FROM " + getObjectsTableName(clazz);
+ case ACTION_SELECT:
+ return "SELECT * FROM " + getObjectsTableName(clazz) + " WHERE id = :id";
+ case ACTION_DELETE:
+ return "DELETE FROM " + getObjectsTableName(clazz) + " WHERE id = :id";
+ default:
+ throw new IllegalArgumentException("Unknown action");
+ }
+ }
+
+ public static String constructPermissionQuery(String action, Class<?> owner, Class<?> property) {
+ switch (action) {
+ case ACTION_SELECT_ALL:
+ return "SELECT " + makeNameId(owner) + ", " + makeNameId(property) + " FROM "
+ + getPermissionsTableName(owner, property);
+ case ACTION_INSERT:
+ return "INSERT INTO " + getPermissionsTableName(owner, property)
+ + " (" + makeNameId(owner) + ", " + makeNameId(property) + ") VALUES (:"
+ + makeNameId(owner) + ", :" + makeNameId(property) + ")";
+ case ACTION_DELETE:
+ return "DELETE FROM " + getPermissionsTableName(owner, property)
+ + " WHERE " + makeNameId(owner) + " = :" + makeNameId(owner)
+ + " AND " + makeNameId(property) + " = :" + makeNameId(property);
+ default:
+ throw new IllegalArgumentException("Unknown action");
+ }
+ }
+
+ private String getQuery(String key) {
+ String query = config.getString(key);
+ if (query == null) {
+ LOGGER.info("Query not provided: " + key);
+ }
+ return query;
+ }
+
+ public String getQuery(String action, Class<?> clazz) {
+ return getQuery(action, clazz, false);
+ }
+
+ public String getQuery(String action, Class<?> clazz, boolean extended) {
+ String queryName;
+ if (action.equals(ACTION_SELECT_ALL)) {
+ queryName = "database.select" + clazz.getSimpleName() + "s";
+ } else {
+ queryName = "database." + action.toLowerCase() + clazz.getSimpleName();
+ if (extended) {
+ queryName += "Extended";
+ }
+ }
+ String query = config.getString(queryName);
+ if (query == null) {
+ if (generateQueries) {
+ query = constructObjectQuery(action, clazz, extended);
+ config.setString(queryName, query);
+ } else {
+ LOGGER.info("Query not provided: " + queryName);
+ }
+ }
+
+ return query;
+ }
+
+ public String getQuery(String action, Class<?> owner, Class<?> property) {
+ String queryName;
+ switch (action) {
+ case ACTION_SELECT_ALL:
+ queryName = "database.select" + owner.getSimpleName() + property.getSimpleName() + "s";
+ break;
+ case ACTION_INSERT:
+ queryName = "database.link" + owner.getSimpleName() + property.getSimpleName();
+ break;
+ default:
+ queryName = "database.unlink" + owner.getSimpleName() + property.getSimpleName();
+ break;
+ }
+ String query = config.getString(queryName);
+ if (query == null) {
+ if (generateQueries) {
+ query = constructPermissionQuery(action, owner,
+ property.equals(User.class) ? ManagedUser.class : property);
+ config.setString(queryName, query);
+ } else {
+ LOGGER.info("Query not provided: " + queryName);
+ }
+ }
+
+ return query;
+ }
+
+ private static String getPermissionsTableName(Class<?> owner, Class<?> property) {
+ String propertyName = property.getSimpleName();
+ if (propertyName.equals("ManagedUser")) {
+ propertyName = "User";
+ }
+ return "tc_" + Introspector.decapitalize(owner.getSimpleName())
+ + "_" + Introspector.decapitalize(propertyName);
+ }
+
+ private static String getObjectsTableName(Class<?> clazz) {
+ String result = "tc_" + Introspector.decapitalize(clazz.getSimpleName());
+ // Add "s" ending if object name is not plural already
+ if (!result.endsWith("s")) {
+ result += "s";
+ }
+ return result;
+ }
+
+ private void initDatabaseSchema() throws SQLException, LiquibaseException {
+
+ if (config.hasKey("database.changelog")) {
+
+ ResourceAccessor resourceAccessor = new FileSystemResourceAccessor();
+
+ Database database = DatabaseFactory.getInstance().openDatabase(
+ config.getString("database.url"),
+ config.getString("database.user"),
+ config.getString("database.password"),
+ config.getString("database.driver"),
+ null, null, null, resourceAccessor);
+
+ Liquibase liquibase = new Liquibase(
+ config.getString("database.changelog"), resourceAccessor, database);
+
+ liquibase.clearCheckSums();
+
+ liquibase.update(new Contexts());
+ }
+ }
+
+ public User login(String email, String password) throws SQLException {
+ User user = QueryBuilder.create(dataSource, getQuery("database.loginUser"))
+ .setString("email", email.trim())
+ .executeQuerySingle(User.class);
+ LdapProvider ldapProvider = Context.getLdapProvider();
+ if (user != null) {
+ if (ldapProvider != null && user.getLogin() != null && ldapProvider.login(user.getLogin(), password)
+ || !forceLdap && user.isPasswordValid(password)) {
+ return user;
+ }
+ } else {
+ if (ldapProvider != null && ldapProvider.login(email, password)) {
+ user = ldapProvider.getUser(email);
+ Context.getUsersManager().addItem(user);
+ return user;
+ }
+ }
+ return null;
+ }
+
+ public void updateDeviceStatus(Device device) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery(ACTION_UPDATE, Device.class, true))
+ .setObject(device)
+ .executeUpdate();
+ }
+
+ public Collection<Position> getPositions(long deviceId, Date from, Date to) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectPositions"))
+ .setLong("deviceId", deviceId)
+ .setDate("from", from)
+ .setDate("to", to)
+ .executeQuery(Position.class);
+ }
+
+ public void updateLatestPosition(Position position) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.updateLatestPosition"))
+ .setDate("now", new Date())
+ .setObject(position)
+ .executeUpdate();
+ }
+
+ public Collection<Position> getLatestPositions() throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectLatestPositions"))
+ .executeQuery(Position.class);
+ }
+
+ public void clearHistory() throws SQLException {
+ long historyDays = config.getInteger("database.historyDays");
+ if (historyDays != 0) {
+ Date timeLimit = new Date(System.currentTimeMillis() - historyDays * 24 * 3600 * 1000);
+ LOGGER.info("Clearing history earlier than " + DateUtil.formatDate(timeLimit, false));
+ QueryBuilder.create(dataSource, getQuery("database.deletePositions"))
+ .setDate("serverTime", timeLimit)
+ .executeUpdate();
+ QueryBuilder.create(dataSource, getQuery("database.deleteEvents"))
+ .setDate("serverTime", timeLimit)
+ .executeUpdate();
+ }
+ }
+
+ public Server getServer() throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery(ACTION_SELECT_ALL, Server.class))
+ .executeQuerySingle(Server.class);
+ }
+
+ public Collection<Event> getEvents(long deviceId, Date from, Date to) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectEvents"))
+ .setLong("deviceId", deviceId)
+ .setDate("from", from)
+ .setDate("to", to)
+ .executeQuery(Event.class);
+ }
+
+ public Collection<Statistics> getStatistics(Date from, Date to) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectStatistics"))
+ .setDate("from", from)
+ .setDate("to", to)
+ .executeQuery(Statistics.class);
+ }
+
+ public static Class<?> getClassByName(String name) throws ClassNotFoundException {
+ switch (name.toLowerCase().replace("id", "")) {
+ case "device":
+ return Device.class;
+ case "group":
+ return Group.class;
+ case "user":
+ return User.class;
+ case "manageduser":
+ return ManagedUser.class;
+ case "geofence":
+ return Geofence.class;
+ case "driver":
+ return Driver.class;
+ case "attribute":
+ return Attribute.class;
+ case "calendar":
+ return Calendar.class;
+ case "command":
+ return Command.class;
+ case "maintenance":
+ return Maintenance.class;
+ case "notification":
+ return Notification.class;
+ default:
+ throw new ClassNotFoundException();
+ }
+ }
+
+ private static String makeNameId(Class<?> clazz) {
+ String name = clazz.getSimpleName();
+ return Introspector.decapitalize(name) + (!name.contains("Id") ? "Id" : "");
+ }
+
+ public Collection<Permission> getPermissions(Class<? extends BaseModel> owner, Class<? extends BaseModel> property)
+ throws SQLException, ClassNotFoundException {
+ return QueryBuilder.create(dataSource, getQuery(ACTION_SELECT_ALL, owner, property))
+ .executePermissionsQuery();
+ }
+
+ public void linkObject(Class<?> owner, long ownerId, Class<?> property, long propertyId, boolean link)
+ throws SQLException {
+ QueryBuilder.create(dataSource, getQuery(link ? ACTION_INSERT : ACTION_DELETE, owner, property))
+ .setLong(makeNameId(owner), ownerId)
+ .setLong(makeNameId(property), propertyId)
+ .executeUpdate();
+ }
+
+ public <T extends BaseModel> T getObject(Class<T> clazz, long entityId) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery(ACTION_SELECT, clazz))
+ .setLong("id", entityId)
+ .executeQuerySingle(clazz);
+ }
+
+ public <T extends BaseModel> Collection<T> getObjects(Class<T> clazz) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery(ACTION_SELECT_ALL, clazz))
+ .executeQuery(clazz);
+ }
+
+ public void addObject(BaseModel entity) throws SQLException {
+ entity.setId(QueryBuilder.create(dataSource, getQuery(ACTION_INSERT, entity.getClass()), true)
+ .setObject(entity)
+ .executeUpdate());
+ }
+
+ public void updateObject(BaseModel entity) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery(ACTION_UPDATE, entity.getClass()))
+ .setObject(entity)
+ .executeUpdate();
+ if (entity instanceof User && ((User) entity).getHashedPassword() != null) {
+ QueryBuilder.create(dataSource, getQuery(ACTION_UPDATE, User.class, true))
+ .setObject(entity)
+ .executeUpdate();
+ }
+ }
+
+ public void removeObject(Class<? extends BaseModel> clazz, long entityId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery(ACTION_DELETE, clazz))
+ .setLong("id", entityId)
+ .executeUpdate();
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/DeviceManager.java b/src/main/java/org/traccar/database/DeviceManager.java
new file mode 100644
index 000000000..de4607d1f
--- /dev/null
+++ b/src/main/java/org/traccar/database/DeviceManager.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.DeviceState;
+import org.traccar.model.DeviceAccumulators;
+import org.traccar.model.Group;
+import org.traccar.model.Position;
+import org.traccar.model.Server;
+
+public class DeviceManager extends BaseObjectManager<Device> implements IdentityManager, ManagableObjects {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DeviceManager.class);
+
+ public static final long DEFAULT_REFRESH_DELAY = 300;
+
+ private final Config config;
+ private final long dataRefreshDelay;
+ private boolean lookupGroupsAttribute;
+
+ private Map<String, Device> devicesByUniqueId;
+ private Map<String, Device> devicesByPhone;
+ private AtomicLong devicesLastUpdate = new AtomicLong();
+
+ private final Map<Long, Position> positions = new ConcurrentHashMap<>();
+
+ private final Map<Long, DeviceState> deviceStates = new ConcurrentHashMap<>();
+
+ public DeviceManager(DataManager dataManager) {
+ super(dataManager, Device.class);
+ this.config = Context.getConfig();
+ if (devicesByPhone == null) {
+ devicesByPhone = new ConcurrentHashMap<>();
+ }
+ if (devicesByUniqueId == null) {
+ devicesByUniqueId = new ConcurrentHashMap<>();
+ }
+ dataRefreshDelay = config.getLong("database.refreshDelay", DEFAULT_REFRESH_DELAY) * 1000;
+ lookupGroupsAttribute = config.getBoolean("deviceManager.lookupGroupsAttribute");
+ refreshLastPositions();
+ }
+
+ @Override
+ public long addUnknownDevice(String uniqueId) {
+ Device device = new Device();
+ device.setName(uniqueId);
+ device.setUniqueId(uniqueId);
+ device.setCategory(Context.getConfig().getString("database.registerUnknown.defaultCategory"));
+
+ long defaultGroupId = Context.getConfig().getLong("database.registerUnknown.defaultGroupId");
+ if (defaultGroupId != 0) {
+ device.setGroupId(defaultGroupId);
+ }
+
+ try {
+ addItem(device);
+
+ LOGGER.info("Automatically registered device " + uniqueId);
+
+ if (defaultGroupId != 0) {
+ Context.getPermissionsManager().refreshDeviceAndGroupPermissions();
+ Context.getPermissionsManager().refreshAllExtendedPermissions();
+ }
+
+ return device.getId();
+ } catch (SQLException e) {
+ LOGGER.warn("Automatic device registration error", e);
+ return 0;
+ }
+ }
+
+ public void updateDeviceCache(boolean force) throws SQLException {
+ long lastUpdate = devicesLastUpdate.get();
+ if ((force || System.currentTimeMillis() - lastUpdate > dataRefreshDelay)
+ && devicesLastUpdate.compareAndSet(lastUpdate, System.currentTimeMillis())) {
+ refreshItems();
+ }
+ }
+
+ @Override
+ public Device getByUniqueId(String uniqueId) throws SQLException {
+ boolean forceUpdate = !devicesByUniqueId.containsKey(uniqueId) && !config.getBoolean("database.ignoreUnknown");
+
+ updateDeviceCache(forceUpdate);
+
+ return devicesByUniqueId.get(uniqueId);
+ }
+
+ public Device getDeviceByPhone(String phone) {
+ return devicesByPhone.get(phone);
+ }
+
+ @Override
+ public Set<Long> getAllItems() {
+ Set<Long> result = super.getAllItems();
+ if (result.isEmpty()) {
+ try {
+ updateDeviceCache(true);
+ } catch (SQLException e) {
+ LOGGER.warn("Update device cache error", e);
+ }
+ result = super.getAllItems();
+ }
+ return result;
+ }
+
+ public Collection<Device> getAllDevices() {
+ return getItems(getAllItems());
+ }
+
+ public Set<Long> getAllUserItems(long userId) {
+ return Context.getPermissionsManager().getDevicePermissions(userId);
+ }
+
+ @Override
+ public Set<Long> getUserItems(long userId) {
+ if (Context.getPermissionsManager() != null) {
+ Set<Long> result = new HashSet<>();
+ for (long deviceId : Context.getPermissionsManager().getDevicePermissions(userId)) {
+ Device device = getById(deviceId);
+ if (device != null && !device.getDisabled()) {
+ result.add(deviceId);
+ }
+ }
+ return result;
+ } else {
+ return new HashSet<>();
+ }
+ }
+
+ public Set<Long> getAllManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getAllUserItems(userId));
+ for (long managedUserId : Context.getUsersManager().getUserItems(userId)) {
+ result.addAll(getAllUserItems(managedUserId));
+ }
+ return result;
+ }
+
+ @Override
+ public Set<Long> getManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getUserItems(userId));
+ for (long managedUserId : Context.getUsersManager().getUserItems(userId)) {
+ result.addAll(getUserItems(managedUserId));
+ }
+ return result;
+ }
+
+ private void putUniqueDeviceId(Device device) {
+ if (devicesByUniqueId == null) {
+ devicesByUniqueId = new ConcurrentHashMap<>(getAllItems().size());
+ }
+ devicesByUniqueId.put(device.getUniqueId(), device);
+ }
+
+ private void putPhone(Device device) {
+ if (devicesByPhone == null) {
+ devicesByPhone = new ConcurrentHashMap<>(getAllItems().size());
+ }
+ devicesByPhone.put(device.getPhone(), device);
+ }
+
+ @Override
+ protected void addNewItem(Device device) {
+ super.addNewItem(device);
+ putUniqueDeviceId(device);
+ if (device.getPhone() != null && !device.getPhone().isEmpty()) {
+ putPhone(device);
+ }
+ if (Context.getGeofenceManager() != null) {
+ Position lastPosition = getLastPosition(device.getId());
+ if (lastPosition != null) {
+ device.setGeofenceIds(Context.getGeofenceManager().getCurrentDeviceGeofences(lastPosition));
+ }
+ }
+ }
+
+ @Override
+ protected void updateCachedItem(Device device) {
+ Device cachedDevice = getById(device.getId());
+ cachedDevice.setName(device.getName());
+ cachedDevice.setGroupId(device.getGroupId());
+ cachedDevice.setCategory(device.getCategory());
+ cachedDevice.setContact(device.getContact());
+ cachedDevice.setModel(device.getModel());
+ cachedDevice.setDisabled(device.getDisabled());
+ cachedDevice.setAttributes(device.getAttributes());
+ if (!device.getUniqueId().equals(cachedDevice.getUniqueId())) {
+ devicesByUniqueId.remove(cachedDevice.getUniqueId());
+ cachedDevice.setUniqueId(device.getUniqueId());
+ putUniqueDeviceId(cachedDevice);
+ }
+ if (device.getPhone() != null && !device.getPhone().isEmpty()
+ && !device.getPhone().equals(cachedDevice.getPhone())) {
+ String phone = cachedDevice.getPhone();
+ if (phone != null && !phone.isEmpty()) {
+ devicesByPhone.remove(phone);
+ }
+ cachedDevice.setPhone(device.getPhone());
+ putPhone(cachedDevice);
+ }
+ }
+
+ @Override
+ protected void removeCachedItem(long deviceId) {
+ Device cachedDevice = getById(deviceId);
+ if (cachedDevice != null) {
+ String deviceUniqueId = cachedDevice.getUniqueId();
+ String phone = cachedDevice.getPhone();
+ super.removeCachedItem(deviceId);
+ devicesByUniqueId.remove(deviceUniqueId);
+ if (phone != null && !phone.isEmpty()) {
+ devicesByPhone.remove(phone);
+ }
+ }
+ positions.remove(deviceId);
+ }
+
+ public void updateDeviceStatus(Device device) throws SQLException {
+ getDataManager().updateDeviceStatus(device);
+ Device cachedDevice = getById(device.getId());
+ if (cachedDevice != null) {
+ cachedDevice.setStatus(device.getStatus());
+ }
+ }
+
+ private void refreshLastPositions() {
+ if (getDataManager() != null) {
+ try {
+ for (Position position : getDataManager().getLatestPositions()) {
+ positions.put(position.getDeviceId(), position);
+ }
+ } catch (SQLException error) {
+ LOGGER.warn("Load latest positions error", error);
+ }
+ }
+ }
+
+ public boolean isLatestPosition(Position position) {
+ Position lastPosition = getLastPosition(position.getDeviceId());
+ return lastPosition == null || position.getFixTime().compareTo(lastPosition.getFixTime()) >= 0;
+ }
+
+ public void updateLatestPosition(Position position) throws SQLException {
+
+ if (isLatestPosition(position)) {
+
+ getDataManager().updateLatestPosition(position);
+
+ Device device = getById(position.getDeviceId());
+ if (device != null) {
+ device.setPositionId(position.getId());
+ }
+
+ positions.put(position.getDeviceId(), position);
+
+ if (Context.getConnectionManager() != null) {
+ Context.getConnectionManager().updatePosition(position);
+ }
+ }
+ }
+
+ @Override
+ public Position getLastPosition(long deviceId) {
+ return positions.get(deviceId);
+ }
+
+ public Collection<Position> getInitialState(long userId) {
+
+ List<Position> result = new LinkedList<>();
+
+ if (Context.getPermissionsManager() != null) {
+ for (long deviceId : Context.getPermissionsManager().getUserAdmin(userId)
+ ? getAllUserItems(userId) : getUserItems(userId)) {
+ if (positions.containsKey(deviceId)) {
+ result.add(positions.get(deviceId));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean lookupAttributeBoolean(
+ long deviceId, String attributeName, boolean defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ if (result != null) {
+ return result instanceof String ? Boolean.parseBoolean((String) result) : (Boolean) result;
+ }
+ return defaultValue;
+ }
+
+ @Override
+ public String lookupAttributeString(
+ long deviceId, String attributeName, String defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ return result != null ? (String) result : defaultValue;
+ }
+
+ @Override
+ public int lookupAttributeInteger(long deviceId, String attributeName, int defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ if (result != null) {
+ return result instanceof String ? Integer.parseInt((String) result) : ((Number) result).intValue();
+ }
+ return defaultValue;
+ }
+
+ @Override
+ public long lookupAttributeLong(
+ long deviceId, String attributeName, long defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ if (result != null) {
+ return result instanceof String ? Long.parseLong((String) result) : ((Number) result).longValue();
+ }
+ return defaultValue;
+ }
+
+ public double lookupAttributeDouble(
+ long deviceId, String attributeName, double defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ if (result != null) {
+ return result instanceof String ? Double.parseDouble((String) result) : ((Number) result).doubleValue();
+ }
+ return defaultValue;
+ }
+
+ private Object lookupAttribute(long deviceId, String attributeName, boolean lookupConfig) {
+ Object result = null;
+ Device device = getById(deviceId);
+ if (device != null) {
+ result = device.getAttributes().get(attributeName);
+ if (result == null && lookupGroupsAttribute) {
+ long groupId = device.getGroupId();
+ while (groupId != 0) {
+ Group group = Context.getGroupsManager().getById(groupId);
+ if (group != null) {
+ result = group.getAttributes().get(attributeName);
+ if (result != null) {
+ break;
+ }
+ groupId = group.getGroupId();
+ } else {
+ groupId = 0;
+ }
+ }
+ }
+ if (result == null) {
+ if (lookupConfig) {
+ result = Context.getConfig().getString(attributeName);
+ } else {
+ Server server = Context.getPermissionsManager().getServer();
+ result = server.getAttributes().get(attributeName);
+ }
+ }
+ }
+ return result;
+ }
+
+ public void resetDeviceAccumulators(DeviceAccumulators deviceAccumulators) throws SQLException {
+ Position last = positions.get(deviceAccumulators.getDeviceId());
+ if (last != null) {
+ if (deviceAccumulators.getTotalDistance() != null) {
+ last.getAttributes().put(Position.KEY_TOTAL_DISTANCE, deviceAccumulators.getTotalDistance());
+ }
+ if (deviceAccumulators.getHours() != null) {
+ last.getAttributes().put(Position.KEY_HOURS, deviceAccumulators.getHours());
+ }
+ getDataManager().addObject(last);
+ updateLatestPosition(last);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public DeviceState getDeviceState(long deviceId) {
+ DeviceState deviceState = deviceStates.get(deviceId);
+ if (deviceState == null) {
+ deviceState = new DeviceState();
+ deviceStates.put(deviceId, deviceState);
+ }
+ return deviceState;
+ }
+
+ public void setDeviceState(long deviceId, DeviceState deviceState) {
+ deviceStates.put(deviceId, deviceState);
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/DriversManager.java b/src/main/java/org/traccar/database/DriversManager.java
new file mode 100644
index 000000000..930951460
--- /dev/null
+++ b/src/main/java/org/traccar/database/DriversManager.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.traccar.model.Driver;
+
+public class DriversManager extends ExtendedObjectManager<Driver> {
+
+ private Map<String, Driver> driversByUniqueId;
+
+ public DriversManager(DataManager dataManager) {
+ super(dataManager, Driver.class);
+ if (driversByUniqueId == null) {
+ driversByUniqueId = new ConcurrentHashMap<>();
+ }
+ }
+
+ private void putUniqueDriverId(Driver driver) {
+ if (driversByUniqueId == null) {
+ driversByUniqueId = new ConcurrentHashMap<>(getAllItems().size());
+ }
+ driversByUniqueId.put(driver.getUniqueId(), driver);
+ }
+
+ @Override
+ protected void addNewItem(Driver driver) {
+ super.addNewItem(driver);
+ putUniqueDriverId(driver);
+ }
+
+ @Override
+ protected void updateCachedItem(Driver driver) {
+ Driver cachedDriver = getById(driver.getId());
+ cachedDriver.setName(driver.getName());
+ if (!driver.getUniqueId().equals(cachedDriver.getUniqueId())) {
+ driversByUniqueId.remove(cachedDriver.getUniqueId());
+ cachedDriver.setUniqueId(driver.getUniqueId());
+ putUniqueDriverId(cachedDriver);
+ }
+ cachedDriver.setAttributes(driver.getAttributes());
+ }
+
+ @Override
+ protected void removeCachedItem(long driverId) {
+ Driver cachedDriver = getById(driverId);
+ if (cachedDriver != null) {
+ String driverUniqueId = cachedDriver.getUniqueId();
+ super.removeCachedItem(driverId);
+ driversByUniqueId.remove(driverUniqueId);
+ }
+ }
+
+ public Driver getDriverByUniqueId(String uniqueId) {
+ return driversByUniqueId.get(uniqueId);
+ }
+}
diff --git a/src/main/java/org/traccar/database/ExtendedObjectManager.java b/src/main/java/org/traccar/database/ExtendedObjectManager.java
new file mode 100644
index 000000000..ceb85b537
--- /dev/null
+++ b/src/main/java/org/traccar/database/ExtendedObjectManager.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.model.Permission;
+import org.traccar.model.BaseModel;
+
+public abstract class ExtendedObjectManager<T extends BaseModel> extends SimpleObjectManager<T> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ExtendedObjectManager.class);
+
+ private final Map<Long, Set<Long>> deviceItems = new ConcurrentHashMap<>();
+ private final Map<Long, Set<Long>> deviceItemsWithGroups = new ConcurrentHashMap<>();
+ private final Map<Long, Set<Long>> groupItems = new ConcurrentHashMap<>();
+
+ protected ExtendedObjectManager(DataManager dataManager, Class<T> baseClass) {
+ super(dataManager, baseClass);
+ refreshExtendedPermissions();
+ }
+
+ public final Set<Long> getGroupItems(long groupId) {
+ if (!groupItems.containsKey(groupId)) {
+ groupItems.put(groupId, new HashSet<Long>());
+ }
+ return groupItems.get(groupId);
+ }
+
+ public final Set<Long> getDeviceItems(long deviceId) {
+ if (!deviceItems.containsKey(deviceId)) {
+ deviceItems.put(deviceId, new HashSet<Long>());
+ }
+ return deviceItems.get(deviceId);
+ }
+
+ public Set<Long> getAllDeviceItems(long deviceId) {
+ if (!deviceItemsWithGroups.containsKey(deviceId)) {
+ deviceItemsWithGroups.put(deviceId, new HashSet<Long>());
+ }
+ return deviceItemsWithGroups.get(deviceId);
+ }
+
+ @Override
+ public void removeItem(long itemId) throws SQLException {
+ super.removeItem(itemId);
+ refreshExtendedPermissions();
+ }
+
+ public void refreshExtendedPermissions() {
+ if (getDataManager() != null) {
+ try {
+
+ Collection<Permission> databaseGroupPermissions =
+ getDataManager().getPermissions(Group.class, getBaseClass());
+
+ groupItems.clear();
+ for (Permission groupPermission : databaseGroupPermissions) {
+ getGroupItems(groupPermission.getOwnerId()).add(groupPermission.getPropertyId());
+ }
+
+ Collection<Permission> databaseDevicePermissions =
+ getDataManager().getPermissions(Device.class, getBaseClass());
+
+ deviceItems.clear();
+ deviceItemsWithGroups.clear();
+
+ for (Permission devicePermission : databaseDevicePermissions) {
+ getDeviceItems(devicePermission.getOwnerId()).add(devicePermission.getPropertyId());
+ getAllDeviceItems(devicePermission.getOwnerId()).add(devicePermission.getPropertyId());
+ }
+
+ for (Device device : Context.getDeviceManager().getAllDevices()) {
+ long groupId = device.getGroupId();
+ while (groupId != 0) {
+ getAllDeviceItems(device.getId()).addAll(getGroupItems(groupId));
+ Group group = Context.getGroupsManager().getById(groupId);
+ if (group != null) {
+ groupId = group.getGroupId();
+ } else {
+ groupId = 0;
+ }
+ }
+ }
+
+ } catch (SQLException | ClassNotFoundException error) {
+ LOGGER.warn("Refresh permissions error", error);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/database/GeofenceManager.java b/src/main/java/org/traccar/database/GeofenceManager.java
new file mode 100644
index 000000000..a32847cf9
--- /dev/null
+++ b/src/main/java/org/traccar/database/GeofenceManager.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Geofence;
+import org.traccar.model.Position;
+
+public class GeofenceManager extends ExtendedObjectManager<Geofence> {
+
+ public GeofenceManager(DataManager dataManager) {
+ super(dataManager, Geofence.class);
+ }
+
+ @Override
+ public final void refreshExtendedPermissions() {
+ super.refreshExtendedPermissions();
+ recalculateDevicesGeofences();
+ }
+
+ public List<Long> getCurrentDeviceGeofences(Position position) {
+ List<Long> result = new ArrayList<>();
+ for (long geofenceId : getAllDeviceItems(position.getDeviceId())) {
+ Geofence geofence = getById(geofenceId);
+ if (geofence != null && geofence.getGeometry()
+ .containsPoint(position.getLatitude(), position.getLongitude())) {
+ result.add(geofenceId);
+ }
+ }
+ return result;
+ }
+
+ public void recalculateDevicesGeofences() {
+ for (Device device : Context.getDeviceManager().getAllDevices()) {
+ List<Long> deviceGeofenceIds = device.getGeofenceIds();
+ if (deviceGeofenceIds == null) {
+ deviceGeofenceIds = new ArrayList<>();
+ } else {
+ deviceGeofenceIds.clear();
+ }
+ Position lastPosition = Context.getIdentityManager().getLastPosition(device.getId());
+ if (lastPosition != null && getAllDeviceItems(device.getId()) != null) {
+ deviceGeofenceIds.addAll(getCurrentDeviceGeofences(lastPosition));
+ }
+ device.setGeofenceIds(deviceGeofenceIds);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/GroupTree.java b/src/main/java/org/traccar/database/GroupTree.java
new file mode 100644
index 000000000..8798f55bc
--- /dev/null
+++ b/src/main/java/org/traccar/database/GroupTree.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class GroupTree {
+
+ private static class TreeNode {
+
+ private Group group;
+ private Device device;
+ private Collection<TreeNode> children = new HashSet<>();
+
+ TreeNode(Group group) {
+ this.group = group;
+ }
+
+ TreeNode(Device device) {
+ this.device = device;
+ }
+
+ @Override
+ public int hashCode() {
+ if (group != null) {
+ return (int) group.getId();
+ } else {
+ return (int) device.getId();
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TreeNode)) {
+ return false;
+ }
+ TreeNode other = (TreeNode) obj;
+ if (other == this) {
+ return true;
+ }
+ if (group != null && other.group != null) {
+ return group.getId() == other.group.getId();
+ } else if (device != null && other.device != null) {
+ return device.getId() == other.device.getId();
+ }
+ return false;
+ }
+
+ public Group getGroup() {
+ return group;
+ }
+
+ public Device getDevice() {
+ return device;
+ }
+
+ public void setParent(TreeNode parent) {
+ if (parent != null) {
+ parent.children.add(this);
+ }
+ }
+
+ public Collection<TreeNode> getChildren() {
+ return children;
+ }
+
+ }
+
+ private final Map<Long, TreeNode> groupMap = new HashMap<>();
+
+ public GroupTree(Collection<Group> groups, Collection<Device> devices) {
+
+ for (Group group : groups) {
+ groupMap.put(group.getId(), new TreeNode(group));
+ }
+
+ for (TreeNode node : groupMap.values()) {
+ if (node.getGroup().getGroupId() != 0) {
+ node.setParent(groupMap.get(node.getGroup().getGroupId()));
+ }
+ }
+
+ Map<Long, TreeNode> deviceMap = new HashMap<>();
+
+ for (Device device : devices) {
+ deviceMap.put(device.getId(), new TreeNode(device));
+ }
+
+ for (TreeNode node : deviceMap.values()) {
+ if (node.getDevice().getGroupId() != 0) {
+ node.setParent(groupMap.get(node.getDevice().getGroupId()));
+ }
+ }
+
+ }
+
+ public Collection<Group> getGroups(long groupId) {
+ Set<TreeNode> results = new HashSet<>();
+ getNodes(results, groupMap.get(groupId));
+ Collection<Group> groups = new ArrayList<>();
+ for (TreeNode node : results) {
+ if (node.getGroup() != null) {
+ groups.add(node.getGroup());
+ }
+ }
+ return groups;
+ }
+
+ public Collection<Device> getDevices(long groupId) {
+ Set<TreeNode> results = new HashSet<>();
+ getNodes(results, groupMap.get(groupId));
+ Collection<Device> devices = new ArrayList<>();
+ for (TreeNode node : results) {
+ if (node.getDevice() != null) {
+ devices.add(node.getDevice());
+ }
+ }
+ return devices;
+ }
+
+ private void getNodes(Set<TreeNode> results, TreeNode node) {
+ if (node != null) {
+ for (TreeNode child : node.getChildren()) {
+ results.add(child);
+ getNodes(results, child);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/GroupsManager.java b/src/main/java/org/traccar/database/GroupsManager.java
new file mode 100644
index 000000000..d8404c614
--- /dev/null
+++ b/src/main/java/org/traccar/database/GroupsManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.sql.SQLException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Group;
+
+public class GroupsManager extends BaseObjectManager<Group> implements ManagableObjects {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GroupsManager.class);
+
+ private AtomicLong groupsLastUpdate = new AtomicLong();
+ private final long dataRefreshDelay;
+
+ public GroupsManager(DataManager dataManager) {
+ super(dataManager, Group.class);
+ dataRefreshDelay = Context.getConfig().getLong("database.refreshDelay",
+ DeviceManager.DEFAULT_REFRESH_DELAY) * 1000;
+ }
+
+ private void checkGroupCycles(Group group) {
+ Set<Long> groups = new HashSet<>();
+ while (group != null) {
+ if (groups.contains(group.getId())) {
+ throw new IllegalArgumentException("Cycle in group hierarchy");
+ }
+ groups.add(group.getId());
+ group = getById(group.getGroupId());
+ }
+ }
+
+ public void updateGroupCache(boolean force) throws SQLException {
+ long lastUpdate = groupsLastUpdate.get();
+ if ((force || System.currentTimeMillis() - lastUpdate > dataRefreshDelay)
+ && groupsLastUpdate.compareAndSet(lastUpdate, System.currentTimeMillis())) {
+ refreshItems();
+ }
+ }
+
+ @Override
+ public Set<Long> getAllItems() {
+ Set<Long> result = super.getAllItems();
+ if (result.isEmpty()) {
+ try {
+ updateGroupCache(true);
+ } catch (SQLException e) {
+ LOGGER.warn("Update group cache error", e);
+ }
+ result = super.getAllItems();
+ }
+ return result;
+ }
+
+ @Override
+ protected void addNewItem(Group group) {
+ checkGroupCycles(group);
+ super.addNewItem(group);
+ }
+
+ @Override
+ public void updateItem(Group group) throws SQLException {
+ checkGroupCycles(group);
+ super.updateItem(group);
+ }
+
+ @Override
+ public Set<Long> getUserItems(long userId) {
+ if (Context.getPermissionsManager() != null) {
+ return Context.getPermissionsManager().getGroupPermissions(userId);
+ } else {
+ return new HashSet<>();
+ }
+ }
+
+ @Override
+ public Set<Long> getManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getUserItems(userId));
+ for (long managedUserId : Context.getUsersManager().getUserItems(userId)) {
+ result.addAll(getUserItems(managedUserId));
+ }
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/IdentityManager.java b/src/main/java/org/traccar/database/IdentityManager.java
new file mode 100644
index 000000000..6228a0f75
--- /dev/null
+++ b/src/main/java/org/traccar/database/IdentityManager.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+
+public interface IdentityManager {
+
+ long addUnknownDevice(String uniqueId);
+
+ Device getById(long id);
+
+ Device getByUniqueId(String uniqueId) throws Exception;
+
+ Position getLastPosition(long deviceId);
+
+ boolean isLatestPosition(Position position);
+
+ boolean lookupAttributeBoolean(long deviceId, String attributeName, boolean defaultValue, boolean lookupConfig);
+
+ String lookupAttributeString(long deviceId, String attributeName, String defaultValue, boolean lookupConfig);
+
+ int lookupAttributeInteger(long deviceId, String attributeName, int defaultValue, boolean lookupConfig);
+
+ long lookupAttributeLong(long deviceId, String attributeName, long defaultValue, boolean lookupConfig);
+
+ double lookupAttributeDouble(long deviceId, String attributeName, double defaultValue, boolean lookupConfig);
+
+}
diff --git a/src/main/java/org/traccar/database/LdapProvider.java b/src/main/java/org/traccar/database/LdapProvider.java
new file mode 100644
index 000000000..d8b5c9f52
--- /dev/null
+++ b/src/main/java/org/traccar/database/LdapProvider.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.model.User;
+
+import java.util.Hashtable;
+
+public class LdapProvider {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(LdapProvider.class);
+
+ private String url;
+ private String searchBase;
+ private String idAttribute;
+ private String nameAttribute;
+ private String mailAttribute;
+ private String searchFilter;
+ private String adminFilter;
+ private String serviceUser;
+ private String servicePassword;
+
+ public LdapProvider(Config config) {
+ String url = config.getString("ldap.url");
+ if (url != null) {
+ this.url = url;
+ } else {
+ this.url = "ldap://" + config.getString("ldap.server") + ":" + config.getInteger("ldap.port", 389);
+ }
+ this.searchBase = config.getString("ldap.base");
+ this.idAttribute = config.getString("ldap.idAttribute", "uid");
+ this.nameAttribute = config.getString("ldap.nameAttribute", "cn");
+ this.mailAttribute = config.getString("ldap.mailAttribute", "mail");
+ this.searchFilter = config.getString("ldap.searchFilter", "(" + idAttribute + "=:login)");
+ String adminGroup = config.getString("ldap.adminGroup");
+ this.adminFilter = config.getString("ldap.adminFilter");
+ if (this.adminFilter == null && adminGroup != null) {
+ this.adminFilter = "(&(" + idAttribute + "=:login)(memberOf=" + adminGroup + "))";
+ }
+ this.serviceUser = config.getString("ldap.user");
+ this.servicePassword = config.getString("ldap.password");
+ }
+
+ private InitialDirContext auth(String accountName, String password) throws NamingException {
+ Hashtable<String, String> env = new Hashtable<>();
+ env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+ env.put(Context.PROVIDER_URL, url);
+
+ env.put(Context.SECURITY_AUTHENTICATION, "simple");
+ env.put(Context.SECURITY_PRINCIPAL, accountName);
+ env.put(Context.SECURITY_CREDENTIALS, password);
+
+ return new InitialDirContext(env);
+ }
+
+ private boolean isAdmin(String accountName) {
+ if (this.adminFilter != null) {
+ try {
+ InitialDirContext context = initContext();
+ String searchString = adminFilter.replace(":login", accountName);
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ NamingEnumeration<SearchResult> results = context.search(searchBase, searchString, searchControls);
+ if (results.hasMoreElements()) {
+ results.nextElement();
+ if (results.hasMoreElements()) {
+ LOGGER.warn("Matched multiple users for the accountName: " + accountName);
+ return false;
+ }
+ return true;
+ }
+ } catch (NamingException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public InitialDirContext initContext() throws NamingException {
+ return auth(serviceUser, servicePassword);
+ }
+
+ private SearchResult lookupUser(String accountName) throws NamingException {
+ InitialDirContext context = initContext();
+
+ String searchString = searchFilter.replace(":login", accountName);
+
+ SearchControls searchControls = new SearchControls();
+ String[] attributeFilter = {idAttribute, nameAttribute, mailAttribute};
+ searchControls.setReturningAttributes(attributeFilter);
+ searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+ NamingEnumeration<SearchResult> results = context.search(searchBase, searchString, searchControls);
+
+ SearchResult searchResult = null;
+ if (results.hasMoreElements()) {
+ searchResult = results.nextElement();
+ if (results.hasMoreElements()) {
+ LOGGER.warn("Matched multiple users for the accountName: " + accountName);
+ return null;
+ }
+ }
+
+ return searchResult;
+ }
+
+ public User getUser(String accountName) {
+ SearchResult ldapUser;
+ User user = new User();
+ try {
+ ldapUser = lookupUser(accountName);
+ if (ldapUser != null) {
+ Attribute attribute = ldapUser.getAttributes().get(idAttribute);
+ if (attribute != null) {
+ user.setLogin((String) attribute.get());
+ } else {
+ user.setLogin(accountName);
+ }
+ attribute = ldapUser.getAttributes().get(nameAttribute);
+ if (attribute != null) {
+ user.setName((String) attribute.get());
+ } else {
+ user.setName(accountName);
+ }
+ attribute = ldapUser.getAttributes().get(mailAttribute);
+ if (attribute != null) {
+ user.setEmail((String) attribute.get());
+ } else {
+ user.setEmail(accountName);
+ }
+ }
+ user.setAdministrator(isAdmin(accountName));
+ } catch (NamingException e) {
+ user.setLogin(accountName);
+ user.setName(accountName);
+ user.setEmail(accountName);
+ LOGGER.warn("User lookup error", e);
+ }
+ return user;
+ }
+
+ public boolean login(String username, String password) {
+ try {
+ SearchResult ldapUser = lookupUser(username);
+ if (ldapUser != null) {
+ auth(ldapUser.getNameInNamespace(), password).close();
+ return true;
+ }
+ } catch (NamingException e) {
+ return false;
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/MailManager.java b/src/main/java/org/traccar/database/MailManager.java
new file mode 100644
index 000000000..8a2f002cd
--- /dev/null
+++ b/src/main/java/org/traccar/database/MailManager.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.Main;
+import org.traccar.model.User;
+import org.traccar.notification.PropertiesProvider;
+
+import javax.mail.BodyPart;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Multipart;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import java.util.Date;
+import java.util.Properties;
+
+public final class MailManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MailManager.class);
+
+ private static Properties getProperties(PropertiesProvider provider) {
+ Properties properties = new Properties();
+ String host = provider.getString("mail.smtp.host");
+ if (host != null) {
+ properties.put("mail.transport.protocol", provider.getString("mail.transport.protocol", "smtp"));
+ properties.put("mail.smtp.host", host);
+ properties.put("mail.smtp.port", String.valueOf(provider.getInteger("mail.smtp.port", 25)));
+
+ Boolean starttlsEnable = provider.getBoolean("mail.smtp.starttls.enable");
+ if (starttlsEnable != null) {
+ properties.put("mail.smtp.starttls.enable", String.valueOf(starttlsEnable));
+ }
+ Boolean starttlsRequired = provider.getBoolean("mail.smtp.starttls.required");
+ if (starttlsRequired != null) {
+ properties.put("mail.smtp.starttls.required", String.valueOf(starttlsRequired));
+ }
+
+ Boolean sslEnable = provider.getBoolean("mail.smtp.ssl.enable");
+ if (sslEnable != null) {
+ properties.put("mail.smtp.ssl.enable", String.valueOf(sslEnable));
+ }
+ String sslTrust = provider.getString("mail.smtp.ssl.trust");
+ if (sslTrust != null) {
+ properties.put("mail.smtp.ssl.trust", sslTrust);
+ }
+
+ String sslProtocols = provider.getString("mail.smtp.ssl.protocols");
+ if (sslProtocols != null) {
+ properties.put("mail.smtp.ssl.protocols", sslProtocols);
+ }
+
+ String username = provider.getString("mail.smtp.username");
+ if (username != null) {
+ properties.put("mail.smtp.username", username);
+ }
+ String password = provider.getString("mail.smtp.password");
+ if (password != null) {
+ properties.put("mail.smtp.password", password);
+ }
+ String from = provider.getString("mail.smtp.from");
+ if (from != null) {
+ properties.put("mail.smtp.from", from);
+ }
+ }
+ return properties;
+ }
+
+ public void sendMessage(
+ long userId, String subject, String body) throws MessagingException {
+ sendMessage(userId, subject, body, null);
+ }
+
+ public void sendMessage(
+ long userId, String subject, String body, MimeBodyPart attachment) throws MessagingException {
+ User user = Context.getPermissionsManager().getUser(userId);
+
+ Properties properties = null;
+ if (!Context.getConfig().getBoolean("mail.smtp.ignoreUserConfig")) {
+ properties = getProperties(new PropertiesProvider(user));
+ }
+ if (properties == null || !properties.containsKey("mail.smtp.host")) {
+ properties = getProperties(new PropertiesProvider(Context.getConfig()));
+ }
+ if (!properties.containsKey("mail.smtp.host")) {
+ LOGGER.warn("No SMTP configuration found");
+ return;
+ }
+
+ Session session = Session.getInstance(properties);
+
+ MimeMessage message = new MimeMessage(session);
+
+ String from = properties.getProperty("mail.smtp.from");
+ if (from != null) {
+ message.setFrom(new InternetAddress(from));
+ }
+
+ message.addRecipient(Message.RecipientType.TO, new InternetAddress(user.getEmail()));
+ message.setSubject(subject);
+ message.setSentDate(new Date());
+
+ if (attachment != null) {
+ Multipart multipart = new MimeMultipart();
+
+ BodyPart messageBodyPart = new MimeBodyPart();
+ messageBodyPart.setContent(body, "text/html; charset=utf-8");
+ multipart.addBodyPart(messageBodyPart);
+ multipart.addBodyPart(attachment);
+
+ message.setContent(multipart);
+ } else {
+ message.setContent(body, "text/html; charset=utf-8");
+ }
+
+ try (Transport transport = session.getTransport()) {
+ Main.getInjector().getInstance(StatisticsManager.class).registerMail();
+ transport.connect(
+ properties.getProperty("mail.smtp.host"),
+ properties.getProperty("mail.smtp.username"),
+ properties.getProperty("mail.smtp.password"));
+ transport.sendMessage(message, message.getAllRecipients());
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/MaintenancesManager.java b/src/main/java/org/traccar/database/MaintenancesManager.java
new file mode 100644
index 000000000..4e266cb78
--- /dev/null
+++ b/src/main/java/org/traccar/database/MaintenancesManager.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.traccar.model.Maintenance;
+
+public class MaintenancesManager extends ExtendedObjectManager<Maintenance> {
+
+ public MaintenancesManager(DataManager dataManager) {
+ super(dataManager, Maintenance.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/ManagableObjects.java b/src/main/java/org/traccar/database/ManagableObjects.java
new file mode 100644
index 000000000..ec9549493
--- /dev/null
+++ b/src/main/java/org/traccar/database/ManagableObjects.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.util.Set;
+
+public interface ManagableObjects {
+
+ Set<Long> getUserItems(long userId);
+
+ Set<Long> getManagedItems(long userId);
+
+}
diff --git a/src/main/java/org/traccar/database/MediaManager.java b/src/main/java/org/traccar/database/MediaManager.java
new file mode 100644
index 000000000..edade5766
--- /dev/null
+++ b/src/main/java/org/traccar/database/MediaManager.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import io.netty.buffer.ByteBuf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class MediaManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MediaManager.class);
+
+ private String path;
+
+ public MediaManager(String path) {
+ this.path = path;
+ }
+
+ private File createFile(String uniqueId, String name) throws IOException {
+ Path filePath = Paths.get(path, uniqueId, name);
+ Path directoryPath = filePath.getParent();
+ if (directoryPath != null) {
+ Files.createDirectories(directoryPath);
+ }
+ return filePath.toFile();
+ }
+
+ public String writeFile(String uniqueId, ByteBuf buf, String extension) {
+ if (path != null) {
+ int size = buf.readableBytes();
+ String name = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "." + extension;
+ try (FileOutputStream output = new FileOutputStream(createFile(uniqueId, name));
+ FileChannel fileChannel = output.getChannel()) {
+ ByteBuffer byteBuffer = buf.nioBuffer();
+ int written = 0;
+ while (written < size) {
+ written += fileChannel.write(byteBuffer);
+ }
+ fileChannel.force(false);
+ return name;
+ } catch (IOException e) {
+ LOGGER.warn("Save media file error", e);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/NotificationManager.java b/src/main/java/org/traccar/database/NotificationManager.java
new file mode 100644
index 000000000..09df4c571
--- /dev/null
+++ b/src/main/java/org/traccar/database/NotificationManager.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Calendar;
+import org.traccar.model.Event;
+import org.traccar.model.Notification;
+import org.traccar.model.Position;
+import org.traccar.model.Typed;
+
+public class NotificationManager extends ExtendedObjectManager<Notification> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NotificationManager.class);
+
+ private boolean geocodeOnRequest;
+
+ public NotificationManager(DataManager dataManager) {
+ super(dataManager, Notification.class);
+ geocodeOnRequest = Context.getConfig().getBoolean("geocoder.onRequest");
+ }
+
+ private Set<Long> getEffectiveNotifications(long userId, long deviceId, Date time) {
+ Set<Long> result = new HashSet<>();
+ Set<Long> deviceNotifications = getAllDeviceItems(deviceId);
+ for (long itemId : getUserItems(userId)) {
+ if (getById(itemId).getAlways() || deviceNotifications.contains(itemId)) {
+ long calendarId = getById(itemId).getCalendarId();
+ Calendar calendar = calendarId != 0 ? Context.getCalendarManager().getById(calendarId) : null;
+ if (calendar == null || calendar.checkMoment(time)) {
+ result.add(itemId);
+ }
+ }
+ }
+ return result;
+ }
+
+ public void updateEvent(Event event, Position position) {
+ try {
+ getDataManager().addObject(event);
+ } catch (SQLException error) {
+ LOGGER.warn("Event save error", error);
+ }
+
+ if (position != null && geocodeOnRequest && Context.getGeocoder() != null && position.getAddress() == null) {
+ position.setAddress(Context.getGeocoder()
+ .getAddress(position.getLatitude(), position.getLongitude(), null));
+ }
+
+ long deviceId = event.getDeviceId();
+ Set<Long> users = Context.getPermissionsManager().getDeviceUsers(deviceId);
+ Set<Long> usersToForward = null;
+ if (Context.getEventForwarder() != null) {
+ usersToForward = new HashSet<>();
+ }
+ for (long userId : users) {
+ if ((event.getGeofenceId() == 0
+ || Context.getGeofenceManager().checkItemPermission(userId, event.getGeofenceId()))
+ && (event.getMaintenanceId() == 0
+ || Context.getMaintenancesManager().checkItemPermission(userId, event.getMaintenanceId()))) {
+ if (usersToForward != null) {
+ usersToForward.add(userId);
+ }
+ final Set<String> notificators = new HashSet<>();
+ for (long notificationId : getEffectiveNotifications(userId, deviceId, event.getServerTime())) {
+ Notification notification = getById(notificationId);
+ if (getById(notificationId).getType().equals(event.getType())) {
+ boolean filter = false;
+ if (event.getType().equals(Event.TYPE_ALARM)) {
+ String alarms = notification.getString("alarms");
+ if (alarms == null || !alarms.contains(event.getString(Position.KEY_ALARM))) {
+ filter = true;
+ }
+ }
+ if (!filter) {
+ notificators.addAll(notification.getNotificatorsTypes());
+ }
+ }
+ }
+ for (String notificator : notificators) {
+ Context.getNotificatorManager().getNotificator(notificator).sendAsync(userId, event, position);
+ }
+ }
+ }
+ if (Context.getEventForwarder() != null) {
+ Context.getEventForwarder().forwardEvent(event, position, usersToForward);
+ }
+ }
+
+ public void updateEvents(Map<Event, Position> events) {
+ for (Entry<Event, Position> event : events.entrySet()) {
+ updateEvent(event.getKey(), event.getValue());
+ }
+ }
+
+ public Set<Typed> getAllNotificationTypes() {
+ Set<Typed> types = new HashSet<>();
+ Field[] fields = Event.class.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers()) && field.getName().startsWith("TYPE_")) {
+ try {
+ types.add(new Typed(field.get(null).toString()));
+ } catch (IllegalArgumentException | IllegalAccessException error) {
+ LOGGER.warn("Get event types error", error);
+ }
+ }
+ }
+ return types;
+ }
+}
diff --git a/src/main/java/org/traccar/database/PermissionsManager.java b/src/main/java/org/traccar/database/PermissionsManager.java
new file mode 100644
index 000000000..ced0df1c0
--- /dev/null
+++ b/src/main/java/org/traccar/database/PermissionsManager.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Attribute;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Calendar;
+import org.traccar.model.Command;
+import org.traccar.model.Device;
+import org.traccar.model.Driver;
+import org.traccar.model.Geofence;
+import org.traccar.model.Group;
+import org.traccar.model.Maintenance;
+import org.traccar.model.ManagedUser;
+import org.traccar.model.Notification;
+import org.traccar.model.Permission;
+import org.traccar.model.Server;
+import org.traccar.model.User;
+
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class PermissionsManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PermissionsManager.class);
+
+ private final DataManager dataManager;
+ private final UsersManager usersManager;
+
+ private volatile Server server;
+
+ private final Map<Long, Set<Long>> groupPermissions = new HashMap<>();
+ private final Map<Long, Set<Long>> devicePermissions = new HashMap<>();
+ private final Map<Long, Set<Long>> deviceUsers = new HashMap<>();
+ private final Map<Long, Set<Long>> groupDevices = new HashMap<>();
+
+ public PermissionsManager(DataManager dataManager, UsersManager usersManager) {
+ this.dataManager = dataManager;
+ this.usersManager = usersManager;
+ refreshServer();
+ refreshDeviceAndGroupPermissions();
+ }
+
+ public User getUser(long userId) {
+ return usersManager.getById(userId);
+ }
+
+ public Set<Long> getGroupPermissions(long userId) {
+ if (!groupPermissions.containsKey(userId)) {
+ groupPermissions.put(userId, new HashSet<>());
+ }
+ return groupPermissions.get(userId);
+ }
+
+ public Set<Long> getDevicePermissions(long userId) {
+ if (!devicePermissions.containsKey(userId)) {
+ devicePermissions.put(userId, new HashSet<>());
+ }
+ return devicePermissions.get(userId);
+ }
+
+ private Set<Long> getAllDeviceUsers(long deviceId) {
+ if (!deviceUsers.containsKey(deviceId)) {
+ deviceUsers.put(deviceId, new HashSet<>());
+ }
+ return deviceUsers.get(deviceId);
+ }
+
+ public Set<Long> getDeviceUsers(long deviceId) {
+ Device device = Context.getIdentityManager().getById(deviceId);
+ if (device != null && !device.getDisabled()) {
+ return getAllDeviceUsers(deviceId);
+ } else {
+ Set<Long> result = new HashSet<>();
+ for (long userId : getAllDeviceUsers(deviceId)) {
+ if (getUserAdmin(userId)) {
+ result.add(userId);
+ }
+ }
+ return result;
+ }
+ }
+
+ public Set<Long> getGroupDevices(long groupId) {
+ if (!groupDevices.containsKey(groupId)) {
+ groupDevices.put(groupId, new HashSet<>());
+ }
+ return groupDevices.get(groupId);
+ }
+
+ public void refreshServer() {
+ try {
+ server = dataManager.getServer();
+ } catch (SQLException error) {
+ LOGGER.warn("Refresh server config error", error);
+ }
+ }
+
+ public final void refreshDeviceAndGroupPermissions() {
+ groupPermissions.clear();
+ devicePermissions.clear();
+ try {
+ GroupTree groupTree = new GroupTree(Context.getGroupsManager().getItems(
+ Context.getGroupsManager().getAllItems()),
+ Context.getDeviceManager().getAllDevices());
+ for (Permission groupPermission : dataManager.getPermissions(User.class, Group.class)) {
+ Set<Long> userGroupPermissions = getGroupPermissions(groupPermission.getOwnerId());
+ Set<Long> userDevicePermissions = getDevicePermissions(groupPermission.getOwnerId());
+ userGroupPermissions.add(groupPermission.getPropertyId());
+ for (Group group : groupTree.getGroups(groupPermission.getPropertyId())) {
+ userGroupPermissions.add(group.getId());
+ }
+ for (Device device : groupTree.getDevices(groupPermission.getPropertyId())) {
+ userDevicePermissions.add(device.getId());
+ }
+ }
+
+ for (Permission devicePermission : dataManager.getPermissions(User.class, Device.class)) {
+ getDevicePermissions(devicePermission.getOwnerId()).add(devicePermission.getPropertyId());
+ }
+
+ groupDevices.clear();
+ for (long groupId : Context.getGroupsManager().getAllItems()) {
+ for (Device device : groupTree.getDevices(groupId)) {
+ getGroupDevices(groupId).add(device.getId());
+ }
+ }
+
+ } catch (SQLException | ClassNotFoundException error) {
+ LOGGER.warn("Refresh device permissions error", error);
+ }
+
+ deviceUsers.clear();
+ for (Map.Entry<Long, Set<Long>> entry : devicePermissions.entrySet()) {
+ for (long deviceId : entry.getValue()) {
+ getAllDeviceUsers(deviceId).add(entry.getKey());
+ }
+ }
+ }
+
+ public boolean getUserAdmin(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getAdministrator();
+ }
+
+ public void checkAdmin(long userId) throws SecurityException {
+ if (!getUserAdmin(userId)) {
+ throw new SecurityException("Admin access required");
+ }
+ }
+
+ public boolean getUserManager(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getUserLimit() != 0;
+ }
+
+ public void checkManager(long userId) throws SecurityException {
+ if (!getUserManager(userId)) {
+ throw new SecurityException("Manager access required");
+ }
+ }
+
+ public void checkManager(long userId, long managedUserId) throws SecurityException {
+ checkManager(userId);
+ if (!usersManager.getUserItems(userId).contains(managedUserId)) {
+ throw new SecurityException("User access denied");
+ }
+ }
+
+ public void checkUserLimit(long userId) throws SecurityException {
+ int userLimit = getUser(userId).getUserLimit();
+ if (userLimit != -1 && usersManager.getUserItems(userId).size() >= userLimit) {
+ throw new SecurityException("Manager user limit reached");
+ }
+ }
+
+ public void checkDeviceLimit(long userId) throws SecurityException {
+ int deviceLimit = getUser(userId).getDeviceLimit();
+ if (deviceLimit != -1) {
+ int deviceCount = 0;
+ if (getUserManager(userId)) {
+ deviceCount = Context.getDeviceManager().getAllManagedItems(userId).size();
+ } else {
+ deviceCount = Context.getDeviceManager().getAllUserItems(userId).size();
+ }
+ if (deviceCount >= deviceLimit) {
+ throw new SecurityException("User device limit reached");
+ }
+ }
+ }
+
+ public boolean getUserReadonly(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getReadonly();
+ }
+
+ public boolean getUserDeviceReadonly(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getDeviceReadonly();
+ }
+
+ public boolean getUserLimitCommands(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getLimitCommands();
+ }
+
+ public void checkReadonly(long userId) throws SecurityException {
+ if (!getUserAdmin(userId) && (server.getReadonly() || getUserReadonly(userId))) {
+ throw new SecurityException("Account is readonly");
+ }
+ }
+
+ public void checkDeviceReadonly(long userId) throws SecurityException {
+ if (!getUserAdmin(userId) && (server.getDeviceReadonly() || getUserDeviceReadonly(userId))) {
+ throw new SecurityException("Account is device readonly");
+ }
+ }
+
+ public void checkLimitCommands(long userId) throws SecurityException {
+ if (!getUserAdmin(userId) && (server.getLimitCommands() || getUserLimitCommands(userId))) {
+ throw new SecurityException("Account has limit sending commands");
+ }
+ }
+
+ public void checkUserDeviceCommand(long userId, long deviceId, long commandId) throws SecurityException {
+ if (!getUserAdmin(userId) && Context.getCommandsManager().checkDeviceCommand(deviceId, commandId)) {
+ throw new SecurityException("Command can not be sent to this device");
+ }
+ }
+
+ public void checkUserEnabled(long userId) throws SecurityException {
+ User user = getUser(userId);
+ if (user == null) {
+ throw new SecurityException("Unknown account");
+ }
+ if (user.getDisabled()) {
+ throw new SecurityException("Account is disabled");
+ }
+ if (user.getExpirationTime() != null && System.currentTimeMillis() > user.getExpirationTime().getTime()) {
+ throw new SecurityException("Account has expired");
+ }
+ }
+
+ public void checkUserUpdate(long userId, User before, User after) throws SecurityException {
+ if (before.getAdministrator() != after.getAdministrator()
+ || before.getDeviceLimit() != after.getDeviceLimit()
+ || before.getUserLimit() != after.getUserLimit()) {
+ checkAdmin(userId);
+ }
+ User user = getUser(userId);
+ if (user != null && user.getExpirationTime() != null
+ && (after.getExpirationTime() == null
+ || user.getExpirationTime().compareTo(after.getExpirationTime()) < 0)) {
+ checkAdmin(userId);
+ }
+ if (before.getReadonly() != after.getReadonly()
+ || before.getDeviceReadonly() != after.getDeviceReadonly()
+ || before.getDisabled() != after.getDisabled()
+ || before.getLimitCommands() != after.getLimitCommands()) {
+ if (userId == after.getId()) {
+ checkAdmin(userId);
+ }
+ if (!getUserAdmin(userId)) {
+ checkManager(userId);
+ }
+ }
+ }
+
+ public void checkUser(long userId, long managedUserId) throws SecurityException {
+ if (userId != managedUserId && !getUserAdmin(userId)) {
+ checkManager(userId, managedUserId);
+ }
+ }
+
+ public void checkGroup(long userId, long groupId) throws SecurityException {
+ if (!getGroupPermissions(userId).contains(groupId) && !getUserAdmin(userId)) {
+ checkManager(userId);
+ for (long managedUserId : usersManager.getUserItems(userId)) {
+ if (getGroupPermissions(managedUserId).contains(groupId)) {
+ return;
+ }
+ }
+ throw new SecurityException("Group access denied");
+ }
+ }
+
+ public void checkDevice(long userId, long deviceId) throws SecurityException {
+ if (!Context.getDeviceManager().getUserItems(userId).contains(deviceId) && !getUserAdmin(userId)) {
+ checkManager(userId);
+ for (long managedUserId : usersManager.getUserItems(userId)) {
+ if (Context.getDeviceManager().getUserItems(managedUserId).contains(deviceId)) {
+ return;
+ }
+ }
+ throw new SecurityException("Device access denied");
+ }
+ }
+
+ public void checkRegistration(long userId) {
+ if (!server.getRegistration() && !getUserAdmin(userId)) {
+ throw new SecurityException("Registration disabled");
+ }
+ }
+
+ public void checkPermission(Class<?> object, long userId, long objectId)
+ throws SecurityException {
+ SimpleObjectManager<? extends BaseModel> manager = null;
+
+ if (object.equals(Device.class)) {
+ checkDevice(userId, objectId);
+ } else if (object.equals(Group.class)) {
+ checkGroup(userId, objectId);
+ } else if (object.equals(User.class) || object.equals(ManagedUser.class)) {
+ checkUser(userId, objectId);
+ } else if (object.equals(Geofence.class)) {
+ manager = Context.getGeofenceManager();
+ } else if (object.equals(Attribute.class)) {
+ manager = Context.getAttributesManager();
+ } else if (object.equals(Driver.class)) {
+ manager = Context.getDriversManager();
+ } else if (object.equals(Calendar.class)) {
+ manager = Context.getCalendarManager();
+ } else if (object.equals(Command.class)) {
+ manager = Context.getCommandsManager();
+ } else if (object.equals(Maintenance.class)) {
+ manager = Context.getMaintenancesManager();
+ } else if (object.equals(Notification.class)) {
+ manager = Context.getNotificationManager();
+ } else {
+ throw new IllegalArgumentException("Unknown object type");
+ }
+
+ if (manager != null && !manager.checkItemPermission(userId, objectId) && !getUserAdmin(userId)) {
+ checkManager(userId);
+ for (long managedUserId : usersManager.getManagedItems(userId)) {
+ if (manager.checkItemPermission(managedUserId, objectId)) {
+ return;
+ }
+ }
+ throw new SecurityException("Type " + object + " access denied");
+ }
+ }
+
+ public void refreshAllUsersPermissions() {
+ if (Context.getGeofenceManager() != null) {
+ Context.getGeofenceManager().refreshUserItems();
+ }
+ Context.getCalendarManager().refreshUserItems();
+ Context.getDriversManager().refreshUserItems();
+ Context.getAttributesManager().refreshUserItems();
+ Context.getCommandsManager().refreshUserItems();
+ Context.getMaintenancesManager().refreshUserItems();
+ if (Context.getNotificationManager() != null) {
+ Context.getNotificationManager().refreshUserItems();
+ }
+ }
+
+ public void refreshAllExtendedPermissions() {
+ if (Context.getGeofenceManager() != null) {
+ Context.getGeofenceManager().refreshExtendedPermissions();
+ }
+ Context.getDriversManager().refreshExtendedPermissions();
+ Context.getAttributesManager().refreshExtendedPermissions();
+ Context.getCommandsManager().refreshExtendedPermissions();
+ Context.getMaintenancesManager().refreshExtendedPermissions();
+ }
+
+ public void refreshPermissions(Permission permission) {
+ if (permission.getOwnerClass().equals(User.class)) {
+ if (permission.getPropertyClass().equals(Device.class)
+ || permission.getPropertyClass().equals(Group.class)) {
+ refreshDeviceAndGroupPermissions();
+ refreshAllExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(ManagedUser.class)) {
+ usersManager.refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Geofence.class) && Context.getGeofenceManager() != null) {
+ Context.getGeofenceManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Driver.class)) {
+ Context.getDriversManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Attribute.class)) {
+ Context.getAttributesManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Calendar.class)) {
+ Context.getCalendarManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Command.class)) {
+ Context.getCommandsManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Maintenance.class)) {
+ Context.getMaintenancesManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Notification.class)
+ && Context.getNotificationManager() != null) {
+ Context.getNotificationManager().refreshUserItems();
+ }
+ } else if (permission.getOwnerClass().equals(Device.class) || permission.getOwnerClass().equals(Group.class)) {
+ if (permission.getPropertyClass().equals(Geofence.class) && Context.getGeofenceManager() != null) {
+ Context.getGeofenceManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Driver.class)) {
+ Context.getDriversManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Attribute.class)) {
+ Context.getAttributesManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Command.class)) {
+ Context.getCommandsManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Maintenance.class)) {
+ Context.getMaintenancesManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Notification.class)
+ && Context.getNotificationManager() != null) {
+ Context.getNotificationManager().refreshExtendedPermissions();
+ }
+ }
+ }
+
+ public Server getServer() {
+ return server;
+ }
+
+ public void updateServer(Server server) throws SQLException {
+ dataManager.updateObject(server);
+ this.server = server;
+ }
+
+ public User login(String email, String password) throws SQLException {
+ User user = dataManager.login(email, password);
+ if (user != null) {
+ checkUserEnabled(user.getId());
+ return getUser(user.getId());
+ }
+ return null;
+ }
+
+ public Object lookupAttribute(long userId, String key, Object defaultValue) {
+ Object preference;
+ Object serverPreference = server.getAttributes().get(key);
+ Object userPreference = getUser(userId).getAttributes().get(key);
+ if (server.getForceSettings()) {
+ preference = serverPreference != null ? serverPreference : userPreference;
+ } else {
+ preference = userPreference != null ? userPreference : serverPreference;
+ }
+ return preference != null ? preference : defaultValue;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/QueryBuilder.java b/src/main/java/org/traccar/database/QueryBuilder.java
new file mode 100644
index 000000000..5528b2320
--- /dev/null
+++ b/src/main/java/org/traccar/database/QueryBuilder.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.MiscFormatter;
+import org.traccar.model.Permission;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public final class QueryBuilder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(QueryBuilder.class);
+
+ private final Map<String, List<Integer>> indexMap = new HashMap<>();
+ private Connection connection;
+ private PreparedStatement statement;
+ private final String query;
+ private final boolean returnGeneratedKeys;
+
+ private QueryBuilder(DataSource dataSource, String query, boolean returnGeneratedKeys) throws SQLException {
+ this.query = query;
+ this.returnGeneratedKeys = returnGeneratedKeys;
+ if (query != null) {
+ connection = dataSource.getConnection();
+ String parsedQuery = parse(query.trim(), indexMap);
+ try {
+ if (returnGeneratedKeys) {
+ statement = connection.prepareStatement(parsedQuery, Statement.RETURN_GENERATED_KEYS);
+ } else {
+ statement = connection.prepareStatement(parsedQuery);
+ }
+ } catch (SQLException error) {
+ connection.close();
+ throw error;
+ }
+ }
+ }
+
+ private static String parse(String query, Map<String, List<Integer>> paramMap) {
+
+ int length = query.length();
+ StringBuilder parsedQuery = new StringBuilder(length);
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
+ int index = 1;
+
+ for (int i = 0; i < length; i++) {
+
+ char c = query.charAt(i);
+
+ // String end
+ if (inSingleQuote) {
+ if (c == '\'') {
+ inSingleQuote = false;
+ }
+ } else if (inDoubleQuote) {
+ if (c == '"') {
+ inDoubleQuote = false;
+ }
+ } else {
+
+ // String begin
+ if (c == '\'') {
+ inSingleQuote = true;
+ } else if (c == '"') {
+ inDoubleQuote = true;
+ } else if (c == ':' && i + 1 < length
+ && Character.isJavaIdentifierStart(query.charAt(i + 1))) {
+
+ // Identifier name
+ int j = i + 2;
+ while (j < length && Character.isJavaIdentifierPart(query.charAt(j))) {
+ j++;
+ }
+
+ String name = query.substring(i + 1, j);
+ c = '?';
+ i += name.length();
+ name = name.toLowerCase();
+
+ // Add to list
+ List<Integer> indexList = paramMap.get(name);
+ if (indexList == null) {
+ indexList = new LinkedList<>();
+ paramMap.put(name, indexList);
+ }
+ indexList.add(index);
+
+ index++;
+ }
+ }
+
+ parsedQuery.append(c);
+ }
+
+ return parsedQuery.toString();
+ }
+
+ public static QueryBuilder create(DataSource dataSource, String query) throws SQLException {
+ return new QueryBuilder(dataSource, query, false);
+ }
+
+ public static QueryBuilder create(
+ DataSource dataSource, String query, boolean returnGeneratedKeys) throws SQLException {
+ return new QueryBuilder(dataSource, query, returnGeneratedKeys);
+ }
+
+ private List<Integer> indexes(String name) {
+ name = name.toLowerCase();
+ List<Integer> result = indexMap.get(name);
+ if (result == null) {
+ result = new LinkedList<>();
+ }
+ return result;
+ }
+
+ public QueryBuilder setBoolean(String name, boolean value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setBoolean(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setInteger(String name, int value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setInt(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setLong(String name, long value) throws SQLException {
+ return setLong(name, value, false);
+ }
+
+ public QueryBuilder setLong(String name, long value, boolean nullIfZero) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == 0 && nullIfZero) {
+ statement.setNull(i, Types.INTEGER);
+ } else {
+ statement.setLong(i, value);
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setDouble(String name, double value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setDouble(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setString(String name, String value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == null) {
+ statement.setNull(i, Types.VARCHAR);
+ } else {
+ statement.setString(i, value);
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setDate(String name, Date value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == null) {
+ statement.setNull(i, Types.TIMESTAMP);
+ } else {
+ statement.setTimestamp(i, new Timestamp(value.getTime()));
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setBlob(String name, byte[] value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == null) {
+ statement.setNull(i, Types.BLOB);
+ } else {
+ statement.setBytes(i, value);
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setObject(Object object) throws SQLException {
+
+ Method[] methods = object.getClass().getMethods();
+
+ for (Method method : methods) {
+ if (method.getName().startsWith("get") && method.getParameterTypes().length == 0
+ && !method.isAnnotationPresent(QueryIgnore.class)) {
+ String name = method.getName().substring(3);
+ try {
+ if (method.getReturnType().equals(boolean.class)) {
+ setBoolean(name, (Boolean) method.invoke(object));
+ } else if (method.getReturnType().equals(int.class)) {
+ setInteger(name, (Integer) method.invoke(object));
+ } else if (method.getReturnType().equals(long.class)) {
+ setLong(name, (Long) method.invoke(object), name.endsWith("Id"));
+ } else if (method.getReturnType().equals(double.class)) {
+ setDouble(name, (Double) method.invoke(object));
+ } else if (method.getReturnType().equals(String.class)) {
+ setString(name, (String) method.invoke(object));
+ } else if (method.getReturnType().equals(Date.class)) {
+ setDate(name, (Date) method.invoke(object));
+ } else if (method.getReturnType().equals(byte[].class)) {
+ setBlob(name, (byte[]) method.invoke(object));
+ } else {
+ if (method.getReturnType().equals(Map.class)
+ && Context.getConfig().getBoolean("database.xml")) {
+ setString(name, MiscFormatter.toXmlString((Map) method.invoke(object)));
+ } else {
+ setString(name, Context.getObjectMapper().writeValueAsString(method.invoke(object)));
+ }
+ }
+ } catch (IllegalAccessException | InvocationTargetException | JsonProcessingException error) {
+ LOGGER.warn("Get property error", error);
+ }
+ }
+ }
+
+ return this;
+ }
+
+ private interface ResultSetProcessor<T> {
+ void process(T object, ResultSet resultSet) throws SQLException;
+ }
+
+ public <T> T executeQuerySingle(Class<T> clazz) throws SQLException {
+ Collection<T> result = executeQuery(clazz);
+ if (!result.isEmpty()) {
+ return result.iterator().next();
+ } else {
+ return null;
+ }
+ }
+
+ private <T> void addProcessors(
+ List<ResultSetProcessor<T>> processors,
+ final Class<?> parameterType, final Method method, final String name) {
+
+ if (parameterType.equals(boolean.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getBoolean(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(int.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getInt(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(long.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getLong(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(double.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getDouble(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(String.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getString(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(Date.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ Timestamp timestamp = resultSet.getTimestamp(name);
+ if (timestamp != null) {
+ method.invoke(object, new Date(timestamp.getTime()));
+ }
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(byte[].class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getBytes(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ String value = resultSet.getString(name);
+ if (value != null && !value.isEmpty()) {
+ try {
+ method.invoke(object, Context.getObjectMapper().readValue(value, parameterType));
+ } catch (InvocationTargetException | IllegalAccessException | IOException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ public <T> Collection<T> executeQuery(Class<T> clazz) throws SQLException {
+ List<T> result = new LinkedList<>();
+
+ if (query != null) {
+
+ try {
+
+ try (ResultSet resultSet = statement.executeQuery()) {
+
+ ResultSetMetaData resultMetaData = resultSet.getMetaData();
+
+ List<ResultSetProcessor<T>> processors = new LinkedList<>();
+
+ Method[] methods = clazz.getMethods();
+
+ for (final Method method : methods) {
+ if (method.getName().startsWith("set") && method.getParameterTypes().length == 1
+ && !method.isAnnotationPresent(QueryIgnore.class)) {
+
+ final String name = method.getName().substring(3);
+
+ // Check if column exists
+ boolean column = false;
+ for (int i = 1; i <= resultMetaData.getColumnCount(); i++) {
+ if (name.equalsIgnoreCase(resultMetaData.getColumnLabel(i))) {
+ column = true;
+ break;
+ }
+ }
+ if (!column) {
+ continue;
+ }
+
+ addProcessors(processors, method.getParameterTypes()[0], method, name);
+ }
+ }
+
+ while (resultSet.next()) {
+ try {
+ T object = clazz.newInstance();
+ for (ResultSetProcessor<T> processor : processors) {
+ processor.process(object, resultSet);
+ }
+ result.add(object);
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+
+ return result;
+ }
+
+ public long executeUpdate() throws SQLException {
+
+ if (query != null) {
+ try {
+ statement.execute();
+ if (returnGeneratedKeys) {
+ ResultSet resultSet = statement.getGeneratedKeys();
+ if (resultSet.next()) {
+ return resultSet.getLong(1);
+ }
+ }
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+ return 0;
+ }
+
+ public Collection<Permission> executePermissionsQuery() throws SQLException, ClassNotFoundException {
+ List<Permission> result = new LinkedList<>();
+ if (query != null) {
+ try {
+ try (ResultSet resultSet = statement.executeQuery()) {
+ ResultSetMetaData resultMetaData = resultSet.getMetaData();
+ while (resultSet.next()) {
+ LinkedHashMap<String, Long> map = new LinkedHashMap<>();
+ for (int i = 1; i <= resultMetaData.getColumnCount(); i++) {
+ String label = resultMetaData.getColumnLabel(i);
+ map.put(label, resultSet.getLong(label));
+ }
+ result.add(new Permission(map));
+ }
+ }
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/QueryExtended.java b/src/main/java/org/traccar/database/QueryExtended.java
new file mode 100644
index 000000000..07bc2c211
--- /dev/null
+++ b/src/main/java/org/traccar/database/QueryExtended.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QueryExtended {
+}
diff --git a/src/main/java/org/traccar/database/QueryIgnore.java b/src/main/java/org/traccar/database/QueryIgnore.java
new file mode 100644
index 000000000..ac835cf2f
--- /dev/null
+++ b/src/main/java/org/traccar/database/QueryIgnore.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QueryIgnore {
+}
diff --git a/src/main/java/org/traccar/database/SimpleObjectManager.java b/src/main/java/org/traccar/database/SimpleObjectManager.java
new file mode 100644
index 000000000..15dda4520
--- /dev/null
+++ b/src/main/java/org/traccar/database/SimpleObjectManager.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.sql.SQLException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Permission;
+import org.traccar.model.User;
+
+public abstract class SimpleObjectManager<T extends BaseModel> extends BaseObjectManager<T>
+ implements ManagableObjects {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleObjectManager.class);
+
+ private Map<Long, Set<Long>> userItems;
+
+ protected SimpleObjectManager(DataManager dataManager, Class<T> baseClass) {
+ super(dataManager, baseClass);
+ }
+
+ @Override
+ public final Set<Long> getUserItems(long userId) {
+ if (!userItems.containsKey(userId)) {
+ userItems.put(userId, new HashSet<Long>());
+ }
+ return userItems.get(userId);
+ }
+
+ @Override
+ public Set<Long> getManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getUserItems(userId));
+ for (long managedUserId : Context.getUsersManager().getUserItems(userId)) {
+ result.addAll(getUserItems(managedUserId));
+ }
+ return result;
+ }
+
+ public final boolean checkItemPermission(long userId, long itemId) {
+ return getUserItems(userId).contains(itemId);
+ }
+
+ @Override
+ public void refreshItems() {
+ super.refreshItems();
+ refreshUserItems();
+ }
+
+ public final void refreshUserItems() {
+ if (getDataManager() != null) {
+ try {
+ if (userItems != null) {
+ userItems.clear();
+ } else {
+ userItems = new ConcurrentHashMap<>();
+ }
+ for (Permission permission : getDataManager().getPermissions(User.class, getBaseClass())) {
+ getUserItems(permission.getOwnerId()).add(permission.getPropertyId());
+ }
+ } catch (SQLException | ClassNotFoundException error) {
+ LOGGER.warn("Error getting permissions", error);
+ }
+ }
+ }
+
+ @Override
+ public void removeItem(long itemId) throws SQLException {
+ super.removeItem(itemId);
+ refreshUserItems();
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/StatisticsManager.java b/src/main/java/org/traccar/database/StatisticsManager.java
new file mode 100644
index 000000000..e59f8e767
--- /dev/null
+++ b/src/main/java/org/traccar/database/StatisticsManager.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Statistics;
+
+import javax.inject.Inject;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Form;
+import java.sql.SQLException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class StatisticsManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(StatisticsManager.class);
+
+ private static final int SPLIT_MODE = Calendar.DAY_OF_MONTH;
+
+ private final Config config;
+ private final DataManager dataManager;
+ private final Client client;
+
+ private AtomicInteger lastUpdate = new AtomicInteger(Calendar.getInstance().get(SPLIT_MODE));
+
+ private Set<Long> users = new HashSet<>();
+ private Set<Long> devices = new HashSet<>();
+
+ private int requests;
+ private int messagesReceived;
+ private int messagesStored;
+ private int mailSent;
+ private int smsSent;
+ private int geocoderRequests;
+ private int geolocationRequests;
+
+ @Inject
+ public StatisticsManager(Config config, DataManager dataManager, Client client) {
+ this.config = config;
+ this.dataManager = dataManager;
+ this.client = client;
+ }
+
+ private void checkSplit() {
+ int currentUpdate = Calendar.getInstance().get(SPLIT_MODE);
+ if (lastUpdate.getAndSet(currentUpdate) != currentUpdate) {
+ Statistics statistics = new Statistics();
+ statistics.setCaptureTime(new Date());
+ statistics.setActiveUsers(users.size());
+ statistics.setActiveDevices(devices.size());
+ statistics.setRequests(requests);
+ statistics.setMessagesReceived(messagesReceived);
+ statistics.setMessagesStored(messagesStored);
+ statistics.setMailSent(mailSent);
+ statistics.setSmsSent(smsSent);
+ statistics.setGeocoderRequests(geocoderRequests);
+ statistics.setGeolocationRequests(geolocationRequests);
+
+ try {
+ dataManager.addObject(statistics);
+ } catch (SQLException e) {
+ LOGGER.warn("Error saving statistics", e);
+ }
+
+ String url = config.getString(Keys.SERVER_STATISTICS);
+ if (url != null) {
+ String time = DateUtil.formatDate(statistics.getCaptureTime());
+
+ Form form = new Form();
+ form.param("version", getClass().getPackage().getImplementationVersion());
+ form.param("captureTime", time);
+ form.param("activeUsers", String.valueOf(statistics.getActiveUsers()));
+ form.param("activeDevices", String.valueOf(statistics.getActiveDevices()));
+ form.param("requests", String.valueOf(statistics.getRequests()));
+ form.param("messagesReceived", String.valueOf(statistics.getMessagesReceived()));
+ form.param("messagesStored", String.valueOf(statistics.getMessagesStored()));
+ form.param("mailSent", String.valueOf(statistics.getMailSent()));
+ form.param("smsSent", String.valueOf(statistics.getSmsSent()));
+ form.param("geocoderRequests", String.valueOf(statistics.getGeocoderRequests()));
+ form.param("geolocationRequests", String.valueOf(statistics.getGeolocationRequests()));
+
+ client.target(url).request().async().post(Entity.form(form));
+ }
+
+ users.clear();
+ devices.clear();
+ requests = 0;
+ messagesReceived = 0;
+ messagesStored = 0;
+ mailSent = 0;
+ smsSent = 0;
+ geocoderRequests = 0;
+ geolocationRequests = 0;
+ }
+ }
+
+ public synchronized void registerRequest(long userId) {
+ checkSplit();
+ requests += 1;
+ if (userId != 0) {
+ users.add(userId);
+ }
+ }
+
+ public synchronized void registerMessageReceived() {
+ checkSplit();
+ messagesReceived += 1;
+ }
+
+ public synchronized void registerMessageStored(long deviceId) {
+ checkSplit();
+ messagesStored += 1;
+ if (deviceId != 0) {
+ devices.add(deviceId);
+ }
+ }
+
+ public synchronized void registerMail() {
+ checkSplit();
+ mailSent += 1;
+ }
+
+ public synchronized void registerSms() {
+ checkSplit();
+ smsSent += 1;
+ }
+
+ public synchronized void registerGeocoderRequest() {
+ checkSplit();
+ geocoderRequests += 1;
+ }
+
+ public synchronized void registerGeolocationRequest() {
+ checkSplit();
+ geolocationRequests += 1;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/UsersManager.java b/src/main/java/org/traccar/database/UsersManager.java
new file mode 100644
index 000000000..576a9e6c7
--- /dev/null
+++ b/src/main/java/org/traccar/database/UsersManager.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.traccar.model.User;
+
+public class UsersManager extends SimpleObjectManager<User> {
+
+ private Map<String, User> usersTokens;
+
+ public UsersManager(DataManager dataManager) {
+ super(dataManager, User.class);
+ if (usersTokens == null) {
+ usersTokens = new ConcurrentHashMap<>();
+ }
+ }
+
+ private void putToken(User user) {
+ if (usersTokens == null) {
+ usersTokens = new ConcurrentHashMap<>();
+ }
+ if (user.getToken() != null) {
+ usersTokens.put(user.getToken(), user);
+ }
+ }
+
+ @Override
+ protected void addNewItem(User user) {
+ super.addNewItem(user);
+ putToken(user);
+ }
+
+ @Override
+ protected void updateCachedItem(User user) {
+ User cachedUser = getById(user.getId());
+ super.updateCachedItem(user);
+ putToken(user);
+ if (cachedUser.getToken() != null && !cachedUser.getToken().equals(user.getToken())) {
+ usersTokens.remove(cachedUser.getToken());
+ }
+ }
+
+ @Override
+ protected void removeCachedItem(long userId) {
+ User cachedUser = getById(userId);
+ if (cachedUser != null) {
+ String userToken = cachedUser.getToken();
+ super.removeCachedItem(userId);
+ if (userToken != null) {
+ usersTokens.remove(userToken);
+ }
+ }
+ }
+
+ @Override
+ public Set<Long> getManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getUserItems(userId));
+ result.add(userId);
+ return result;
+ }
+
+ public User getUserByToken(String token) {
+ return usersTokens.get(token);
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/Address.java b/src/main/java/org/traccar/geocoder/Address.java
new file mode 100644
index 000000000..fe39da8e1
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/Address.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+public class Address {
+
+ private String postcode;
+
+ public String getPostcode() {
+ return postcode;
+ }
+
+ public void setPostcode(String postcode) {
+ this.postcode = postcode;
+ }
+
+ private String country;
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ private String state;
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ private String district;
+
+ public String getDistrict() {
+ return district;
+ }
+
+ public void setDistrict(String district) {
+ this.district = district;
+ }
+
+ private String settlement;
+
+ public String getSettlement() {
+ return settlement;
+ }
+
+ public void setSettlement(String settlement) {
+ this.settlement = settlement;
+ }
+
+ private String suburb;
+
+ public String getSuburb() {
+ return suburb;
+ }
+
+ public void setSuburb(String suburb) {
+ this.suburb = suburb;
+ }
+
+ private String street;
+
+ public String getStreet() {
+ return street;
+ }
+
+ public void setStreet(String street) {
+ this.street = street;
+ }
+
+ private String house;
+
+ public String getHouse() {
+ return house;
+ }
+
+ public void setHouse(String house) {
+ this.house = house;
+ }
+
+ private String formattedAddress;
+
+ public String getFormattedAddress() {
+ return formattedAddress;
+ }
+
+ public void setFormattedAddress(String formattedAddress) {
+ this.formattedAddress = formattedAddress;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/AddressFormat.java b/src/main/java/org/traccar/geocoder/AddressFormat.java
new file mode 100644
index 000000000..ad19432b9
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/AddressFormat.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+
+/**
+ * Available parameters:
+ *
+ * %p - postcode
+ * %c - country
+ * %s - state
+ * %d - district
+ * %t - settlement (town)
+ * %u - suburb
+ * %r - street (road)
+ * %h - house
+ * %f - formatted address
+ *
+ */
+public class AddressFormat extends Format {
+
+ private final String format;
+
+ public AddressFormat() {
+ this("%h %r, %t, %s, %c");
+ }
+
+ public AddressFormat(String format) {
+ this.format = format;
+ }
+
+ private static String replace(String s, String key, String value) {
+ if (value != null) {
+ s = s.replace(key, value);
+ } else {
+ s = s.replaceAll("[, ]*" + key, "");
+ }
+ return s;
+ }
+
+ @Override
+ public StringBuffer format(Object o, StringBuffer stringBuffer, FieldPosition fieldPosition) {
+ Address address = (Address) o;
+ String result = format;
+
+ result = replace(result, "%p", address.getPostcode());
+ result = replace(result, "%c", address.getCountry());
+ result = replace(result, "%s", address.getState());
+ result = replace(result, "%d", address.getDistrict());
+ result = replace(result, "%t", address.getSettlement());
+ result = replace(result, "%u", address.getSuburb());
+ result = replace(result, "%r", address.getStreet());
+ result = replace(result, "%h", address.getHouse());
+ result = replace(result, "%f", address.getFormattedAddress());
+
+ result = result.replaceAll("^[, ]*", "");
+
+ return stringBuffer.append(result);
+ }
+
+ @Override
+ public Address parseObject(String s, ParsePosition parsePosition) {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/BanGeocoder.java b/src/main/java/org/traccar/geocoder/BanGeocoder.java
new file mode 100644
index 000000000..b1f0900a4
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/BanGeocoder.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 Olivier Girondel (olivier@biniou.info)
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+/*
+ * API documentation: https://adresse.data.gouv.fr/api
+ */
+
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+
+public class BanGeocoder extends JsonGeocoder {
+
+ public BanGeocoder(int cacheSize, AddressFormat addressFormat) {
+ super("https://api-adresse.data.gouv.fr/reverse/?lat=%f&lon=%f", cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonArray result = json.getJsonArray("features");
+
+ if (result != null && !result.isEmpty()) {
+ JsonObject location = result.getJsonObject(0).getJsonObject("properties");
+ Address address = new Address();
+
+ address.setCountry("FR");
+ if (location.containsKey("postcode")) {
+ address.setPostcode(location.getString("postcode"));
+ }
+ if (location.containsKey("context")) {
+ address.setDistrict(location.getString("context"));
+ }
+ if (location.containsKey("name")) {
+ address.setStreet(location.getString("name"));
+ }
+ if (location.containsKey("city")) {
+ address.setSettlement(location.getString("city"));
+ }
+ if (location.containsKey("housenumber")) {
+ address.setHouse(location.getString("housenumber"));
+ }
+ if (location.containsKey("label")) {
+ address.setFormattedAddress(location.getString("label"));
+ }
+
+ return address;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/BingMapsGeocoder.java b/src/main/java/org/traccar/geocoder/BingMapsGeocoder.java
new file mode 100644
index 000000000..32a26ee0c
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/BingMapsGeocoder.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2014 - 2015 Stefaan Van Dooren (stefaan.vandooren@gmail.com)
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+
+public class BingMapsGeocoder extends JsonGeocoder {
+
+ public BingMapsGeocoder(String url, String key, int cacheSize, AddressFormat addressFormat) {
+ super(url + "/Locations/%f,%f?key=" + key + "&include=ciso2", cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonArray result = json.getJsonArray("resourceSets");
+ if (result != null) {
+ JsonObject location =
+ result.getJsonObject(0).getJsonArray("resources").getJsonObject(0).getJsonObject("address");
+ if (location != null) {
+ Address address = new Address();
+ if (location.containsKey("addressLine")) {
+ address.setStreet(location.getString("addressLine"));
+ }
+ if (location.containsKey("locality")) {
+ address.setSettlement(location.getString("locality"));
+ }
+ if (location.containsKey("adminDistrict2")) {
+ address.setDistrict(location.getString("adminDistrict2"));
+ }
+ if (location.containsKey("adminDistrict")) {
+ address.setState(location.getString("adminDistrict"));
+ }
+ if (location.containsKey("countryRegionIso2")) {
+ address.setCountry(location.getString("countryRegionIso2").toUpperCase());
+ }
+ if (location.containsKey("postalCode")) {
+ address.setPostcode(location.getString("postalCode"));
+ }
+ if (location.containsKey("formattedAddress")) {
+ address.setFormattedAddress(location.getString("formattedAddress"));
+ }
+ return address;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/FactualGeocoder.java b/src/main/java/org/traccar/geocoder/FactualGeocoder.java
new file mode 100644
index 000000000..c7a68c293
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/FactualGeocoder.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014 - 2015 Stefaan Van Dooren (stefaan.vandooren@gmail.com)
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonObject;
+
+public class FactualGeocoder extends JsonGeocoder {
+
+ public FactualGeocoder(String url, String key, int cacheSize, AddressFormat addressFormat) {
+ super(url + "?latitude=%f&longitude=%f&KEY=" + key, cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonObject result = json.getJsonObject("response").getJsonObject("data");
+ if (result != null) {
+ Address address = new Address();
+ if (result.getJsonObject("street_number") != null) {
+ address.setHouse(result.getJsonObject("street_number").getString("name"));
+ }
+ if (result.getJsonObject("street_name") != null) {
+ address.setStreet(result.getJsonObject("street_name").getString("name"));
+ }
+ if (result.getJsonObject("locality") != null) {
+ address.setSettlement(result.getJsonObject("locality").getString("name"));
+ }
+ if (result.getJsonObject("county") != null) {
+ address.setDistrict(result.getJsonObject("county").getString("name"));
+ }
+ if (result.getJsonObject("region") != null) {
+ address.setState(result.getJsonObject("region").getString("name"));
+ }
+ if (result.getJsonObject("country") != null) {
+ address.setCountry(result.getJsonObject("country").getString("name"));
+ }
+ if (result.getJsonObject("postcode") != null) {
+ address.setPostcode(result.getJsonObject("postcode").getString("name"));
+ }
+ return address;
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/GeocodeFarmGeocoder.java b/src/main/java/org/traccar/geocoder/GeocodeFarmGeocoder.java
new file mode 100644
index 000000000..39a3300a0
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/GeocodeFarmGeocoder.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonObject;
+
+public class GeocodeFarmGeocoder extends JsonGeocoder {
+
+ private static String formatUrl(String key, String language) {
+ String url = "https://www.geocode.farm/v3/json/reverse/";
+ url += "?lat=%f&lon=%f&country=us&count=1";
+ if (key != null) {
+ url += "&key=" + key;
+ }
+ if (language != null) {
+ url += "&lang=" + language;
+ }
+ return url;
+ }
+ public GeocodeFarmGeocoder(String key, String language, int cacheSize, AddressFormat addressFormat) {
+ super(formatUrl(key, language), cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ Address address = new Address();
+
+ JsonObject result = json
+ .getJsonObject("geocoding_results")
+ .getJsonArray("RESULTS")
+ .getJsonObject(0);
+
+ JsonObject resultAddress = result.getJsonObject("ADDRESS");
+
+ if (result.containsKey("formatted_address")) {
+ address.setFormattedAddress(result.getString("formatted_address"));
+ }
+ if (resultAddress.containsKey("street_number")) {
+ address.setStreet(resultAddress.getString("street_number"));
+ }
+ if (resultAddress.containsKey("street_name")) {
+ address.setStreet(resultAddress.getString("street_name"));
+ }
+ if (resultAddress.containsKey("locality")) {
+ address.setSettlement(resultAddress.getString("locality"));
+ }
+ if (resultAddress.containsKey("admin_1")) {
+ address.setState(resultAddress.getString("admin_1"));
+ }
+ if (resultAddress.containsKey("country")) {
+ address.setCountry(resultAddress.getString("country"));
+ }
+
+ return address;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/GeocodeXyzGeocoder.java b/src/main/java/org/traccar/geocoder/GeocodeXyzGeocoder.java
new file mode 100644
index 000000000..aca360c3d
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/GeocodeXyzGeocoder.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonObject;
+
+public class GeocodeXyzGeocoder extends JsonGeocoder {
+
+ private static String formatUrl(String key) {
+ String url = "https://geocode.xyz/%f,%f?geoit=JSON";
+ if (key != null) {
+ url += "&key=" + key;
+ }
+ return url;
+ }
+
+ public GeocodeXyzGeocoder(String key, int cacheSize, AddressFormat addressFormat) {
+ super(formatUrl(key), cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ Address address = new Address();
+
+ if (json.containsKey("stnumber")) {
+ address.setHouse(json.getString("stnumber"));
+ }
+ if (json.containsKey("staddress")) {
+ address.setStreet(json.getString("staddress"));
+ }
+ if (json.containsKey("city")) {
+ address.setSettlement(json.getString("city"));
+ }
+ if (json.containsKey("region")) {
+ address.setState(json.getString("region"));
+ }
+ if (json.containsKey("prov")) {
+ address.setCountry(json.getString("prov"));
+ }
+ if (json.containsKey("postal")) {
+ address.setPostcode(json.getString("postal"));
+ }
+
+ return address;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/Geocoder.java b/src/main/java/org/traccar/geocoder/Geocoder.java
new file mode 100644
index 000000000..587a27520
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/Geocoder.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+public interface Geocoder {
+
+ interface ReverseGeocoderCallback {
+
+ void onSuccess(String address);
+
+ void onFailure(Throwable e);
+
+ }
+
+ String getAddress(double latitude, double longitude, ReverseGeocoderCallback callback);
+
+}
diff --git a/src/main/java/org/traccar/geocoder/GeocoderException.java b/src/main/java/org/traccar/geocoder/GeocoderException.java
new file mode 100644
index 000000000..608916641
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/GeocoderException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+public class GeocoderException extends RuntimeException {
+
+ public GeocoderException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/GisgraphyGeocoder.java b/src/main/java/org/traccar/geocoder/GisgraphyGeocoder.java
new file mode 100644
index 000000000..3a173f985
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/GisgraphyGeocoder.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonObject;
+
+public class GisgraphyGeocoder extends JsonGeocoder {
+
+ public GisgraphyGeocoder(AddressFormat addressFormat) {
+ this("http://services.gisgraphy.com/reversegeocoding/search", 0, addressFormat);
+ }
+
+ public GisgraphyGeocoder(String url, int cacheSize, AddressFormat addressFormat) {
+ super(url + "?format=json&lat=%f&lng=%f&from=1&to=1", cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ Address address = new Address();
+
+ JsonObject result = json.getJsonArray("result").getJsonObject(0);
+
+ if (result.containsKey("streetName")) {
+ address.setStreet(result.getString("streetName"));
+ }
+ if (result.containsKey("city")) {
+ address.setSettlement(result.getString("city"));
+ }
+ if (result.containsKey("state")) {
+ address.setState(result.getString("state"));
+ }
+ if (result.containsKey("countryCode")) {
+ address.setCountry(result.getString("countryCode"));
+ }
+ if (result.containsKey("formatedFull")) {
+ address.setFormattedAddress(result.getString("formatedFull"));
+ }
+
+ return address;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/GoogleGeocoder.java b/src/main/java/org/traccar/geocoder/GoogleGeocoder.java
new file mode 100644
index 000000000..9494cab45
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/GoogleGeocoder.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2012 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.json.JsonString;
+
+public class GoogleGeocoder extends JsonGeocoder {
+
+ private static String formatUrl(String key, String language) {
+ String url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=%f,%f";
+ if (key != null) {
+ url += "&key=" + key;
+ }
+ if (language != null) {
+ url += "&language=" + language;
+ }
+ return url;
+ }
+
+ public GoogleGeocoder(String key, String language, int cacheSize, AddressFormat addressFormat) {
+ super(formatUrl(key, language), cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonArray results = json.getJsonArray("results");
+
+ if (!results.isEmpty()) {
+ Address address = new Address();
+
+ JsonObject result = (JsonObject) results.get(0);
+ JsonArray components = result.getJsonArray("address_components");
+
+ if (result.containsKey("formatted_address")) {
+ address.setFormattedAddress(result.getString("formatted_address"));
+ }
+
+ for (JsonObject component : components.getValuesAs(JsonObject.class)) {
+
+ String value = component.getString("short_name");
+
+ typesLoop: for (JsonString type : component.getJsonArray("types").getValuesAs(JsonString.class)) {
+
+ switch (type.getString()) {
+ case "street_number":
+ address.setHouse(value);
+ break typesLoop;
+ case "route":
+ address.setStreet(value);
+ break typesLoop;
+ case "locality":
+ address.setSettlement(value);
+ break typesLoop;
+ case "administrative_area_level_2":
+ address.setDistrict(value);
+ break typesLoop;
+ case "administrative_area_level_1":
+ address.setState(value);
+ break typesLoop;
+ case "country":
+ address.setCountry(value);
+ break typesLoop;
+ case "postal_code":
+ address.setPostcode(value);
+ break typesLoop;
+ default:
+ break;
+ }
+ }
+ }
+
+ return address;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected String parseError(JsonObject json) {
+ return json.getString("error_message");
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/HereGeocoder.java b/src/main/java/org/traccar/geocoder/HereGeocoder.java
new file mode 100644
index 000000000..756260b52
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/HereGeocoder.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonObject;
+
+public class HereGeocoder extends JsonGeocoder {
+
+ private static String formatUrl(String id, String key, String language) {
+ String url = "https://reverse.geocoder.api.here.com/6.2/reversegeocode.json";
+ url += "?mode=retrieveAddresses&maxresults=1";
+ url += "&prox=%f,%f,0";
+ url += "&app_id=" + id;
+ url += "&app_code=" + key;
+ if (language != null) {
+ url += "&language=" + language;
+ }
+ return url;
+ }
+
+ public HereGeocoder(String id, String key, String language, int cacheSize, AddressFormat addressFormat) {
+ super(formatUrl(id, key, language), cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonObject result = json
+ .getJsonObject("Response")
+ .getJsonArray("View")
+ .getJsonObject(0)
+ .getJsonArray("Result")
+ .getJsonObject(0)
+ .getJsonObject("Location")
+ .getJsonObject("Address");
+
+ if (result != null) {
+ Address address = new Address();
+
+ if (json.containsKey("Label")) {
+ address.setFormattedAddress(json.getString("Label"));
+ }
+
+ if (result.containsKey("HouseNumber")) {
+ address.setHouse(result.getString("HouseNumber"));
+ }
+ if (result.containsKey("Street")) {
+ address.setStreet(result.getString("Street"));
+ }
+ if (result.containsKey("City")) {
+ address.setSettlement(result.getString("City"));
+ }
+ if (result.containsKey("District")) {
+ address.setDistrict(result.getString("District"));
+ }
+ if (result.containsKey("State")) {
+ address.setState(result.getString("State"));
+ }
+ if (result.containsKey("Country")) {
+ address.setCountry(result.getString("Country").toUpperCase());
+ }
+ if (result.containsKey("PostalCode")) {
+ address.setPostcode(result.getString("PostalCode"));
+ }
+
+ return address;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/JsonGeocoder.java b/src/main/java/org/traccar/geocoder/JsonGeocoder.java
new file mode 100644
index 000000000..ed59a1d8d
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/JsonGeocoder.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+
+import javax.json.JsonObject;
+import javax.ws.rs.ClientErrorException;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.InvocationCallback;
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public abstract class JsonGeocoder implements Geocoder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JsonGeocoder.class);
+
+ private final String url;
+ private final AddressFormat addressFormat;
+
+ private Map<Map.Entry<Double, Double>, String> cache;
+
+ public JsonGeocoder(String url, final int cacheSize, AddressFormat addressFormat) {
+ this.url = url;
+ this.addressFormat = addressFormat;
+ if (cacheSize > 0) {
+ this.cache = Collections.synchronizedMap(new LinkedHashMap<Map.Entry<Double, Double>, String>() {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > cacheSize;
+ }
+ });
+ }
+ }
+
+ private String handleResponse(
+ double latitude, double longitude, JsonObject json, ReverseGeocoderCallback callback) {
+
+ Address address = parseAddress(json);
+ if (address != null) {
+ String formattedAddress = addressFormat.format(address);
+ if (cache != null) {
+ cache.put(new AbstractMap.SimpleImmutableEntry<>(latitude, longitude), formattedAddress);
+ }
+ if (callback != null) {
+ callback.onSuccess(formattedAddress);
+ }
+ return formattedAddress;
+ } else {
+ String msg = "Empty address. Error: " + parseError(json);
+ if (callback != null) {
+ callback.onFailure(new GeocoderException(msg));
+ } else {
+ LOGGER.warn(msg);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getAddress(
+ final double latitude, final double longitude, final ReverseGeocoderCallback callback) {
+
+ if (cache != null) {
+ String cachedAddress = cache.get(new AbstractMap.SimpleImmutableEntry<>(latitude, longitude));
+ if (cachedAddress != null) {
+ if (callback != null) {
+ callback.onSuccess(cachedAddress);
+ }
+ return cachedAddress;
+ }
+ }
+
+ Invocation.Builder request = Context.getClient().target(String.format(url, latitude, longitude)).request();
+
+ if (callback != null) {
+ request.async().get(new InvocationCallback<JsonObject>() {
+ @Override
+ public void completed(JsonObject json) {
+ handleResponse(latitude, longitude, json, callback);
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ callback.onFailure(throwable);
+ }
+ });
+ } else {
+ try {
+ return handleResponse(latitude, longitude, request.get(JsonObject.class), null);
+ } catch (ClientErrorException e) {
+ LOGGER.warn("Geocoder network error", e);
+ }
+ }
+ return null;
+ }
+
+ public abstract Address parseAddress(JsonObject json);
+
+ protected String parseError(JsonObject json) {
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/MapQuestGeocoder.java b/src/main/java/org/traccar/geocoder/MapQuestGeocoder.java
new file mode 100644
index 000000000..4029e3f07
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/MapQuestGeocoder.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2014 - 2015 Stefaan Van Dooren (stefaan.vandooren@gmail.com)
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+
+public class MapQuestGeocoder extends JsonGeocoder {
+
+ public MapQuestGeocoder(String url, String key, int cacheSize, AddressFormat addressFormat) {
+ super(url + "?key=" + key + "&location=%f,%f", cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonArray result = json.getJsonArray("results");
+ if (result != null) {
+ JsonArray locations = result.getJsonObject(0).getJsonArray("locations");
+ if (locations != null) {
+ JsonObject location = locations.getJsonObject(0);
+
+ Address address = new Address();
+
+ if (location.containsKey("street")) {
+ address.setStreet(location.getString("street"));
+ }
+ if (location.containsKey("adminArea5")) {
+ address.setSettlement(location.getString("adminArea5"));
+ }
+ if (location.containsKey("adminArea4")) {
+ address.setDistrict(location.getString("adminArea4"));
+ }
+ if (location.containsKey("adminArea3")) {
+ address.setState(location.getString("adminArea3"));
+ }
+ if (location.containsKey("adminArea1")) {
+ address.setCountry(location.getString("adminArea1").toUpperCase());
+ }
+ if (location.containsKey("postalCode")) {
+ address.setPostcode(location.getString("postalCode"));
+ }
+
+ return address;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/MapmyIndiaGeocoder.java b/src/main/java/org/traccar/geocoder/MapmyIndiaGeocoder.java
new file mode 100644
index 000000000..2b70708a1
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/MapmyIndiaGeocoder.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+
+public class MapmyIndiaGeocoder extends JsonGeocoder {
+
+ public MapmyIndiaGeocoder(String url, String key, int cacheSize, AddressFormat addressFormat) {
+ super(url + "/" + key + "/rev_geocode?lat=%f&lng=%f", cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonArray results = json.getJsonArray("results");
+
+ if (!results.isEmpty()) {
+ Address address = new Address();
+
+ JsonObject result = (JsonObject) results.get(0);
+
+ if (result.containsKey("formatted_address")) {
+ address.setFormattedAddress(result.getString("formatted_address"));
+ }
+
+ if (result.containsKey("house_number") && !result.getString("house_number").isEmpty()) {
+ address.setHouse(result.getString("house_number"));
+ } else if (result.containsKey("house_name") && !result.getString("house_name").isEmpty()) {
+ address.setHouse(result.getString("house_name"));
+ }
+
+ if (result.containsKey("street")) {
+ address.setStreet(result.getString("street"));
+ }
+
+ if (result.containsKey("locality") && !result.getString("locality").isEmpty()) {
+ address.setSuburb(result.getString("locality"));
+ } else if (result.containsKey("sublocality") && !result.getString("sublocality").isEmpty()) {
+ address.setSuburb(result.getString("sublocality"));
+ } else if (result.containsKey("subsublocality") && !result.getString("subsublocality").isEmpty()) {
+ address.setSuburb(result.getString("subsublocality"));
+ }
+
+ if (result.containsKey("city") && !result.getString("city").isEmpty()) {
+ address.setSettlement(result.getString("city"));
+ } else if (result.containsKey("village") && !result.getString("village").isEmpty()) {
+ address.setSettlement(result.getString("village"));
+ }
+
+ if (result.containsKey("district")) {
+ address.setDistrict(result.getString("district"));
+ } else if (result.containsKey("subDistrict")) {
+ address.setDistrict(result.getString("subDistrict"));
+ }
+
+ if (result.containsKey("state")) {
+ address.setState(result.getString("state"));
+ }
+
+ if (result.containsKey("pincode")) {
+ address.setPostcode(result.getString("pincode"));
+ }
+
+ return address;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/traccar/geocoder/NominatimGeocoder.java b/src/main/java/org/traccar/geocoder/NominatimGeocoder.java
new file mode 100644
index 000000000..8db25bf15
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/NominatimGeocoder.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2014 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonObject;
+
+public class NominatimGeocoder extends JsonGeocoder {
+
+ private static String formatUrl(String url, String key, String language) {
+ if (url == null) {
+ url = "https://nominatim.openstreetmap.org/reverse";
+ }
+ url += "?format=json&lat=%f&lon=%f&zoom=18&addressdetails=1";
+ if (key != null) {
+ url += "&key=" + key;
+ }
+ if (language != null) {
+ url += "&accept-language=" + language;
+ }
+ return url;
+ }
+
+ public NominatimGeocoder(String url, String key, String language, int cacheSize, AddressFormat addressFormat) {
+ super(formatUrl(url, key, language), cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonObject result = json.getJsonObject("address");
+
+ if (result != null) {
+ Address address = new Address();
+
+ if (json.containsKey("display_name")) {
+ address.setFormattedAddress(json.getString("display_name"));
+ }
+
+ if (result.containsKey("house_number")) {
+ address.setHouse(result.getString("house_number"));
+ }
+ if (result.containsKey("road")) {
+ address.setStreet(result.getString("road"));
+ }
+ if (result.containsKey("suburb")) {
+ address.setSuburb(result.getString("suburb"));
+ }
+
+ if (result.containsKey("village")) {
+ address.setSettlement(result.getString("village"));
+ } else if (result.containsKey("town")) {
+ address.setSettlement(result.getString("town"));
+ } else if (result.containsKey("city")) {
+ address.setSettlement(result.getString("city"));
+ }
+
+ if (result.containsKey("state_district")) {
+ address.setDistrict(result.getString("state_district"));
+ } else if (result.containsKey("region")) {
+ address.setDistrict(result.getString("region"));
+ }
+
+ if (result.containsKey("state")) {
+ address.setState(result.getString("state"));
+ }
+ if (result.containsKey("country_code")) {
+ address.setCountry(result.getString("country_code").toUpperCase());
+ }
+ if (result.containsKey("postcode")) {
+ address.setPostcode(result.getString("postcode"));
+ }
+
+ return address;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geocoder/OpenCageGeocoder.java b/src/main/java/org/traccar/geocoder/OpenCageGeocoder.java
new file mode 100644
index 000000000..822b6e91e
--- /dev/null
+++ b/src/main/java/org/traccar/geocoder/OpenCageGeocoder.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2014 - 2015 Stefaan Van Dooren (stefaan.vandooren@gmail.com)
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geocoder;
+
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+
+public class OpenCageGeocoder extends JsonGeocoder {
+
+ public OpenCageGeocoder(String url, String key, int cacheSize, AddressFormat addressFormat) {
+ super(url + "/json?q=%f,%f&no_annotations=1&key=" + key, cacheSize, addressFormat);
+ }
+
+ @Override
+ public Address parseAddress(JsonObject json) {
+ JsonArray result = json.getJsonArray("results");
+ if (result != null) {
+ JsonObject location = result.getJsonObject(0).getJsonObject("components");
+ if (location != null) {
+ Address address = new Address();
+
+ if (result.getJsonObject(0).containsKey("formatted")) {
+ address.setFormattedAddress(result.getJsonObject(0).getString("formatted"));
+ }
+ if (location.containsKey("building")) {
+ address.setHouse(location.getString("building"));
+ }
+ if (location.containsKey("house_number")) {
+ address.setHouse(location.getString("house_number"));
+ }
+ if (location.containsKey("road")) {
+ address.setStreet(location.getString("road"));
+ }
+ if (location.containsKey("suburb")) {
+ address.setSuburb(location.getString("suburb"));
+ }
+ if (location.containsKey("city")) {
+ address.setSettlement(location.getString("city"));
+ }
+ if (location.containsKey("city_district")) {
+ address.setSettlement(location.getString("city_district"));
+ }
+ if (location.containsKey("county")) {
+ address.setDistrict(location.getString("county"));
+ }
+ if (location.containsKey("state")) {
+ address.setState(location.getString("state"));
+ }
+ if (location.containsKey("country_code")) {
+ address.setCountry(location.getString("country_code").toUpperCase());
+ }
+ if (location.containsKey("postcode")) {
+ address.setPostcode(location.getString("postcode"));
+ }
+
+ return address;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geofence/GeofenceCircle.java b/src/main/java/org/traccar/geofence/GeofenceCircle.java
new file mode 100644
index 000000000..f6fca63ca
--- /dev/null
+++ b/src/main/java/org/traccar/geofence/GeofenceCircle.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geofence;
+
+import java.text.DecimalFormat;
+import java.text.ParseException;
+
+import org.traccar.helper.DistanceCalculator;
+
+public class GeofenceCircle extends GeofenceGeometry {
+
+ private double centerLatitude;
+ private double centerLongitude;
+ private double radius;
+
+ public GeofenceCircle() {
+ }
+
+ public GeofenceCircle(String wkt) throws ParseException {
+ fromWkt(wkt);
+ }
+
+ public GeofenceCircle(double latitude, double longitude, double radius) {
+ this.centerLatitude = latitude;
+ this.centerLongitude = longitude;
+ this.radius = radius;
+ }
+
+ public double distanceFromCenter(double latitude, double longitude) {
+ return DistanceCalculator.distance(centerLatitude, centerLongitude, latitude, longitude);
+ }
+
+ @Override
+ public boolean containsPoint(double latitude, double longitude) {
+ return distanceFromCenter(latitude, longitude) <= radius;
+ }
+
+ @Override
+ public String toWkt() {
+ String wkt = "";
+ wkt = "CIRCLE (";
+ wkt += String.valueOf(centerLatitude);
+ wkt += " ";
+ wkt += String.valueOf(centerLongitude);
+ wkt += ", ";
+ DecimalFormat format = new DecimalFormat("0.#");
+ wkt += format.format(radius);
+ wkt += ")";
+ return wkt;
+ }
+
+ @Override
+ public void fromWkt(String wkt) throws ParseException {
+ if (!wkt.startsWith("CIRCLE")) {
+ throw new ParseException("Mismatch geometry type", 0);
+ }
+ String content = wkt.substring(wkt.indexOf("(") + 1, wkt.indexOf(")"));
+ if (content == null || content.equals("")) {
+ throw new ParseException("No content", 0);
+ }
+ String[] commaTokens = content.split(",");
+ if (commaTokens.length != 2) {
+ throw new ParseException("Not valid content", 0);
+ }
+ String[] tokens = commaTokens[0].split("\\s");
+ if (tokens.length != 2) {
+ throw new ParseException("Too much or less coordinates", 0);
+ }
+ try {
+ centerLatitude = Double.parseDouble(tokens[0]);
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[0] + " is not a double", 0);
+ }
+ try {
+ centerLongitude = Double.parseDouble(tokens[1]);
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[1] + " is not a double", 0);
+ }
+ try {
+ radius = Double.parseDouble(commaTokens[1]);
+ } catch (NumberFormatException e) {
+ throw new ParseException(commaTokens[1] + " is not a double", 0);
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/geofence/GeofenceGeometry.java b/src/main/java/org/traccar/geofence/GeofenceGeometry.java
new file mode 100644
index 000000000..857ba3414
--- /dev/null
+++ b/src/main/java/org/traccar/geofence/GeofenceGeometry.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geofence;
+
+import java.text.ParseException;
+
+public abstract class GeofenceGeometry {
+
+ public abstract boolean containsPoint(double latitude, double longitude);
+
+ public abstract String toWkt();
+
+ public abstract void fromWkt(String wkt) throws ParseException;
+
+ public static class Coordinate {
+
+ private double lat;
+ private double lon;
+
+ public double getLat() {
+ return lat;
+ }
+
+ public void setLat(double lat) {
+ this.lat = lat;
+ }
+
+ public double getLon() {
+ return lon;
+ }
+
+ public void setLon(double lon) {
+ this.lon = lon;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/geofence/GeofencePolygon.java b/src/main/java/org/traccar/geofence/GeofencePolygon.java
new file mode 100644
index 000000000..2048ba26d
--- /dev/null
+++ b/src/main/java/org/traccar/geofence/GeofencePolygon.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geofence;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+
+public class GeofencePolygon extends GeofenceGeometry {
+
+ public GeofencePolygon() {
+ }
+
+ public GeofencePolygon(String wkt) throws ParseException {
+ fromWkt(wkt);
+ }
+
+ private ArrayList<Coordinate> coordinates;
+
+ private double[] constant;
+ private double[] multiple;
+
+ private boolean needNormalize = false;
+
+ private void precalc() {
+ if (coordinates == null) {
+ return;
+ }
+
+ int polyCorners = coordinates.size();
+ int i;
+ int j = polyCorners - 1;
+
+ if (constant != null) {
+ constant = null;
+ }
+ if (multiple != null) {
+ multiple = null;
+ }
+
+ constant = new double[polyCorners];
+ multiple = new double[polyCorners];
+
+ boolean hasNegative = false;
+ boolean hasPositive = false;
+ for (i = 0; i < polyCorners; i++) {
+ if (coordinates.get(i).getLon() > 90) {
+ hasPositive = true;
+ } else if (coordinates.get(i).getLon() < -90) {
+ hasNegative = true;
+ }
+ }
+ needNormalize = hasPositive && hasNegative;
+
+ for (i = 0; i < polyCorners; j = i++) {
+ if (normalizeLon(coordinates.get(j).getLon()) == normalizeLon(coordinates.get(i).getLon())) {
+ constant[i] = coordinates.get(i).getLat();
+ multiple[i] = 0;
+ } else {
+ constant[i] = coordinates.get(i).getLat()
+ - (normalizeLon(coordinates.get(i).getLon()) * coordinates.get(j).getLat())
+ / (normalizeLon(coordinates.get(j).getLon()) - normalizeLon(coordinates.get(i).getLon()))
+ + (normalizeLon(coordinates.get(i).getLon()) * coordinates.get(i).getLat())
+ / (normalizeLon(coordinates.get(j).getLon()) - normalizeLon(coordinates.get(i).getLon()));
+ multiple[i] = (coordinates.get(j).getLat() - coordinates.get(i).getLat())
+ / (normalizeLon(coordinates.get(j).getLon()) - normalizeLon(coordinates.get(i).getLon()));
+ }
+ }
+ }
+
+ private double normalizeLon(double lon) {
+ if (needNormalize && lon < -90) {
+ return lon + 360;
+ }
+ return lon;
+ }
+
+ @Override
+ public boolean containsPoint(double latitude, double longitude) {
+
+ int polyCorners = coordinates.size();
+ int i;
+ int j = polyCorners - 1;
+ double longitudeNorm = normalizeLon(longitude);
+ boolean oddNodes = false;
+
+ for (i = 0; i < polyCorners; j = i++) {
+ if (normalizeLon(coordinates.get(i).getLon()) < longitudeNorm
+ && normalizeLon(coordinates.get(j).getLon()) >= longitudeNorm
+ || normalizeLon(coordinates.get(j).getLon()) < longitudeNorm
+ && normalizeLon(coordinates.get(i).getLon()) >= longitudeNorm) {
+ oddNodes ^= longitudeNorm * multiple[i] + constant[i] < latitude;
+ }
+ }
+ return oddNodes;
+ }
+
+ @Override
+ public String toWkt() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("POLYGON ((");
+ for (Coordinate coordinate : coordinates) {
+ buf.append(String.valueOf(coordinate.getLat()));
+ buf.append(" ");
+ buf.append(String.valueOf(coordinate.getLon()));
+ buf.append(", ");
+ }
+ return buf.substring(0, buf.length() - 2) + "))";
+ }
+
+ @Override
+ public void fromWkt(String wkt) throws ParseException {
+ if (coordinates == null) {
+ coordinates = new ArrayList<>();
+ } else {
+ coordinates.clear();
+ }
+
+ if (!wkt.startsWith("POLYGON")) {
+ throw new ParseException("Mismatch geometry type", 0);
+ }
+ String content = wkt.substring(wkt.indexOf("((") + 2, wkt.indexOf("))"));
+ if (content.isEmpty()) {
+ throw new ParseException("No content", 0);
+ }
+ String[] commaTokens = content.split(",");
+ if (commaTokens.length < 3) {
+ throw new ParseException("Not valid content", 0);
+ }
+
+ for (String commaToken : commaTokens) {
+ String[] tokens = commaToken.trim().split("\\s");
+ if (tokens.length != 2) {
+ throw new ParseException("Here must be two coordinates: " + commaToken, 0);
+ }
+ Coordinate coordinate = new Coordinate();
+ try {
+ coordinate.setLat(Double.parseDouble(tokens[0]));
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[0] + " is not a double", 0);
+ }
+ try {
+ coordinate.setLon(Double.parseDouble(tokens[1]));
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[1] + " is not a double", 0);
+ }
+ coordinates.add(coordinate);
+ }
+ precalc();
+ }
+
+}
diff --git a/src/main/java/org/traccar/geofence/GeofencePolyline.java b/src/main/java/org/traccar/geofence/GeofencePolyline.java
new file mode 100644
index 000000000..d84f512e3
--- /dev/null
+++ b/src/main/java/org/traccar/geofence/GeofencePolyline.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geofence;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+
+import org.traccar.helper.DistanceCalculator;
+
+public class GeofencePolyline extends GeofenceGeometry {
+
+ private ArrayList<Coordinate> coordinates;
+ private double distance;
+
+ public GeofencePolyline() {
+ }
+
+ public GeofencePolyline(String wkt, double distance) throws ParseException {
+ fromWkt(wkt);
+ this.distance = distance;
+ }
+
+ @Override
+ public boolean containsPoint(double latitude, double longitude) {
+ for (int i = 1; i < coordinates.size(); i++) {
+ if (DistanceCalculator.distanceToLine(
+ latitude, longitude, coordinates.get(i - 1).getLat(), coordinates.get(i - 1).getLon(),
+ coordinates.get(i).getLat(), coordinates.get(i).getLon()) <= distance) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toWkt() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("LINESTRING (");
+ for (Coordinate coordinate : coordinates) {
+ buf.append(String.valueOf(coordinate.getLat()));
+ buf.append(" ");
+ buf.append(String.valueOf(coordinate.getLon()));
+ buf.append(", ");
+ }
+ return buf.substring(0, buf.length() - 2) + ")";
+ }
+
+ @Override
+ public void fromWkt(String wkt) throws ParseException {
+ if (coordinates == null) {
+ coordinates = new ArrayList<>();
+ } else {
+ coordinates.clear();
+ }
+
+ if (!wkt.startsWith("LINESTRING")) {
+ throw new ParseException("Mismatch geometry type", 0);
+ }
+ String content = wkt.substring(wkt.indexOf("(") + 1, wkt.indexOf(")"));
+ if (content.isEmpty()) {
+ throw new ParseException("No content", 0);
+ }
+ String[] commaTokens = content.split(",");
+ if (commaTokens.length < 2) {
+ throw new ParseException("Not valid content", 0);
+ }
+
+ for (String commaToken : commaTokens) {
+ String[] tokens = commaToken.trim().split("\\s");
+ if (tokens.length != 2) {
+ throw new ParseException("Here must be two coordinates: " + commaToken, 0);
+ }
+ Coordinate coordinate = new Coordinate();
+ try {
+ coordinate.setLat(Double.parseDouble(tokens[0]));
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[0] + " is not a double", 0);
+ }
+ try {
+ coordinate.setLon(Double.parseDouble(tokens[1]));
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[1] + " is not a double", 0);
+ }
+ coordinates.add(coordinate);
+ }
+
+ }
+
+ public void setDistance(double distance) {
+ this.distance = distance;
+ }
+
+}
diff --git a/src/main/java/org/traccar/geolocation/GeolocationException.java b/src/main/java/org/traccar/geolocation/GeolocationException.java
new file mode 100644
index 000000000..5847cc807
--- /dev/null
+++ b/src/main/java/org/traccar/geolocation/GeolocationException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geolocation;
+
+public class GeolocationException extends RuntimeException {
+
+ public GeolocationException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/main/java/org/traccar/geolocation/GeolocationProvider.java b/src/main/java/org/traccar/geolocation/GeolocationProvider.java
new file mode 100644
index 000000000..d9dec6bbb
--- /dev/null
+++ b/src/main/java/org/traccar/geolocation/GeolocationProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geolocation;
+
+import org.traccar.model.Network;
+
+public interface GeolocationProvider {
+
+ interface LocationProviderCallback {
+
+ void onSuccess(double latitude, double longitude, double accuracy);
+
+ void onFailure(Throwable e);
+
+ }
+
+ void getLocation(Network network, LocationProviderCallback callback);
+
+}
diff --git a/src/main/java/org/traccar/geolocation/GoogleGeolocationProvider.java b/src/main/java/org/traccar/geolocation/GoogleGeolocationProvider.java
new file mode 100644
index 000000000..5901b47cd
--- /dev/null
+++ b/src/main/java/org/traccar/geolocation/GoogleGeolocationProvider.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geolocation;
+
+public class GoogleGeolocationProvider extends UniversalGeolocationProvider {
+
+ private static final String URL = "https://www.googleapis.com/geolocation/v1/geolocate";
+
+ public GoogleGeolocationProvider(String key) {
+ super(URL, key);
+ }
+
+}
diff --git a/src/main/java/org/traccar/geolocation/MozillaGeolocationProvider.java b/src/main/java/org/traccar/geolocation/MozillaGeolocationProvider.java
new file mode 100644
index 000000000..c6a73a52b
--- /dev/null
+++ b/src/main/java/org/traccar/geolocation/MozillaGeolocationProvider.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geolocation;
+
+public class MozillaGeolocationProvider extends UniversalGeolocationProvider {
+
+ private static final String URL = "https://location.services.mozilla.com/v1/geolocate";
+
+ public MozillaGeolocationProvider(String key) {
+ super(URL, key != null ? key : "test");
+ }
+
+}
diff --git a/src/main/java/org/traccar/geolocation/OpenCellIdGeolocationProvider.java b/src/main/java/org/traccar/geolocation/OpenCellIdGeolocationProvider.java
new file mode 100644
index 000000000..768aaf6a2
--- /dev/null
+++ b/src/main/java/org/traccar/geolocation/OpenCellIdGeolocationProvider.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geolocation;
+
+import org.traccar.Context;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+
+import javax.json.JsonObject;
+import javax.ws.rs.client.InvocationCallback;
+
+public class OpenCellIdGeolocationProvider implements GeolocationProvider {
+
+ private String url;
+
+ public OpenCellIdGeolocationProvider(String key) {
+ this("http://opencellid.org/cell/get", key);
+ }
+
+ public OpenCellIdGeolocationProvider(String url, String key) {
+ this.url = url + "?format=json&mcc=%d&mnc=%d&lac=%d&cellid=%d&key=" + key;
+ }
+
+ @Override
+ public void getLocation(Network network, final LocationProviderCallback callback) {
+ if (network.getCellTowers() != null && !network.getCellTowers().isEmpty()) {
+
+ CellTower cellTower = network.getCellTowers().iterator().next();
+ String request = String.format(url, cellTower.getMobileCountryCode(), cellTower.getMobileNetworkCode(),
+ cellTower.getLocationAreaCode(), cellTower.getCellId());
+
+ Context.getClient().target(request).request().async().get(new InvocationCallback<JsonObject>() {
+ @Override
+ public void completed(JsonObject json) {
+ if (json.containsKey("lat") && json.containsKey("lon")) {
+ callback.onSuccess(
+ json.getJsonNumber("lat").doubleValue(),
+ json.getJsonNumber("lon").doubleValue(), 0);
+ } else {
+ callback.onFailure(new GeolocationException("Coordinates are missing"));
+ }
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ callback.onFailure(throwable);
+ }
+ });
+
+ } else {
+ callback.onFailure(new GeolocationException("No network information"));
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/geolocation/UniversalGeolocationProvider.java b/src/main/java/org/traccar/geolocation/UniversalGeolocationProvider.java
new file mode 100644
index 000000000..f71620d8a
--- /dev/null
+++ b/src/main/java/org/traccar/geolocation/UniversalGeolocationProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geolocation;
+
+import org.traccar.Context;
+import org.traccar.model.Network;
+
+import javax.json.JsonObject;
+import javax.ws.rs.client.AsyncInvoker;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.InvocationCallback;
+
+public class UniversalGeolocationProvider implements GeolocationProvider {
+
+ private String url;
+
+ public UniversalGeolocationProvider(String url, String key) {
+ this.url = url + "?key=" + key;
+ }
+
+ @Override
+ public void getLocation(Network network, final LocationProviderCallback callback) {
+ AsyncInvoker invoker = Context.getClient().target(url).request().async();
+ invoker.post(Entity.json(network), new InvocationCallback<JsonObject>() {
+ @Override
+ public void completed(JsonObject json) {
+ if (json.containsKey("error")) {
+ callback.onFailure(new GeolocationException(json.getJsonObject("error").getString("message")));
+ } else {
+ JsonObject location = json.getJsonObject("location");
+ callback.onSuccess(
+ location.getJsonNumber("lat").doubleValue(),
+ location.getJsonNumber("lng").doubleValue(),
+ json.getJsonNumber("accuracy").doubleValue());
+ }
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ callback.onFailure(throwable);
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/geolocation/UnwiredGeolocationProvider.java b/src/main/java/org/traccar/geolocation/UnwiredGeolocationProvider.java
new file mode 100644
index 000000000..963bcb688
--- /dev/null
+++ b/src/main/java/org/traccar/geolocation/UnwiredGeolocationProvider.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.geolocation;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.traccar.Context;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.WifiAccessPoint;
+
+import javax.json.JsonObject;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.InvocationCallback;
+import java.util.Collection;
+
+public class UnwiredGeolocationProvider implements GeolocationProvider {
+
+ private String url;
+ private String key;
+
+ private ObjectMapper objectMapper;
+
+ private abstract static class NetworkMixIn {
+ @JsonProperty("mcc")
+ abstract Integer getHomeMobileCountryCode();
+ @JsonProperty("mnc")
+ abstract Integer getHomeMobileNetworkCode();
+ @JsonProperty("radio")
+ abstract String getRadioType();
+ @JsonIgnore
+ abstract String getCarrier();
+ @JsonIgnore
+ abstract Boolean getConsiderIp();
+ @JsonProperty("cells")
+ abstract Collection<CellTower> getCellTowers();
+ @JsonProperty("wifi")
+ abstract Collection<WifiAccessPoint> getWifiAccessPoints();
+ }
+
+ private abstract static class CellTowerMixIn {
+ @JsonProperty("radio")
+ abstract String getRadioType();
+ @JsonProperty("mcc")
+ abstract Integer getMobileCountryCode();
+ @JsonProperty("mnc")
+ abstract Integer getMobileNetworkCode();
+ @JsonProperty("lac")
+ abstract Integer getLocationAreaCode();
+ @JsonProperty("cid")
+ abstract Long getCellId();
+ }
+
+ private abstract static class WifiAccessPointMixIn {
+ @JsonProperty("bssid")
+ abstract String getMacAddress();
+ @JsonProperty("signal")
+ abstract Integer getSignalStrength();
+ }
+
+ public UnwiredGeolocationProvider(String url, String key) {
+ this.url = url;
+ this.key = key;
+
+ objectMapper = new ObjectMapper();
+ objectMapper.addMixIn(Network.class, NetworkMixIn.class);
+ objectMapper.addMixIn(CellTower.class, CellTowerMixIn.class);
+ objectMapper.addMixIn(WifiAccessPoint.class, WifiAccessPointMixIn.class);
+ }
+
+ @Override
+ public void getLocation(Network network, final LocationProviderCallback callback) {
+ ObjectNode json = objectMapper.valueToTree(network);
+ json.put("token", key);
+
+ Context.getClient().target(url).request().async().post(Entity.json(json), new InvocationCallback<JsonObject>() {
+ @Override
+ public void completed(JsonObject json) {
+ if (json.getString("status").equals("error")) {
+ callback.onFailure(new GeolocationException(json.getString("message")));
+ } else {
+ callback.onSuccess(
+ json.getJsonNumber("lat").doubleValue(),
+ json.getJsonNumber("lon").doubleValue(),
+ json.getJsonNumber("accuracy").doubleValue());
+ }
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ callback.onFailure(throwable);
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/ComputedAttributesHandler.java b/src/main/java/org/traccar/handler/ComputedAttributesHandler.java
new file mode 100644
index 000000000..153da29b9
--- /dev/null
+++ b/src/main/java/org/traccar/handler/ComputedAttributesHandler.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import io.netty.channel.ChannelHandler;
+import org.apache.commons.jexl2.JexlEngine;
+import org.apache.commons.jexl2.JexlException;
+import org.apache.commons.jexl2.MapContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseDataHandler;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.database.AttributesManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Attribute;
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class ComputedAttributesHandler extends BaseDataHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ComputedAttributesHandler.class);
+
+ private final IdentityManager identityManager;
+ private final AttributesManager attributesManager;
+
+ private final JexlEngine engine;
+
+ private final boolean includeDeviceAttributes;
+
+ public ComputedAttributesHandler(
+ Config config, IdentityManager identityManager, AttributesManager attributesManager) {
+ this.identityManager = identityManager;
+ this.attributesManager = attributesManager;
+ engine = new JexlEngine();
+ engine.setStrict(true);
+ engine.setFunctions(Collections.singletonMap("math", Math.class));
+ includeDeviceAttributes = config.getBoolean(Keys.PROCESSING_COMPUTED_ATTRIBUTES_DEVICE_ATTRIBUTES);
+ }
+
+ private MapContext prepareContext(Position position) {
+ MapContext result = new MapContext();
+ if (includeDeviceAttributes) {
+ Device device = identityManager.getById(position.getDeviceId());
+ if (device != null) {
+ for (Object key : device.getAttributes().keySet()) {
+ result.set((String) key, device.getAttributes().get(key));
+ }
+ }
+ }
+ Set<Method> methods = new HashSet<>(Arrays.asList(position.getClass().getMethods()));
+ methods.removeAll(Arrays.asList(Object.class.getMethods()));
+ for (Method method : methods) {
+ if (method.getName().startsWith("get") && method.getParameterTypes().length == 0) {
+ String name = Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4);
+
+ try {
+ if (!method.getReturnType().equals(Map.class)) {
+ result.set(name, method.invoke(position));
+ } else {
+ for (Object key : ((Map) method.invoke(position)).keySet()) {
+ result.set((String) key, ((Map) method.invoke(position)).get(key));
+ }
+ }
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Attribute reflection error", error);
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @deprecated logic needs to be extracted to be used in API resource
+ */
+ @Deprecated
+ public Object computeAttribute(Attribute attribute, Position position) throws JexlException {
+ return engine.createExpression(attribute.getExpression()).evaluate(prepareContext(position));
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+ Collection<Attribute> attributes = attributesManager.getItems(
+ attributesManager.getAllDeviceItems(position.getDeviceId()));
+ for (Attribute attribute : attributes) {
+ if (attribute.getAttribute() != null) {
+ Object result = null;
+ try {
+ result = computeAttribute(attribute, position);
+ } catch (JexlException error) {
+ LOGGER.warn("Attribute computation error", error);
+ }
+ if (result != null) {
+ try {
+ switch (attribute.getType()) {
+ case "number":
+ Number numberValue = (Number) result;
+ position.getAttributes().put(attribute.getAttribute(), numberValue);
+ break;
+ case "boolean":
+ Boolean booleanValue = (Boolean) result;
+ position.getAttributes().put(attribute.getAttribute(), booleanValue);
+ break;
+ default:
+ position.getAttributes().put(attribute.getAttribute(), result.toString());
+ }
+ } catch (ClassCastException error) {
+ LOGGER.warn("Attribute cast error", error);
+ }
+ }
+ }
+ }
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/CopyAttributesHandler.java b/src/main/java/org/traccar/handler/CopyAttributesHandler.java
new file mode 100644
index 000000000..6a0966d33
--- /dev/null
+++ b/src/main/java/org/traccar/handler/CopyAttributesHandler.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.BaseDataHandler;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class CopyAttributesHandler extends BaseDataHandler {
+
+ private IdentityManager identityManager;
+
+ public CopyAttributesHandler(IdentityManager identityManager) {
+ this.identityManager = identityManager;
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+ String attributesString = identityManager.lookupAttributeString(
+ position.getDeviceId(), "processing.copyAttributes", "", true);
+ if (attributesString.isEmpty()) {
+ attributesString = Position.KEY_DRIVER_UNIQUE_ID;
+ } else {
+ attributesString += "," + Position.KEY_DRIVER_UNIQUE_ID;
+ }
+ Position last = identityManager.getLastPosition(position.getDeviceId());
+ if (last != null) {
+ for (String attribute : attributesString.split("[ ,]")) {
+ if (last.getAttributes().containsKey(attribute) && !position.getAttributes().containsKey(attribute)) {
+ position.getAttributes().put(attribute, last.getAttributes().get(attribute));
+ }
+ }
+ }
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/DefaultDataHandler.java b/src/main/java/org/traccar/handler/DefaultDataHandler.java
new file mode 100644
index 000000000..9d8ea044d
--- /dev/null
+++ b/src/main/java/org/traccar/handler/DefaultDataHandler.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseDataHandler;
+import org.traccar.database.DataManager;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class DefaultDataHandler extends BaseDataHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDataHandler.class);
+
+ private final DataManager dataManager;
+
+ public DefaultDataHandler(DataManager dataManager) {
+ this.dataManager = dataManager;
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+
+ try {
+ dataManager.addObject(position);
+ } catch (Exception error) {
+ LOGGER.warn("Failed to store position", error);
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/DistanceHandler.java b/src/main/java/org/traccar/handler/DistanceHandler.java
new file mode 100644
index 000000000..a336a884e
--- /dev/null
+++ b/src/main/java/org/traccar/handler/DistanceHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2015 Amila Silva
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.BaseDataHandler;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.database.IdentityManager;
+import org.traccar.helper.DistanceCalculator;
+import org.traccar.model.Position;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+@ChannelHandler.Sharable
+public class DistanceHandler extends BaseDataHandler {
+
+ private final IdentityManager identityManager;
+
+ private final boolean filter;
+ private final int coordinatesMinError;
+ private final int coordinatesMaxError;
+
+ public DistanceHandler(Config config, IdentityManager identityManager) {
+ this.identityManager = identityManager;
+ this.filter = config.getBoolean(Keys.COORDINATES_FILTER);
+ this.coordinatesMinError = config.getInteger(Keys.COORDINATES_MIN_ERROR);
+ this.coordinatesMaxError = config.getInteger(Keys.COORDINATES_MAX_ERROR);
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+
+ double distance = 0.0;
+ if (position.getAttributes().containsKey(Position.KEY_DISTANCE)) {
+ distance = position.getDouble(Position.KEY_DISTANCE);
+ }
+ double totalDistance = 0.0;
+
+ Position last = identityManager != null ? identityManager.getLastPosition(position.getDeviceId()) : null;
+ if (last != null) {
+ totalDistance = last.getDouble(Position.KEY_TOTAL_DISTANCE);
+ if (!position.getAttributes().containsKey(Position.KEY_DISTANCE)) {
+ distance = DistanceCalculator.distance(
+ position.getLatitude(), position.getLongitude(),
+ last.getLatitude(), last.getLongitude());
+ distance = BigDecimal.valueOf(distance).setScale(2, RoundingMode.HALF_EVEN).doubleValue();
+ }
+ if (filter && last.getValid() && last.getLatitude() != 0 && last.getLongitude() != 0) {
+ boolean satisfiesMin = coordinatesMinError == 0 || distance > coordinatesMinError;
+ boolean satisfiesMax = coordinatesMaxError == 0
+ || distance < coordinatesMaxError || position.getValid();
+ if (!satisfiesMin || !satisfiesMax) {
+ position.setLatitude(last.getLatitude());
+ position.setLongitude(last.getLongitude());
+ distance = 0;
+ }
+ }
+ }
+ position.set(Position.KEY_DISTANCE, distance);
+ totalDistance = BigDecimal.valueOf(totalDistance + distance).setScale(2, RoundingMode.HALF_EVEN).doubleValue();
+ position.set(Position.KEY_TOTAL_DISTANCE, totalDistance);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/EngineHoursHandler.java b/src/main/java/org/traccar/handler/EngineHoursHandler.java
new file mode 100644
index 000000000..92da84e6b
--- /dev/null
+++ b/src/main/java/org/traccar/handler/EngineHoursHandler.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.BaseDataHandler;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class EngineHoursHandler extends BaseDataHandler {
+
+ private final IdentityManager identityManager;
+
+ public EngineHoursHandler(IdentityManager identityManager) {
+ this.identityManager = identityManager;
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+ if (!position.getAttributes().containsKey(Position.KEY_HOURS)) {
+ Position last = identityManager.getLastPosition(position.getDeviceId());
+ if (last != null) {
+ long hours = last.getLong(Position.KEY_HOURS);
+ if (last.getBoolean(Position.KEY_IGNITION) && position.getBoolean(Position.KEY_IGNITION)) {
+ hours += position.getFixTime().getTime() - last.getFixTime().getTime();
+ }
+ if (hours != 0) {
+ position.set(Position.KEY_HOURS, hours);
+ }
+ }
+ }
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/FilterHandler.java b/src/main/java/org/traccar/handler/FilterHandler.java
new file mode 100644
index 000000000..dceaede01
--- /dev/null
+++ b/src/main/java/org/traccar/handler/FilterHandler.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseDataHandler;
+import org.traccar.Context;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class FilterHandler extends BaseDataHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FilterHandler.class);
+
+ private boolean filterInvalid;
+ private boolean filterZero;
+ private boolean filterDuplicate;
+ private long filterFuture;
+ private boolean filterApproximate;
+ private int filterAccuracy;
+ private boolean filterStatic;
+ private int filterDistance;
+ private int filterMaxSpeed;
+ private long filterMinPeriod;
+ private long skipLimit;
+ private boolean skipAttributes;
+
+ public FilterHandler(Config config) {
+ filterInvalid = config.getBoolean(Keys.FILTER_INVALID);
+ filterZero = config.getBoolean(Keys.FILTER_ZERO);
+ filterDuplicate = config.getBoolean(Keys.FILTER_DUPLICATE);
+ filterFuture = config.getLong(Keys.FILTER_FUTURE) * 1000;
+ filterAccuracy = config.getInteger(Keys.FILTER_ACCURACY);
+ filterApproximate = config.getBoolean(Keys.FILTER_APPROXIMATE);
+ filterStatic = config.getBoolean(Keys.FILTER_STATIC);
+ filterDistance = config.getInteger(Keys.FILTER_DISTANCE);
+ filterMaxSpeed = config.getInteger(Keys.FILTER_MAX_SPEED);
+ filterMinPeriod = config.getInteger(Keys.FILTER_MIN_PERIOD) * 1000;
+ skipLimit = config.getLong(Keys.FILTER_SKIP_LIMIT) * 1000;
+ skipAttributes = config.getBoolean(Keys.FILTER_SKIP_ATTRIBUTES_ENABLE);
+ }
+
+ private boolean filterInvalid(Position position) {
+ return filterInvalid && (!position.getValid()
+ || position.getLatitude() > 90 || position.getLongitude() > 180
+ || position.getLatitude() < -90 || position.getLongitude() < -180);
+ }
+
+ private boolean filterZero(Position position) {
+ return filterZero && position.getLatitude() == 0.0 && position.getLongitude() == 0.0;
+ }
+
+ private boolean filterDuplicate(Position position, Position last) {
+ if (filterDuplicate && last != null && position.getFixTime().equals(last.getFixTime())) {
+ for (String key : position.getAttributes().keySet()) {
+ if (!last.getAttributes().containsKey(key)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean filterFuture(Position position) {
+ return filterFuture != 0 && position.getFixTime().getTime() > System.currentTimeMillis() + filterFuture;
+ }
+
+ private boolean filterAccuracy(Position position) {
+ return filterAccuracy != 0 && position.getAccuracy() > filterAccuracy;
+ }
+
+ private boolean filterApproximate(Position position) {
+ return filterApproximate && position.getBoolean(Position.KEY_APPROXIMATE);
+ }
+
+ private boolean filterStatic(Position position) {
+ return filterStatic && position.getSpeed() == 0.0;
+ }
+
+ private boolean filterDistance(Position position, Position last) {
+ if (filterDistance != 0 && last != null) {
+ return position.getDouble(Position.KEY_DISTANCE) < filterDistance;
+ }
+ return false;
+ }
+
+ private boolean filterMaxSpeed(Position position, Position last) {
+ if (filterMaxSpeed != 0 && last != null) {
+ double distance = position.getDouble(Position.KEY_DISTANCE);
+ double time = position.getFixTime().getTime() - last.getFixTime().getTime();
+ return UnitsConverter.knotsFromMps(distance / (time / 1000)) > filterMaxSpeed;
+ }
+ return false;
+ }
+
+ private boolean filterMinPeriod(Position position, Position last) {
+ if (filterMinPeriod != 0 && last != null) {
+ long time = position.getFixTime().getTime() - last.getFixTime().getTime();
+ return time > 0 && time < filterMinPeriod;
+ }
+ return false;
+ }
+
+ private boolean skipLimit(Position position, Position last) {
+ if (skipLimit != 0 && last != null) {
+ return (position.getServerTime().getTime() - last.getServerTime().getTime()) > skipLimit;
+ }
+ return false;
+ }
+
+ private boolean skipAttributes(Position position) {
+ if (skipAttributes) {
+ String attributesString = Context.getIdentityManager().lookupAttributeString(
+ position.getDeviceId(), "filter.skipAttributes", "", true);
+ for (String attribute : attributesString.split("[ ,]")) {
+ if (position.getAttributes().containsKey(attribute)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean filter(Position position) {
+
+ StringBuilder filterType = new StringBuilder();
+
+ Position last = null;
+ if (Context.getIdentityManager() != null) {
+ last = Context.getIdentityManager().getLastPosition(position.getDeviceId());
+ }
+
+ if (filterInvalid(position)) {
+ filterType.append("Invalid ");
+ }
+ if (filterZero(position)) {
+ filterType.append("Zero ");
+ }
+ if (filterDuplicate(position, last) && !skipLimit(position, last) && !skipAttributes(position)) {
+ filterType.append("Duplicate ");
+ }
+ if (filterFuture(position)) {
+ filterType.append("Future ");
+ }
+ if (filterAccuracy(position)) {
+ filterType.append("Accuracy ");
+ }
+ if (filterApproximate(position)) {
+ filterType.append("Approximate ");
+ }
+ if (filterStatic(position) && !skipLimit(position, last) && !skipAttributes(position)) {
+ filterType.append("Static ");
+ }
+ if (filterDistance(position, last) && !skipLimit(position, last) && !skipAttributes(position)) {
+ filterType.append("Distance ");
+ }
+ if (filterMaxSpeed(position, last)) {
+ filterType.append("MaxSpeed ");
+ }
+ if (filterMinPeriod(position, last)) {
+ filterType.append("MinPeriod ");
+ }
+
+ if (filterType.length() > 0) {
+
+ StringBuilder message = new StringBuilder();
+ message.append("Position filtered by ");
+ message.append(filterType.toString());
+ message.append("filters from device: ");
+ message.append(Context.getIdentityManager().getById(position.getDeviceId()).getUniqueId());
+
+ LOGGER.info(message.toString());
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+ if (filter(position)) {
+ return null;
+ }
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/GeocoderHandler.java b/src/main/java/org/traccar/handler/GeocoderHandler.java
new file mode 100644
index 000000000..b96f01b3a
--- /dev/null
+++ b/src/main/java/org/traccar/handler/GeocoderHandler.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.database.IdentityManager;
+import org.traccar.database.StatisticsManager;
+import org.traccar.geocoder.Geocoder;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class GeocoderHandler extends ChannelInboundHandlerAdapter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GeocoderHandler.class);
+
+ private final Geocoder geocoder;
+ private final IdentityManager identityManager;
+ private final StatisticsManager statisticsManager;
+ private final boolean ignorePositions;
+ private final boolean processInvalidPositions;
+ private final int geocoderReuseDistance;
+
+ public GeocoderHandler(
+ Config config, Geocoder geocoder, IdentityManager identityManager, StatisticsManager statisticsManager) {
+ this.geocoder = geocoder;
+ this.identityManager = identityManager;
+ this.statisticsManager = statisticsManager;
+ ignorePositions = Context.getConfig().getBoolean(Keys.GEOCODER_IGNORE_POSITIONS);
+ processInvalidPositions = config.getBoolean(Keys.GEOCODER_PROCESS_INVALID_POSITIONS);
+ geocoderReuseDistance = config.getInteger(Keys.GEOCODER_REUSE_DISTANCE, 0);
+ }
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, Object message) {
+ if (message instanceof Position && !ignorePositions) {
+ final Position position = (Position) message;
+ if (processInvalidPositions || position.getValid()) {
+ if (geocoderReuseDistance != 0) {
+ Position lastPosition = identityManager.getLastPosition(position.getDeviceId());
+ if (lastPosition != null && lastPosition.getAddress() != null
+ && position.getDouble(Position.KEY_DISTANCE) <= geocoderReuseDistance) {
+ position.setAddress(lastPosition.getAddress());
+ ctx.fireChannelRead(position);
+ return;
+ }
+ }
+
+ if (statisticsManager != null) {
+ statisticsManager.registerGeocoderRequest();
+ }
+
+ geocoder.getAddress(position.getLatitude(), position.getLongitude(),
+ new Geocoder.ReverseGeocoderCallback() {
+ @Override
+ public void onSuccess(String address) {
+ position.setAddress(address);
+ ctx.fireChannelRead(position);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ LOGGER.warn("Geocoding failed", e);
+ ctx.fireChannelRead(position);
+ }
+ });
+ } else {
+ ctx.fireChannelRead(position);
+ }
+ } else {
+ ctx.fireChannelRead(message);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/GeolocationHandler.java b/src/main/java/org/traccar/handler/GeolocationHandler.java
new file mode 100644
index 000000000..c7b39e491
--- /dev/null
+++ b/src/main/java/org/traccar/handler/GeolocationHandler.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.database.StatisticsManager;
+import org.traccar.geolocation.GeolocationProvider;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class GeolocationHandler extends ChannelInboundHandlerAdapter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GeolocationHandler.class);
+
+ private final GeolocationProvider geolocationProvider;
+ private final StatisticsManager statisticsManager;
+ private final boolean processInvalidPositions;
+
+ public GeolocationHandler(
+ Config config, GeolocationProvider geolocationProvider, StatisticsManager statisticsManager) {
+ this.geolocationProvider = geolocationProvider;
+ this.statisticsManager = statisticsManager;
+ this.processInvalidPositions = config.getBoolean(Keys.GEOLOCATION_PROCESS_INVALID_POSITIONS);
+ }
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, Object message) {
+ if (message instanceof Position) {
+ final Position position = (Position) message;
+ if ((position.getOutdated() || processInvalidPositions && !position.getValid())
+ && position.getNetwork() != null) {
+ if (statisticsManager != null) {
+ statisticsManager.registerGeolocationRequest();
+ }
+
+ geolocationProvider.getLocation(position.getNetwork(),
+ new GeolocationProvider.LocationProviderCallback() {
+ @Override
+ public void onSuccess(double latitude, double longitude, double accuracy) {
+ position.set(Position.KEY_APPROXIMATE, true);
+ position.setValid(true);
+ position.setFixTime(position.getDeviceTime());
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+ position.setAccuracy(accuracy);
+ position.setAltitude(0);
+ position.setSpeed(0);
+ position.setCourse(0);
+ position.set(Position.KEY_RSSI, 0);
+ ctx.fireChannelRead(position);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ LOGGER.warn("Geolocation network error", e);
+ ctx.fireChannelRead(position);
+ }
+ });
+ } else {
+ ctx.fireChannelRead(position);
+ }
+ } else {
+ ctx.fireChannelRead(message);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/HemisphereHandler.java b/src/main/java/org/traccar/handler/HemisphereHandler.java
new file mode 100644
index 000000000..aff3d8a64
--- /dev/null
+++ b/src/main/java/org/traccar/handler/HemisphereHandler.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.BaseDataHandler;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class HemisphereHandler extends BaseDataHandler {
+
+ private int latitudeFactor;
+ private int longitudeFactor;
+
+ public HemisphereHandler(Config config) {
+ String latitudeHemisphere = config.getString(Keys.LOCATION_LATITUDE_HEMISPHERE);
+ if (latitudeHemisphere != null) {
+ if (latitudeHemisphere.equalsIgnoreCase("N")) {
+ latitudeFactor = 1;
+ } else if (latitudeHemisphere.equalsIgnoreCase("S")) {
+ latitudeFactor = -1;
+ }
+ }
+ String longitudeHemisphere = config.getString(Keys.LOCATION_LATITUDE_HEMISPHERE);
+ if (longitudeHemisphere != null) {
+ if (longitudeHemisphere.equalsIgnoreCase("E")) {
+ longitudeFactor = 1;
+ } else if (longitudeHemisphere.equalsIgnoreCase("W")) {
+ longitudeFactor = -1;
+ }
+ }
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+ if (latitudeFactor != 0) {
+ position.setLatitude(Math.abs(position.getLatitude()) * latitudeFactor);
+ }
+ if (longitudeFactor != 0) {
+ position.setLongitude(Math.abs(position.getLongitude()) * longitudeFactor);
+ }
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/MotionHandler.java b/src/main/java/org/traccar/handler/MotionHandler.java
new file mode 100644
index 000000000..e8051dd75
--- /dev/null
+++ b/src/main/java/org/traccar/handler/MotionHandler.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.BaseDataHandler;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class MotionHandler extends BaseDataHandler {
+
+ private double speedThreshold;
+
+ public MotionHandler(double speedThreshold) {
+ this.speedThreshold = speedThreshold;
+ }
+
+ @Override
+ protected Position handlePosition(Position position) {
+ if (!position.getAttributes().containsKey(Position.KEY_MOTION)) {
+ position.set(Position.KEY_MOTION, position.getSpeed() > speedThreshold);
+ }
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/NetworkMessageHandler.java b/src/main/java/org/traccar/handler/NetworkMessageHandler.java
new file mode 100644
index 000000000..b1d926bfa
--- /dev/null
+++ b/src/main/java/org/traccar/handler/NetworkMessageHandler.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.socket.DatagramChannel;
+import io.netty.channel.socket.DatagramPacket;
+import org.traccar.NetworkMessage;
+
+import java.net.InetSocketAddress;
+
+public class NetworkMessageHandler extends ChannelDuplexHandler {
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ if (ctx.channel() instanceof DatagramChannel) {
+ DatagramPacket packet = (DatagramPacket) msg;
+ ctx.fireChannelRead(new NetworkMessage(packet.content(), packet.sender()));
+ } else if (msg instanceof ByteBuf) {
+ ByteBuf buffer = (ByteBuf) msg;
+ ctx.fireChannelRead(new NetworkMessage(buffer, ctx.channel().remoteAddress()));
+ }
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
+ if (msg instanceof NetworkMessage) {
+ NetworkMessage message = (NetworkMessage) msg;
+ if (ctx.channel() instanceof DatagramChannel) {
+ InetSocketAddress recipient = (InetSocketAddress) message.getRemoteAddress();
+ InetSocketAddress sender = (InetSocketAddress) ctx.channel().localAddress();
+ ctx.write(new DatagramPacket((ByteBuf) message.getMessage(), recipient, sender), promise);
+ } else {
+ ctx.write(message.getMessage(), promise);
+ }
+ } else {
+ ctx.write(msg, promise);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/OpenChannelHandler.java b/src/main/java/org/traccar/handler/OpenChannelHandler.java
new file mode 100644
index 000000000..d09d617ab
--- /dev/null
+++ b/src/main/java/org/traccar/handler/OpenChannelHandler.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.TrackerServer;
+
+public class OpenChannelHandler extends ChannelDuplexHandler {
+
+ private final TrackerServer server;
+
+ public OpenChannelHandler(TrackerServer server) {
+ this.server = server;
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+ super.channelActive(ctx);
+ server.getChannelGroup().add(ctx.channel());
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ super.channelInactive(ctx);
+ server.getChannelGroup().remove(ctx.channel());
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/RemoteAddressHandler.java b/src/main/java/org/traccar/handler/RemoteAddressHandler.java
new file mode 100644
index 000000000..c09b8c39a
--- /dev/null
+++ b/src/main/java/org/traccar/handler/RemoteAddressHandler.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import org.traccar.model.Position;
+
+import java.net.InetSocketAddress;
+
+@ChannelHandler.Sharable
+public class RemoteAddressHandler extends ChannelInboundHandlerAdapter {
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+
+ InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress();
+ String hostAddress = remoteAddress != null ? remoteAddress.getAddress().getHostAddress() : null;
+
+ if (msg instanceof Position) {
+ Position position = (Position) msg;
+ position.set(Position.KEY_IP, hostAddress);
+ }
+
+ ctx.fireChannelRead(msg);
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/StandardLoggingHandler.java b/src/main/java/org/traccar/handler/StandardLoggingHandler.java
new file mode 100644
index 000000000..88010458f
--- /dev/null
+++ b/src/main/java/org/traccar/handler/StandardLoggingHandler.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.NetworkMessage;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+public class StandardLoggingHandler extends ChannelDuplexHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(StandardLoggingHandler.class);
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ log(ctx, false, msg);
+ super.channelRead(ctx, msg);
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+ log(ctx, true, msg);
+ super.write(ctx, msg, promise);
+ }
+
+ public void log(ChannelHandlerContext ctx, boolean downstream, Object o) {
+ if (o instanceof NetworkMessage) {
+ NetworkMessage networkMessage = (NetworkMessage) o;
+ if (networkMessage.getMessage() instanceof ByteBuf) {
+ log(ctx, downstream, networkMessage.getRemoteAddress(), (ByteBuf) networkMessage.getMessage());
+ }
+ } else if (o instanceof ByteBuf) {
+ log(ctx, downstream, ctx.channel().remoteAddress(), (ByteBuf) o);
+ }
+ }
+
+ public void log(ChannelHandlerContext ctx, boolean downstream, SocketAddress remoteAddress, ByteBuf buf) {
+ StringBuilder message = new StringBuilder();
+
+ message.append("[").append(ctx.channel().id().asShortText()).append(": ");
+ message.append(((InetSocketAddress) ctx.channel().localAddress()).getPort());
+ if (downstream) {
+ message.append(" > ");
+ } else {
+ message.append(" < ");
+ }
+
+ if (remoteAddress instanceof InetSocketAddress) {
+ message.append(((InetSocketAddress) remoteAddress).getHostString());
+ } else {
+ message.append("unknown");
+ }
+ message.append("]");
+
+ message.append(" HEX: ");
+ message.append(ByteBufUtil.hexDump(buf));
+
+ LOGGER.info(message.toString());
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/AlertEventHandler.java b/src/main/java/org/traccar/handler/events/AlertEventHandler.java
new file mode 100644
index 000000000..0b7c8d23e
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/AlertEventHandler.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.Collections;
+import java.util.Map;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class AlertEventHandler extends BaseEventHandler {
+
+ private final IdentityManager identityManager;
+ private final boolean ignoreDuplicateAlerts;
+
+ public AlertEventHandler(Config config, IdentityManager identityManager) {
+ this.identityManager = identityManager;
+ ignoreDuplicateAlerts = config.getBoolean(Keys.EVENT_IGNORE_DUPLICATE_ALERTS);
+ }
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+ Object alarm = position.getAttributes().get(Position.KEY_ALARM);
+ if (alarm != null) {
+ boolean ignoreAlert = false;
+ if (ignoreDuplicateAlerts) {
+ Position lastPosition = identityManager.getLastPosition(position.getDeviceId());
+ if (lastPosition != null && alarm.equals(lastPosition.getAttributes().get(Position.KEY_ALARM))) {
+ ignoreAlert = true;
+ }
+ }
+ if (!ignoreAlert) {
+ Event event = new Event(Event.TYPE_ALARM, position.getDeviceId(), position.getId());
+ event.set(Position.KEY_ALARM, (String) alarm);
+ return Collections.singletonMap(event, position);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/BaseEventHandler.java b/src/main/java/org/traccar/handler/events/BaseEventHandler.java
new file mode 100644
index 000000000..41f677f6c
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/BaseEventHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.Map;
+
+import org.traccar.BaseDataHandler;
+import org.traccar.Context;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+public abstract class BaseEventHandler extends BaseDataHandler {
+
+ @Override
+ protected Position handlePosition(Position position) {
+ Map<Event, Position> events = analyzePosition(position);
+ if (events != null && Context.getNotificationManager() != null) {
+ Context.getNotificationManager().updateEvents(events);
+ }
+ return position;
+ }
+
+ protected abstract Map<Event, Position> analyzePosition(Position position);
+
+}
diff --git a/src/main/java/org/traccar/handler/events/CommandResultEventHandler.java b/src/main/java/org/traccar/handler/events/CommandResultEventHandler.java
new file mode 100644
index 000000000..cfe676653
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/CommandResultEventHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.Collections;
+import java.util.Map;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class CommandResultEventHandler extends BaseEventHandler {
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+ Object commandResult = position.getAttributes().get(Position.KEY_RESULT);
+ if (commandResult != null) {
+ Event event = new Event(Event.TYPE_COMMAND_RESULT, position.getDeviceId(), position.getId());
+ event.set(Position.KEY_RESULT, (String) commandResult);
+ return Collections.singletonMap(event, position);
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/DriverEventHandler.java b/src/main/java/org/traccar/handler/events/DriverEventHandler.java
new file mode 100644
index 000000000..994df93fa
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/DriverEventHandler.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.Collections;
+import java.util.Map;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class DriverEventHandler extends BaseEventHandler {
+
+ private final IdentityManager identityManager;
+
+ public DriverEventHandler(IdentityManager identityManager) {
+ this.identityManager = identityManager;
+ }
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+ if (!identityManager.isLatestPosition(position)) {
+ return null;
+ }
+ String driverUniqueId = position.getString(Position.KEY_DRIVER_UNIQUE_ID);
+ if (driverUniqueId != null) {
+ String oldDriverUniqueId = null;
+ Position lastPosition = identityManager.getLastPosition(position.getDeviceId());
+ if (lastPosition != null) {
+ oldDriverUniqueId = lastPosition.getString(Position.KEY_DRIVER_UNIQUE_ID);
+ }
+ if (!driverUniqueId.equals(oldDriverUniqueId)) {
+ Event event = new Event(Event.TYPE_DRIVER_CHANGED, position.getDeviceId(), position.getId());
+ event.set(Position.KEY_DRIVER_UNIQUE_ID, driverUniqueId);
+ return Collections.singletonMap(event, position);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/FuelDropEventHandler.java b/src/main/java/org/traccar/handler/events/FuelDropEventHandler.java
new file mode 100644
index 000000000..59de61bba
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/FuelDropEventHandler.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+import java.util.Collections;
+import java.util.Map;
+
+@ChannelHandler.Sharable
+public class FuelDropEventHandler extends BaseEventHandler {
+
+ public static final String ATTRIBUTE_FUEL_DROP_THRESHOLD = "fuelDropThreshold";
+
+ private final IdentityManager identityManager;
+
+ public FuelDropEventHandler(IdentityManager identityManager) {
+ this.identityManager = identityManager;
+ }
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+
+ Device device = identityManager.getById(position.getDeviceId());
+ if (device == null) {
+ return null;
+ }
+ if (!identityManager.isLatestPosition(position)) {
+ return null;
+ }
+
+ double fuelDropThreshold = identityManager
+ .lookupAttributeDouble(device.getId(), ATTRIBUTE_FUEL_DROP_THRESHOLD, 0, false);
+
+ if (fuelDropThreshold > 0) {
+ Position lastPosition = identityManager.getLastPosition(position.getDeviceId());
+ if (position.getAttributes().containsKey(Position.KEY_FUEL_LEVEL)
+ && lastPosition != null && lastPosition.getAttributes().containsKey(Position.KEY_FUEL_LEVEL)) {
+
+ double drop = lastPosition.getDouble(Position.KEY_FUEL_LEVEL)
+ - position.getDouble(Position.KEY_FUEL_LEVEL);
+ if (drop >= fuelDropThreshold) {
+ Event event = new Event(Event.TYPE_DEVICE_FUEL_DROP, position.getDeviceId(), position.getId());
+ event.set(ATTRIBUTE_FUEL_DROP_THRESHOLD, fuelDropThreshold);
+ return Collections.singletonMap(event, position);
+ }
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/GeofenceEventHandler.java b/src/main/java/org/traccar/handler/events/GeofenceEventHandler.java
new file mode 100644
index 000000000..067c97957
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/GeofenceEventHandler.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.database.CalendarManager;
+import org.traccar.database.GeofenceManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Calendar;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class GeofenceEventHandler extends BaseEventHandler {
+
+ private final IdentityManager identityManager;
+ private final GeofenceManager geofenceManager;
+ private final CalendarManager calendarManager;
+
+ public GeofenceEventHandler(
+ IdentityManager identityManager, GeofenceManager geofenceManager, CalendarManager calendarManager) {
+ this.identityManager = identityManager;
+ this.geofenceManager = geofenceManager;
+ this.calendarManager = calendarManager;
+ }
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+ Device device = identityManager.getById(position.getDeviceId());
+ if (device == null) {
+ return null;
+ }
+ if (!identityManager.isLatestPosition(position) || !position.getValid()) {
+ return null;
+ }
+
+ List<Long> currentGeofences = geofenceManager.getCurrentDeviceGeofences(position);
+ List<Long> oldGeofences = new ArrayList<>();
+ if (device.getGeofenceIds() != null) {
+ oldGeofences.addAll(device.getGeofenceIds());
+ }
+ List<Long> newGeofences = new ArrayList<>(currentGeofences);
+ newGeofences.removeAll(oldGeofences);
+ oldGeofences.removeAll(currentGeofences);
+
+ device.setGeofenceIds(currentGeofences);
+
+ Map<Event, Position> events = new HashMap<>();
+ for (long geofenceId : oldGeofences) {
+ long calendarId = geofenceManager.getById(geofenceId).getCalendarId();
+ Calendar calendar = calendarId != 0 ? calendarManager.getById(calendarId) : null;
+ if (calendar == null || calendar.checkMoment(position.getFixTime())) {
+ Event event = new Event(Event.TYPE_GEOFENCE_EXIT, position.getDeviceId(), position.getId());
+ event.setGeofenceId(geofenceId);
+ events.put(event, position);
+ }
+ }
+ for (long geofenceId : newGeofences) {
+ long calendarId = geofenceManager.getById(geofenceId).getCalendarId();
+ Calendar calendar = calendarId != 0 ? calendarManager.getById(calendarId) : null;
+ if (calendar == null || calendar.checkMoment(position.getFixTime())) {
+ Event event = new Event(Event.TYPE_GEOFENCE_ENTER, position.getDeviceId(), position.getId());
+ event.setGeofenceId(geofenceId);
+ events.put(event, position);
+ }
+ }
+ return events;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/IgnitionEventHandler.java b/src/main/java/org/traccar/handler/events/IgnitionEventHandler.java
new file mode 100644
index 000000000..ec133bafc
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/IgnitionEventHandler.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.Collections;
+import java.util.Map;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class IgnitionEventHandler extends BaseEventHandler {
+
+ private final IdentityManager identityManager;
+
+ public IgnitionEventHandler(IdentityManager identityManager) {
+ this.identityManager = identityManager;
+ }
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+ Device device = identityManager.getById(position.getDeviceId());
+ if (device == null || !identityManager.isLatestPosition(position)) {
+ return null;
+ }
+
+ Map<Event, Position> result = null;
+
+ if (position.getAttributes().containsKey(Position.KEY_IGNITION)) {
+ boolean ignition = position.getBoolean(Position.KEY_IGNITION);
+
+ Position lastPosition = identityManager.getLastPosition(position.getDeviceId());
+ if (lastPosition != null && lastPosition.getAttributes().containsKey(Position.KEY_IGNITION)) {
+ boolean oldIgnition = lastPosition.getBoolean(Position.KEY_IGNITION);
+
+ if (ignition && !oldIgnition) {
+ result = Collections.singletonMap(
+ new Event(Event.TYPE_IGNITION_ON, position.getDeviceId(), position.getId()), position);
+ } else if (!ignition && oldIgnition) {
+ result = Collections.singletonMap(
+ new Event(Event.TYPE_IGNITION_OFF, position.getDeviceId(), position.getId()), position);
+ }
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/MaintenanceEventHandler.java b/src/main/java/org/traccar/handler/events/MaintenanceEventHandler.java
new file mode 100644
index 000000000..93ae74142
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/MaintenanceEventHandler.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.database.IdentityManager;
+import org.traccar.database.MaintenancesManager;
+import org.traccar.model.Event;
+import org.traccar.model.Maintenance;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class MaintenanceEventHandler extends BaseEventHandler {
+
+ private final IdentityManager identityManager;
+ private final MaintenancesManager maintenancesManager;
+
+ public MaintenanceEventHandler(IdentityManager identityManager, MaintenancesManager maintenancesManager) {
+ this.identityManager = identityManager;
+ this.maintenancesManager = maintenancesManager;
+ }
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+ if (identityManager.getById(position.getDeviceId()) == null
+ || !identityManager.isLatestPosition(position)) {
+ return null;
+ }
+
+ Position lastPosition = identityManager.getLastPosition(position.getDeviceId());
+ if (lastPosition == null) {
+ return null;
+ }
+
+ Map<Event, Position> events = new HashMap<>();
+ for (long maintenanceId : maintenancesManager.getAllDeviceItems(position.getDeviceId())) {
+ Maintenance maintenance = maintenancesManager.getById(maintenanceId);
+ if (maintenance.getPeriod() != 0) {
+ double oldValue = lastPosition.getDouble(maintenance.getType());
+ double newValue = position.getDouble(maintenance.getType());
+ if (oldValue != 0.0 && newValue != 0.0
+ && (long) ((oldValue - maintenance.getStart()) / maintenance.getPeriod())
+ < (long) ((newValue - maintenance.getStart()) / maintenance.getPeriod())) {
+ Event event = new Event(Event.TYPE_MAINTENANCE, position.getDeviceId(), position.getId());
+ event.setMaintenanceId(maintenanceId);
+ event.set(maintenance.getType(), newValue);
+ events.put(event, position);
+ }
+ }
+ }
+
+ return events;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/MotionEventHandler.java b/src/main/java/org/traccar/handler/events/MotionEventHandler.java
new file mode 100644
index 000000000..9ec02ccfb
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/MotionEventHandler.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.Collections;
+import java.util.Map;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.database.DeviceManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Device;
+import org.traccar.model.DeviceState;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.reports.ReportUtils;
+import org.traccar.reports.model.TripsConfig;
+
+@ChannelHandler.Sharable
+public class MotionEventHandler extends BaseEventHandler {
+
+ private final IdentityManager identityManager;
+ private final DeviceManager deviceManager;
+ private final TripsConfig tripsConfig;
+
+ public MotionEventHandler(IdentityManager identityManager, DeviceManager deviceManager, TripsConfig tripsConfig) {
+ this.identityManager = identityManager;
+ this.deviceManager = deviceManager;
+ this.tripsConfig = tripsConfig;
+ }
+
+ private Map<Event, Position> newEvent(DeviceState deviceState, boolean newMotion) {
+ String eventType = newMotion ? Event.TYPE_DEVICE_MOVING : Event.TYPE_DEVICE_STOPPED;
+ Position position = deviceState.getMotionPosition();
+ Event event = new Event(eventType, position.getDeviceId(), position.getId());
+ deviceState.setMotionState(newMotion);
+ deviceState.setMotionPosition(null);
+ return Collections.singletonMap(event, position);
+ }
+
+ public Map<Event, Position> updateMotionState(DeviceState deviceState) {
+ Map<Event, Position> result = null;
+ if (deviceState.getMotionState() != null && deviceState.getMotionPosition() != null) {
+ boolean newMotion = !deviceState.getMotionState();
+ Position motionPosition = deviceState.getMotionPosition();
+ long currentTime = System.currentTimeMillis();
+ long motionTime = motionPosition.getFixTime().getTime()
+ + (newMotion ? tripsConfig.getMinimalTripDuration() : tripsConfig.getMinimalParkingDuration());
+ if (motionTime <= currentTime) {
+ result = newEvent(deviceState, newMotion);
+ }
+ }
+ return result;
+ }
+
+ public Map<Event, Position> updateMotionState(DeviceState deviceState, Position position) {
+ return updateMotionState(deviceState, position, position.getBoolean(Position.KEY_MOTION));
+ }
+
+ public Map<Event, Position> updateMotionState(DeviceState deviceState, Position position, boolean newMotion) {
+ Map<Event, Position> result = null;
+ Boolean oldMotion = deviceState.getMotionState();
+
+ long currentTime = position.getFixTime().getTime();
+ if (newMotion != oldMotion) {
+ if (deviceState.getMotionPosition() == null) {
+ deviceState.setMotionPosition(position);
+ }
+ } else {
+ deviceState.setMotionPosition(null);
+ }
+
+ Position motionPosition = deviceState.getMotionPosition();
+ if (motionPosition != null) {
+ long motionTime = motionPosition.getFixTime().getTime();
+ double distance = ReportUtils.calculateDistance(motionPosition, position, false);
+ Boolean ignition = null;
+ if (tripsConfig.getUseIgnition()
+ && position.getAttributes().containsKey(Position.KEY_IGNITION)) {
+ ignition = position.getBoolean(Position.KEY_IGNITION);
+ }
+ if (newMotion) {
+ if (motionTime + tripsConfig.getMinimalTripDuration() <= currentTime
+ || distance >= tripsConfig.getMinimalTripDistance()) {
+ result = newEvent(deviceState, newMotion);
+ }
+ } else {
+ if (motionTime + tripsConfig.getMinimalParkingDuration() <= currentTime
+ || ignition != null && !ignition) {
+ result = newEvent(deviceState, newMotion);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+
+ long deviceId = position.getDeviceId();
+ Device device = identityManager.getById(deviceId);
+ if (device == null) {
+ return null;
+ }
+ if (!identityManager.isLatestPosition(position)
+ || !tripsConfig.getProcessInvalidPositions() && !position.getValid()) {
+ return null;
+ }
+
+ Map<Event, Position> result = null;
+ DeviceState deviceState = deviceManager.getDeviceState(deviceId);
+
+ if (deviceState.getMotionState() == null) {
+ deviceState.setMotionState(position.getBoolean(Position.KEY_MOTION));
+ } else {
+ result = updateMotionState(deviceState, position);
+ }
+ deviceManager.setDeviceState(deviceId, deviceState);
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/handler/events/OverspeedEventHandler.java b/src/main/java/org/traccar/handler/events/OverspeedEventHandler.java
new file mode 100644
index 000000000..157bb64e0
--- /dev/null
+++ b/src/main/java/org/traccar/handler/events/OverspeedEventHandler.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.handler.events;
+
+import java.util.Collections;
+import java.util.Map;
+
+import io.netty.channel.ChannelHandler;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.database.DeviceManager;
+import org.traccar.database.GeofenceManager;
+import org.traccar.model.Device;
+import org.traccar.model.DeviceState;
+import org.traccar.model.Event;
+import org.traccar.model.Geofence;
+import org.traccar.model.Position;
+
+@ChannelHandler.Sharable
+public class OverspeedEventHandler extends BaseEventHandler {
+
+ public static final String ATTRIBUTE_SPEED = "speed";
+ public static final String ATTRIBUTE_SPEED_LIMIT = "speedLimit";
+
+ private final DeviceManager deviceManager;
+ private final GeofenceManager geofenceManager;
+
+ private final boolean notRepeat;
+ private final long minimalDuration;
+ private final boolean preferLowest;
+
+ public OverspeedEventHandler(Config config, DeviceManager deviceManager, GeofenceManager geofenceManager) {
+ this.deviceManager = deviceManager;
+ this.geofenceManager = geofenceManager;
+ notRepeat = config.getBoolean(Keys.EVENT_OVERSPEED_NOT_REPEAT);
+ minimalDuration = config.getLong(Keys.EVENT_OVERSPEED_MINIMAL_DURATION) * 1000;
+ preferLowest = config.getBoolean(Keys.EVENT_OVERSPEED_PREFER_LOWEST);
+ }
+
+ private Map<Event, Position> newEvent(DeviceState deviceState, double speedLimit) {
+ Position position = deviceState.getOverspeedPosition();
+ Event event = new Event(Event.TYPE_DEVICE_OVERSPEED, position.getDeviceId(), position.getId());
+ event.set(ATTRIBUTE_SPEED, deviceState.getOverspeedPosition().getSpeed());
+ event.set(ATTRIBUTE_SPEED_LIMIT, speedLimit);
+ event.setGeofenceId(deviceState.getOverspeedGeofenceId());
+ deviceState.setOverspeedState(notRepeat);
+ deviceState.setOverspeedPosition(null);
+ deviceState.setOverspeedGeofenceId(0);
+ return Collections.singletonMap(event, position);
+ }
+
+ public Map<Event, Position> updateOverspeedState(DeviceState deviceState, double speedLimit) {
+ Map<Event, Position> result = null;
+ if (deviceState.getOverspeedState() != null && !deviceState.getOverspeedState()
+ && deviceState.getOverspeedPosition() != null && speedLimit != 0) {
+ long currentTime = System.currentTimeMillis();
+ Position overspeedPosition = deviceState.getOverspeedPosition();
+ long overspeedTime = overspeedPosition.getFixTime().getTime();
+ if (overspeedTime + minimalDuration <= currentTime) {
+ result = newEvent(deviceState, speedLimit);
+ }
+ }
+ return result;
+ }
+
+ public Map<Event, Position> updateOverspeedState(
+ DeviceState deviceState, Position position, double speedLimit, long geofenceId) {
+ Map<Event, Position> result = null;
+
+ Boolean oldOverspeed = deviceState.getOverspeedState();
+
+ long currentTime = position.getFixTime().getTime();
+ boolean newOverspeed = position.getSpeed() > speedLimit;
+ if (newOverspeed && !oldOverspeed) {
+ if (deviceState.getOverspeedPosition() == null) {
+ deviceState.setOverspeedPosition(position);
+ deviceState.setOverspeedGeofenceId(geofenceId);
+ }
+ } else if (oldOverspeed && !newOverspeed) {
+ deviceState.setOverspeedState(false);
+ deviceState.setOverspeedPosition(null);
+ deviceState.setOverspeedGeofenceId(0);
+ } else {
+ deviceState.setOverspeedPosition(null);
+ deviceState.setOverspeedGeofenceId(0);
+ }
+ Position overspeedPosition = deviceState.getOverspeedPosition();
+ if (overspeedPosition != null) {
+ long overspeedTime = overspeedPosition.getFixTime().getTime();
+ if (newOverspeed && overspeedTime + minimalDuration <= currentTime) {
+ result = newEvent(deviceState, speedLimit);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected Map<Event, Position> analyzePosition(Position position) {
+
+ long deviceId = position.getDeviceId();
+ Device device = deviceManager.getById(deviceId);
+ if (device == null) {
+ return null;
+ }
+ if (!deviceManager.isLatestPosition(position) || !position.getValid()) {
+ return null;
+ }
+
+ double speedLimit = deviceManager.lookupAttributeDouble(deviceId, ATTRIBUTE_SPEED_LIMIT, 0, false);
+
+ double geofenceSpeedLimit = 0;
+ long overspeedGeofenceId = 0;
+
+ if (geofenceManager != null && device.getGeofenceIds() != null) {
+ for (long geofenceId : device.getGeofenceIds()) {
+ Geofence geofence = geofenceManager.getById(geofenceId);
+ if (geofence != null) {
+ double currentSpeedLimit = geofence.getDouble(ATTRIBUTE_SPEED_LIMIT);
+ if (currentSpeedLimit > 0 && geofenceSpeedLimit == 0
+ || preferLowest && currentSpeedLimit < geofenceSpeedLimit
+ || !preferLowest && currentSpeedLimit > geofenceSpeedLimit) {
+ geofenceSpeedLimit = currentSpeedLimit;
+ overspeedGeofenceId = geofenceId;
+ }
+ }
+ }
+ }
+ if (geofenceSpeedLimit > 0) {
+ speedLimit = geofenceSpeedLimit;
+ }
+
+ if (speedLimit == 0) {
+ return null;
+ }
+
+ Map<Event, Position> result = null;
+ DeviceState deviceState = deviceManager.getDeviceState(deviceId);
+
+ if (deviceState.getOverspeedState() == null) {
+ deviceState.setOverspeedState(position.getSpeed() > speedLimit);
+ deviceState.setOverspeedGeofenceId(position.getSpeed() > speedLimit ? overspeedGeofenceId : 0);
+ } else {
+ result = updateOverspeedState(deviceState, position, speedLimit, overspeedGeofenceId);
+ }
+
+ deviceManager.setDeviceState(deviceId, deviceState);
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/BcdUtil.java b/src/main/java/org/traccar/helper/BcdUtil.java
new file mode 100644
index 000000000..c87529e32
--- /dev/null
+++ b/src/main/java/org/traccar/helper/BcdUtil.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import io.netty.buffer.ByteBuf;
+
+public final class BcdUtil {
+
+ private BcdUtil() {
+ }
+
+ public static int readInteger(ByteBuf buf, int digits) {
+ int result = 0;
+
+ for (int i = 0; i < digits / 2; i++) {
+ int b = buf.readUnsignedByte();
+ result *= 10;
+ result += b >>> 4;
+ result *= 10;
+ result += b & 0x0f;
+ }
+
+ if (digits % 2 != 0) {
+ int b = buf.getUnsignedByte(buf.readerIndex());
+ result *= 10;
+ result += b >>> 4;
+ }
+
+ return result;
+ }
+
+ public static double readCoordinate(ByteBuf buf) {
+ int b1 = buf.readUnsignedByte();
+ int b2 = buf.readUnsignedByte();
+ int b3 = buf.readUnsignedByte();
+ int b4 = buf.readUnsignedByte();
+
+ double value = (b2 & 0xf) * 10 + (b3 >> 4);
+ value += (((b3 & 0xf) * 10 + (b4 >> 4)) * 10 + (b4 & 0xf)) / 1000.0;
+ value /= 60;
+ value += ((b1 >> 4 & 0x7) * 10 + (b1 & 0xf)) * 10 + (b2 >> 4);
+
+ if ((b1 & 0x80) != 0) {
+ value = -value;
+ }
+
+ return value;
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/BitBuffer.java b/src/main/java/org/traccar/helper/BitBuffer.java
new file mode 100644
index 000000000..f30a4557b
--- /dev/null
+++ b/src/main/java/org/traccar/helper/BitBuffer.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+public class BitBuffer {
+
+ private final ByteBuf buffer;
+
+ private int writeByte;
+ private int writeCount;
+
+ private int readByte;
+ private int readCount;
+
+ public BitBuffer() {
+ buffer = Unpooled.buffer();
+ }
+
+ public BitBuffer(ByteBuf buffer) {
+ this.buffer = buffer;
+ }
+
+ public void writeEncoded(byte[] bytes) {
+ for (byte b : bytes) {
+ b -= 48;
+ if (b > 40) {
+ b -= 8;
+ }
+ write(b);
+ }
+ }
+
+ public void write(int b) {
+ if (writeCount == 0) {
+ writeByte |= b;
+ writeCount = 6;
+ } else {
+ int remaining = 8 - writeCount;
+ writeByte <<= remaining;
+ writeByte |= b >> (6 - remaining);
+ buffer.writeByte(writeByte);
+ writeByte = b & ((1 << (6 - remaining)) - 1);
+ writeCount = 6 - remaining;
+ }
+ }
+
+ public int readUnsigned(int length) {
+ int result = 0;
+
+ while (length > 0) {
+ if (readCount == 0) {
+ readByte = buffer.readUnsignedByte();
+ readCount = 8;
+ }
+ if (readCount >= length) {
+ result <<= length;
+ result |= readByte >> (readCount - length);
+ readByte &= (1 << (readCount - length)) - 1;
+ readCount -= length;
+ length = 0;
+ } else {
+ result <<= readCount;
+ result |= readByte;
+ length -= readCount;
+ readByte = 0;
+ readCount = 0;
+ }
+ }
+
+ return result;
+ }
+
+ public int readSigned(int length) {
+ int result = readUnsigned(length);
+ int signBit = 1 << (length - 1);
+ if ((result & signBit) == 0) {
+ return result;
+ } else {
+ result &= signBit - 1;
+ result += ~(signBit - 1);
+ return result;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/BitUtil.java b/src/main/java/org/traccar/helper/BitUtil.java
new file mode 100644
index 000000000..b6108edff
--- /dev/null
+++ b/src/main/java/org/traccar/helper/BitUtil.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+public final class BitUtil {
+
+ private BitUtil() {
+ }
+
+ public static boolean check(long number, int index) {
+ return (number & (1 << index)) != 0;
+ }
+
+ public static int between(int number, int from, int to) {
+ return (number >> from) & ((1 << to - from) - 1);
+ }
+
+ public static int from(int number, int from) {
+ return number >> from;
+ }
+
+ public static int to(int number, int to) {
+ return between(number, 0, to);
+ }
+
+ public static long between(long number, int from, int to) {
+ return (number >> from) & ((1L << to - from) - 1L);
+ }
+
+ public static long from(long number, int from) {
+ return number >> from;
+ }
+
+ public static long to(long number, int to) {
+ return between(number, 0, to);
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/BufferUtil.java b/src/main/java/org/traccar/helper/BufferUtil.java
new file mode 100644
index 000000000..15c619ec5
--- /dev/null
+++ b/src/main/java/org/traccar/helper/BufferUtil.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import java.nio.charset.StandardCharsets;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+
+public final class BufferUtil {
+
+ private BufferUtil() {
+ }
+
+ public static int indexOf(String needle, ByteBuf haystack) {
+ ByteBuf needleBuffer = Unpooled.wrappedBuffer(needle.getBytes(StandardCharsets.US_ASCII));
+ try {
+ return ByteBufUtil.indexOf(needleBuffer, haystack);
+ } finally {
+ needleBuffer.release();
+ }
+ }
+
+ public static int indexOf(String needle, ByteBuf haystack, int startIndex, int endIndex) {
+ ByteBuf wrappedHaystack = Unpooled.wrappedBuffer(haystack);
+ wrappedHaystack.readerIndex(startIndex - haystack.readerIndex());
+ wrappedHaystack.writerIndex(endIndex - haystack.readerIndex());
+ int result = indexOf(needle, wrappedHaystack);
+ return result < 0 ? result : haystack.readerIndex() + result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/Checksum.java b/src/main/java/org/traccar/helper/Checksum.java
new file mode 100644
index 000000000..adfa697c5
--- /dev/null
+++ b/src/main/java/org/traccar/helper/Checksum.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.CRC32;
+
+public final class Checksum {
+
+ private Checksum() {
+ }
+
+ public static class Algorithm {
+
+ private int poly;
+ private int init;
+ private boolean refIn;
+ private boolean refOut;
+ private int xorOut;
+ private int[] table;
+
+ public Algorithm(int bits, int poly, int init, boolean refIn, boolean refOut, int xorOut) {
+ this.poly = poly;
+ this.init = init;
+ this.refIn = refIn;
+ this.refOut = refOut;
+ this.xorOut = xorOut;
+ this.table = bits == 8 ? initTable8() : initTable16();
+ }
+
+ private int[] initTable8() {
+ int[] table = new int[256];
+ int crc;
+ for (int i = 0; i < 256; i++) {
+ crc = i;
+ for (int j = 0; j < 8; j++) {
+ boolean bit = (crc & 0x80) != 0;
+ crc <<= 1;
+ if (bit) {
+ crc ^= poly;
+ }
+ }
+ table[i] = crc & 0xFF;
+ }
+ return table;
+ }
+
+ private int[] initTable16() {
+ int[] table = new int[256];
+ int crc;
+ for (int i = 0; i < 256; i++) {
+ crc = i << 8;
+ for (int j = 0; j < 8; j++) {
+ boolean bit = (crc & 0x8000) != 0;
+ crc <<= 1;
+ if (bit) {
+ crc ^= poly;
+ }
+ }
+ table[i] = crc & 0xFFFF;
+ }
+ return table;
+ }
+
+ }
+
+ private static int reverse(int value, int bits) {
+ int result = 0;
+ for (int i = 0; i < bits; i++) {
+ result = (result << 1) | (value & 1);
+ value >>= 1;
+ }
+ return result;
+ }
+
+ public static int crc8(Algorithm algorithm, ByteBuffer buf) {
+ int crc = algorithm.init;
+ while (buf.hasRemaining()) {
+ int b = buf.get() & 0xFF;
+ if (algorithm.refIn) {
+ b = reverse(b, 8);
+ }
+ crc = algorithm.table[(crc & 0xFF) ^ b];
+ }
+ if (algorithm.refOut) {
+ crc = reverse(crc, 8);
+ }
+ return (crc ^ algorithm.xorOut) & 0xFF;
+ }
+
+ public static int crc16(Algorithm algorithm, ByteBuffer buf) {
+ int crc = algorithm.init;
+ while (buf.hasRemaining()) {
+ int b = buf.get() & 0xFF;
+ if (algorithm.refIn) {
+ b = reverse(b, 8);
+ }
+ crc = (crc << 8) ^ algorithm.table[((crc >> 8) & 0xFF) ^ b];
+ }
+ if (algorithm.refOut) {
+ crc = reverse(crc, 16);
+ }
+ return (crc ^ algorithm.xorOut) & 0xFFFF;
+ }
+
+ public static final Algorithm CRC8_EGTS = new Algorithm(8, 0x31, 0xFF, false, false, 0x00);
+ public static final Algorithm CRC8_ROHC = new Algorithm(8, 0x07, 0xFF, true, true, 0x00);
+
+ public static final Algorithm CRC16_IBM = new Algorithm(16, 0x8005, 0x0000, true, true, 0x0000);
+ public static final Algorithm CRC16_X25 = new Algorithm(16, 0x1021, 0xFFFF, true, true, 0xFFFF);
+ public static final Algorithm CRC16_MODBUS = new Algorithm(16, 0x8005, 0xFFFF, true, true, 0x0000);
+ public static final Algorithm CRC16_CCITT_FALSE = new Algorithm(16, 0x1021, 0xFFFF, false, false, 0x0000);
+ public static final Algorithm CRC16_KERMIT = new Algorithm(16, 0x1021, 0x0000, true, true, 0x0000);
+ public static final Algorithm CRC16_XMODEM = new Algorithm(16, 0x1021, 0x0000, false, false, 0x0000);
+
+ public static int crc32(ByteBuffer buf) {
+ CRC32 checksum = new CRC32();
+ while (buf.hasRemaining()) {
+ checksum.update(buf.get());
+ }
+ return (int) checksum.getValue();
+ }
+
+ public static int xor(ByteBuffer buf) {
+ int checksum = 0;
+ while (buf.hasRemaining()) {
+ checksum ^= buf.get();
+ }
+ return checksum;
+ }
+
+ public static int xor(String string) {
+ byte checksum = 0;
+ for (byte b : string.getBytes(StandardCharsets.US_ASCII)) {
+ checksum ^= b;
+ }
+ return checksum;
+ }
+
+ public static String nmea(String msg) {
+ int checksum = 0;
+ byte[] bytes = msg.getBytes(StandardCharsets.US_ASCII);
+ for (int i = 1; i < bytes.length; i++) {
+ checksum ^= bytes[i];
+ }
+ return String.format("*%02x", checksum).toUpperCase();
+ }
+
+ public static int sum(ByteBuffer buf) {
+ byte checksum = 0;
+ while (buf.hasRemaining()) {
+ checksum += buf.get();
+ }
+ return checksum;
+ }
+
+ public static String sum(String msg) {
+ byte checksum = 0;
+ for (byte b : msg.getBytes(StandardCharsets.US_ASCII)) {
+ checksum += b;
+ }
+ return String.format("%02X", checksum).toUpperCase();
+ }
+
+ public static long luhn(long imei) {
+ long checksum = 0;
+ long remain = imei;
+
+ for (int i = 0; remain != 0; i++) {
+ long digit = remain % 10;
+
+ if (i % 2 == 0) {
+ digit *= 2;
+ if (digit >= 10) {
+ digit = 1 + (digit % 10);
+ }
+ }
+
+ checksum += digit;
+ remain /= 10;
+ }
+
+ return (10 - (checksum % 10)) % 10;
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/DataConverter.java b/src/main/java/org/traccar/helper/DataConverter.java
new file mode 100644
index 000000000..7abd4ae93
--- /dev/null
+++ b/src/main/java/org/traccar/helper/DataConverter.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.Hex;
+
+public final class DataConverter {
+
+ private DataConverter() {
+ }
+
+ public static byte[] parseHex(String string) {
+ try {
+ return Hex.decodeHex(string);
+ } catch (DecoderException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String printHex(byte[] data) {
+ return Hex.encodeHexString(data);
+ }
+
+ public static byte[] parseBase64(String string) {
+ return Base64.decodeBase64(string);
+ }
+
+ public static String printBase64(byte[] data) {
+ return Base64.encodeBase64String(data);
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/DateBuilder.java b/src/main/java/org/traccar/helper/DateBuilder.java
new file mode 100644
index 000000000..6e1b779f0
--- /dev/null
+++ b/src/main/java/org/traccar/helper/DateBuilder.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+public class DateBuilder {
+
+ private Calendar calendar;
+
+ public DateBuilder() {
+ this(TimeZone.getTimeZone("UTC"));
+ }
+
+ public DateBuilder(Date time) {
+ this(time, TimeZone.getTimeZone("UTC"));
+ }
+
+ public DateBuilder(TimeZone timeZone) {
+ this(new Date(0), timeZone);
+ }
+
+ public DateBuilder(Date time, TimeZone timeZone) {
+ calendar = Calendar.getInstance(timeZone);
+ calendar.clear();
+ calendar.setTimeInMillis(time.getTime());
+ }
+
+ public DateBuilder setYear(int year) {
+ if (year < 100) {
+ year += 2000;
+ }
+ calendar.set(Calendar.YEAR, year);
+ return this;
+ }
+
+ public DateBuilder setMonth(int month) {
+ calendar.set(Calendar.MONTH, month - 1);
+ return this;
+ }
+
+ public DateBuilder setDay(int day) {
+ calendar.set(Calendar.DAY_OF_MONTH, day);
+ return this;
+ }
+
+ public DateBuilder setDate(int year, int month, int day) {
+ return setYear(year).setMonth(month).setDay(day);
+ }
+
+ public DateBuilder setDateReverse(int day, int month, int year) {
+ return setDate(year, month, day);
+ }
+
+ public DateBuilder setCurrentDate() {
+ Calendar now = Calendar.getInstance(calendar.getTimeZone());
+ return setYear(now.get(Calendar.YEAR)).setMonth(now.get(Calendar.MONTH)).setDay(now.get(Calendar.DAY_OF_MONTH));
+ }
+
+ public DateBuilder setHour(int hour) {
+ calendar.set(Calendar.HOUR_OF_DAY, hour);
+ return this;
+ }
+
+ public DateBuilder setMinute(int minute) {
+ calendar.set(Calendar.MINUTE, minute);
+ return this;
+ }
+
+ public DateBuilder addMinute(int minute) {
+ calendar.add(Calendar.MINUTE, minute);
+ return this;
+ }
+
+ public DateBuilder setSecond(int second) {
+ calendar.set(Calendar.SECOND, second);
+ return this;
+ }
+
+ public DateBuilder addSeconds(long seconds) {
+ calendar.setTimeInMillis(calendar.getTimeInMillis() + seconds * 1000);
+ return this;
+ }
+
+ public DateBuilder setMillis(int millis) {
+ calendar.set(Calendar.MILLISECOND, millis);
+ return this;
+ }
+
+ public DateBuilder addMillis(long millis) {
+ calendar.setTimeInMillis(calendar.getTimeInMillis() + millis);
+ return this;
+ }
+
+ public DateBuilder setTime(int hour, int minute, int second) {
+ return setHour(hour).setMinute(minute).setSecond(second);
+ }
+
+ public DateBuilder setTimeReverse(int second, int minute, int hour) {
+ return setHour(hour).setMinute(minute).setSecond(second);
+ }
+
+ public DateBuilder setTime(int hour, int minute, int second, int millis) {
+ return setHour(hour).setMinute(minute).setSecond(second).setMillis(millis);
+ }
+
+ public Date getDate() {
+ return calendar.getTime();
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/DateUtil.java b/src/main/java/org/traccar/helper/DateUtil.java
new file mode 100644
index 000000000..20a483e3c
--- /dev/null
+++ b/src/main/java/org/traccar/helper/DateUtil.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Calendar;
+import java.util.Date;
+
+public final class DateUtil {
+
+ private DateUtil() {
+ }
+
+ public static Date correctDay(Date guess) {
+ return correctDate(new Date(), guess, Calendar.DAY_OF_MONTH);
+ }
+
+ public static Date correctYear(Date guess) {
+ return correctDate(new Date(), guess, Calendar.YEAR);
+ }
+
+ public static Date correctDate(Date now, Date guess, int field) {
+
+ if (guess.getTime() > now.getTime()) {
+ Date previous = dateAdd(guess, field, -1);
+ if (now.getTime() - previous.getTime() < guess.getTime() - now.getTime()) {
+ return previous;
+ }
+ } else if (guess.getTime() < now.getTime()) {
+ Date next = dateAdd(guess, field, 1);
+ if (next.getTime() - now.getTime() < now.getTime() - guess.getTime()) {
+ return next;
+ }
+ }
+
+ return guess;
+ }
+
+ private static Date dateAdd(Date guess, int field, int amount) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(guess);
+ calendar.add(field, amount);
+ return calendar.getTime();
+ }
+
+ public static Date parseDate(String value) {
+ return Date.from(Instant.from(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(value)));
+ }
+
+ public static String formatDate(Date date) {
+ return formatDate(date, true);
+ }
+
+ public static String formatDate(Date date, boolean zoned) {
+ if (zoned) {
+ return DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()).format(date.toInstant());
+ } else {
+ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/DistanceCalculator.java b/src/main/java/org/traccar/helper/DistanceCalculator.java
new file mode 100644
index 000000000..88d4ef8a4
--- /dev/null
+++ b/src/main/java/org/traccar/helper/DistanceCalculator.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2014 - 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+public final class DistanceCalculator {
+
+ private DistanceCalculator() {
+ }
+
+ private static final double EQUATORIAL_EARTH_RADIUS = 6378.1370;
+ private static final double DEG_TO_RAD = Math.PI / 180;
+
+ public static double distance(double lat1, double lon1, double lat2, double lon2) {
+ double dlong = (lon2 - lon1) * DEG_TO_RAD;
+ double dlat = (lat2 - lat1) * DEG_TO_RAD;
+ double a = Math.pow(Math.sin(dlat / 2), 2)
+ + Math.cos(lat1 * DEG_TO_RAD) * Math.cos(lat2 * DEG_TO_RAD) * Math.pow(Math.sin(dlong / 2), 2);
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ double d = EQUATORIAL_EARTH_RADIUS * c;
+ return d * 1000;
+ }
+
+ public static double distanceToLine(
+ double pointLat, double pointLon, double lat1, double lon1, double lat2, double lon2) {
+ double d0 = distance(pointLat, pointLon, lat1, lon1);
+ double d1 = distance(lat1, lon1, lat2, lon2);
+ double d2 = distance(lat2, lon2, pointLat, pointLon);
+ if (Math.pow(d0, 2) > Math.pow(d1, 2) + Math.pow(d2, 2)) {
+ return d2;
+ }
+ if (Math.pow(d2, 2) > Math.pow(d1, 2) + Math.pow(d0, 2)) {
+ return d0;
+ }
+ double halfP = (d0 + d1 + d2) * 0.5;
+ double area = Math.sqrt(halfP * (halfP - d0) * (halfP - d1) * (halfP - d2));
+ return 2 * area / d1;
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/Hashing.java b/src/main/java/org/traccar/helper/Hashing.java
new file mode 100644
index 000000000..e91310eda
--- /dev/null
+++ b/src/main/java/org/traccar/helper/Hashing.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+
+public final class Hashing {
+
+ public static final int ITERATIONS = 1000;
+ public static final int SALT_SIZE = 24;
+ public static final int HASH_SIZE = 24;
+
+ private static SecretKeyFactory factory;
+ static {
+ try {
+ factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static class HashingResult {
+
+ private final String hash;
+ private final String salt;
+
+ public HashingResult(String hash, String salt) {
+ this.hash = hash;
+ this.salt = salt;
+ }
+
+ public String getHash() {
+ return hash;
+ }
+
+ public String getSalt() {
+ return salt;
+ }
+ }
+
+ private Hashing() {
+ }
+
+ private static byte[] function(char[] password, byte[] salt) {
+ try {
+ PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, HASH_SIZE * Byte.SIZE);
+ return factory.generateSecret(spec).getEncoded();
+ } catch (InvalidKeySpecException e) {
+ throw new SecurityException(e);
+ }
+ }
+
+ private static final SecureRandom RANDOM = new SecureRandom();
+
+ public static HashingResult createHash(String password) {
+ byte[] salt = new byte[SALT_SIZE];
+ RANDOM.nextBytes(salt);
+ byte[] hash = function(password.toCharArray(), salt);
+ return new HashingResult(
+ DataConverter.printHex(hash),
+ DataConverter.printHex(salt));
+ }
+
+ public static boolean validatePassword(String password, String hashHex, String saltHex) {
+ byte[] hash = DataConverter.parseHex(hashHex);
+ byte[] salt = DataConverter.parseHex(saltHex);
+ return slowEquals(hash, function(password.toCharArray(), salt));
+ }
+
+ /**
+ * Compares two byte arrays in length-constant time. This comparison method
+ * is used so that password hashes cannot be extracted from an on-line
+ * system using a timing attack and then attacked off-line.
+ */
+ private static boolean slowEquals(byte[] a, byte[] b) {
+ int diff = a.length ^ b.length;
+ for (int i = 0; i < a.length && i < b.length; i++) {
+ diff |= a[i] ^ b[i];
+ }
+ return diff == 0;
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/LocationTree.java b/src/main/java/org/traccar/helper/LocationTree.java
new file mode 100644
index 000000000..3aff3ce33
--- /dev/null
+++ b/src/main/java/org/traccar/helper/LocationTree.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class LocationTree {
+
+ public static class Item {
+
+ private Item left, right;
+ private float x, y;
+ private String data;
+
+ public Item(float x, float y) {
+ this(x, y, null);
+ }
+
+ public Item(float x, float y, String data) {
+ this.x = x;
+ this.y = y;
+ this.data = data;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ private float squaredDistance(Item item) {
+ return (x - item.x) * (x - item.x) + (y - item.y) * (y - item.y);
+ }
+
+ private float axisSquaredDistance(Item item, int axis) {
+ if (axis == 0) {
+ return (x - item.x) * (x - item.x);
+ } else {
+ return (y - item.y) * (y - item.y);
+ }
+ }
+
+ }
+
+ private Item root;
+
+ private ArrayList<Comparator<Item>> comparators = new ArrayList<>();
+
+ public LocationTree(List<Item> items) {
+ comparators.add(new Comparator<Item>() {
+ @Override
+ public int compare(Item o1, Item o2) {
+ return Float.compare(o1.x, o2.x);
+ }
+ });
+ comparators.add(new Comparator<Item>() {
+ @Override
+ public int compare(Item o1, Item o2) {
+ return Float.compare(o1.y, o2.y);
+ }
+ });
+ root = createTree(items, 0);
+ }
+
+ private Item createTree(List<Item> items, int depth) {
+ if (items.isEmpty()) {
+ return null;
+ }
+ Collections.sort(items, comparators.get(depth % 2));
+ int currentIndex = items.size() / 2;
+ Item median = items.get(currentIndex);
+ median.left = createTree(new ArrayList<>(items.subList(0, currentIndex)), depth + 1);
+ median.right = createTree(new ArrayList<>(items.subList(currentIndex + 1, items.size())), depth + 1);
+ return median;
+ }
+
+ public Item findNearest(Item search) {
+ return findNearest(root, search, 0);
+ }
+
+ private Item findNearest(Item current, Item search, int depth) {
+ int direction = comparators.get(depth % 2).compare(search, current);
+
+ Item next, other;
+ if (direction < 0) {
+ next = current.left;
+ other = current.right;
+ } else {
+ next = current.right;
+ other = current.left;
+ }
+
+ Item best = current;
+ if (next != null) {
+ best = findNearest(next, search, depth + 1);
+ }
+
+ if (current.squaredDistance(search) < best.squaredDistance(search)) {
+ best = current;
+ }
+ if (other != null && current.axisSquaredDistance(search, depth % 2) < best.squaredDistance(search)) {
+ Item possibleBest = findNearest(other, search, depth + 1);
+ if (possibleBest.squaredDistance(search) < best.squaredDistance(search)) {
+ best = possibleBest;
+ }
+ }
+
+ return best;
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/Log.java b/src/main/java/org/traccar/helper/Log.java
new file mode 100644
index 000000000..f328e8ce9
--- /dev/null
+++ b/src/main/java/org/traccar/helper/Log.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import org.traccar.config.Config;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+public final class Log {
+
+ private Log() {
+ }
+
+ private static final String STACK_PACKAGE = "org.traccar";
+ private static final int STACK_LIMIT = 3;
+
+ private static class RollingFileHandler extends Handler {
+
+ private String name;
+ private String suffix;
+ private Writer writer;
+ private boolean rotate;
+
+ RollingFileHandler(String name, boolean rotate) {
+ this.name = name;
+ this.rotate = rotate;
+ }
+
+ @Override
+ public synchronized void publish(LogRecord record) {
+ if (isLoggable(record)) {
+ try {
+ String suffix = "";
+ if (rotate) {
+ suffix = new SimpleDateFormat("yyyyMMdd").format(new Date(record.getMillis()));
+ if (writer != null && !suffix.equals(this.suffix)) {
+ writer.close();
+ writer = null;
+ if (!new File(name).renameTo(new File(name + "." + this.suffix))) {
+ throw new RuntimeException("Log file renaming failed");
+ }
+ }
+ }
+ if (writer == null) {
+ this.suffix = suffix;
+ writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(name, true), StandardCharsets.UTF_8));
+ }
+ writer.write(getFormatter().format(record));
+ writer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Override
+ public synchronized void flush() {
+ if (writer != null) {
+ try {
+ writer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Override
+ public synchronized void close() throws SecurityException {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ }
+
+ public static class LogFormatter extends Formatter {
+
+ private boolean fullStackTraces;
+
+ LogFormatter(boolean fullStackTraces) {
+ this.fullStackTraces = fullStackTraces;
+ }
+
+ private static String formatLevel(Level level) {
+ switch (level.getName()) {
+ case "FINEST":
+ return "TRACE";
+ case "FINER":
+ case "FINE":
+ case "CONFIG":
+ return "DEBUG";
+ case "INFO":
+ return "INFO";
+ case "WARNING":
+ return "WARN";
+ case "SEVERE":
+ default:
+ return "ERROR";
+ }
+ }
+
+ @Override
+ public String format(LogRecord record) {
+ StringBuilder message = new StringBuilder();
+
+ if (record.getMessage() != null) {
+ message.append(record.getMessage());
+ }
+
+ if (record.getThrown() != null) {
+ if (message.length() > 0) {
+ message.append(" - ");
+ }
+ if (fullStackTraces) {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+ record.getThrown().printStackTrace(printWriter);
+ message.append(System.lineSeparator()).append(stringWriter.toString());
+ } else {
+ message.append(exceptionStack(record.getThrown()));
+ }
+ }
+
+ return String.format("%1$tF %1$tT %2$5s: %3$s%n",
+ new Date(record.getMillis()), formatLevel(record.getLevel()), message.toString());
+ }
+
+ }
+
+ public static void setupDefaultLogger() {
+ String path = null;
+ URL url = ClassLoader.getSystemClassLoader().getResource(".");
+ if (url != null) {
+ File jarPath = new File(url.getPath());
+ File logsPath = new File(jarPath, "logs");
+ if (!logsPath.exists() || !logsPath.isDirectory()) {
+ logsPath = jarPath;
+ }
+ path = new File(logsPath, "tracker-server.log").getPath();
+ }
+ setupLogger(path == null, path, Level.WARNING.getName(), false, true);
+ }
+
+ public static void setupLogger(Config config) {
+ setupLogger(
+ config.getBoolean("logger.console"),
+ config.getString("logger.file"),
+ config.getString("logger.level"),
+ config.getBoolean("logger.fullStackTraces"),
+ config.getBoolean("logger.rotate"));
+ }
+
+ private static void setupLogger(
+ boolean console, String file, String levelString, boolean fullStackTraces, boolean rotate) {
+
+ Logger rootLogger = Logger.getLogger("");
+ for (Handler handler : rootLogger.getHandlers()) {
+ rootLogger.removeHandler(handler);
+ }
+
+ Handler handler;
+ if (console) {
+ handler = new ConsoleHandler();
+ } else {
+ handler = new RollingFileHandler(file, rotate);
+ }
+
+ handler.setFormatter(new LogFormatter(fullStackTraces));
+
+ Level level = Level.parse(levelString.toUpperCase());
+ rootLogger.setLevel(level);
+ handler.setLevel(level);
+ handler.setFilter(record -> record != null && !record.getLoggerName().startsWith("sun"));
+
+ rootLogger.addHandler(handler);
+ }
+
+ public static String exceptionStack(Throwable exception) {
+ StringBuilder s = new StringBuilder();
+ String exceptionMsg = exception.getMessage();
+ if (exceptionMsg != null) {
+ s.append(exceptionMsg);
+ s.append(" - ");
+ }
+ s.append(exception.getClass().getSimpleName());
+ StackTraceElement[] stack = exception.getStackTrace();
+
+ if (stack.length > 0) {
+ int count = STACK_LIMIT;
+ boolean first = true;
+ boolean skip = false;
+ String file = "";
+ s.append(" (");
+ for (StackTraceElement element : stack) {
+ if (count > 0 && element.getClassName().startsWith(STACK_PACKAGE)) {
+ if (!first) {
+ s.append(" < ");
+ } else {
+ first = false;
+ }
+
+ if (skip) {
+ s.append("... < ");
+ skip = false;
+ }
+
+ if (file.equals(element.getFileName())) {
+ s.append("*");
+ } else {
+ file = element.getFileName();
+ s.append(file, 0, file.length() - 5); // remove ".java"
+ count -= 1;
+ }
+ s.append(":").append(element.getLineNumber());
+ } else {
+ skip = true;
+ }
+ }
+ if (skip) {
+ if (!first) {
+ s.append(" < ");
+ }
+ s.append("...");
+ }
+ s.append(")");
+ }
+ return s.toString();
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/LogAction.java b/src/main/java/org/traccar/helper/LogAction.java
new file mode 100644
index 000000000..db13337b8
--- /dev/null
+++ b/src/main/java/org/traccar/helper/LogAction.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import java.beans.Introspector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.model.BaseModel;
+
+public final class LogAction {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(LogAction.class);
+
+ private LogAction() {
+ }
+
+ private static final String ACTION_CREATE = "create";
+ private static final String ACTION_EDIT = "edit";
+ private static final String ACTION_REMOVE = "remove";
+
+ private static final String ACTION_LINK = "link";
+ private static final String ACTION_UNLINK = "unlink";
+
+ private static final String ACTION_LOGIN = "login";
+ private static final String ACTION_LOGOUT = "logout";
+
+ private static final String ACTION_DEVICE_ACCUMULATORS = "resetDeviceAccumulators";
+
+ private static final String PATTERN_OBJECT = "user: %d, action: %s, object: %s, id: %d";
+ private static final String PATTERN_LINK = "user: %d, action: %s, owner: %s, id: %d, property: %s, id: %d";
+ private static final String PATTERN_LOGIN = "user: %d, action: %s";
+ private static final String PATTERN_DEVICE_ACCUMULATORS = "user: %d, action: %s, deviceId: %d";
+
+ public static void create(long userId, BaseModel object) {
+ logObjectAction(ACTION_CREATE, userId, object.getClass(), object.getId());
+ }
+
+ public static void edit(long userId, BaseModel object) {
+ logObjectAction(ACTION_EDIT, userId, object.getClass(), object.getId());
+ }
+
+ public static void remove(long userId, Class<?> clazz, long objectId) {
+ logObjectAction(ACTION_REMOVE, userId, clazz, objectId);
+ }
+
+ public static void link(long userId, Class<?> owner, long ownerId, Class<?> property, long propertyId) {
+ logLinkAction(ACTION_LINK, userId, owner, ownerId, property, propertyId);
+ }
+
+ public static void unlink(long userId, Class<?> owner, long ownerId, Class<?> property, long propertyId) {
+ logLinkAction(ACTION_UNLINK, userId, owner, ownerId, property, propertyId);
+ }
+
+ public static void login(long userId) {
+ logLoginAction(ACTION_LOGIN, userId);
+ }
+
+ public static void logout(long userId) {
+ logLoginAction(ACTION_LOGOUT, userId);
+ }
+
+ public static void resetDeviceAccumulators(long userId, long deviceId) {
+ LOGGER.info(String.format(
+ PATTERN_DEVICE_ACCUMULATORS, userId, ACTION_DEVICE_ACCUMULATORS, deviceId));
+ }
+
+ private static void logObjectAction(String action, long userId, Class<?> clazz, long objectId) {
+ LOGGER.info(String.format(
+ PATTERN_OBJECT, userId, action, Introspector.decapitalize(clazz.getSimpleName()), objectId));
+ }
+
+ private static void logLinkAction(String action, long userId,
+ Class<?> owner, long ownerId, Class<?> property, long propertyId) {
+ LOGGER.info(String.format(
+ PATTERN_LINK, userId, action,
+ Introspector.decapitalize(owner.getSimpleName()), ownerId,
+ Introspector.decapitalize(property.getSimpleName()), propertyId));
+ }
+
+ private static void logLoginAction(String action, long userId) {
+ LOGGER.info(String.format(PATTERN_LOGIN, userId, action));
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/ObdDecoder.java b/src/main/java/org/traccar/helper/ObdDecoder.java
new file mode 100644
index 000000000..1bdcce352
--- /dev/null
+++ b/src/main/java/org/traccar/helper/ObdDecoder.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import org.traccar.model.Position;
+
+import java.util.AbstractMap;
+import java.util.Map;
+
+public final class ObdDecoder {
+
+ private ObdDecoder() {
+ }
+
+ private static final int MODE_CURRENT = 0x01;
+ private static final int MODE_FREEZE_FRAME = 0x02;
+ private static final int MODE_CODES = 0x03;
+
+ public static Map.Entry<String, Object> decode(int mode, String value) {
+ switch (mode) {
+ case MODE_CURRENT:
+ case MODE_FREEZE_FRAME:
+ return decodeData(
+ Integer.parseInt(value.substring(0, 2), 16),
+ Integer.parseInt(value.substring(2), 16), true);
+ case MODE_CODES:
+ return decodeCodes(value);
+ default:
+ return null;
+ }
+ }
+
+ private static Map.Entry<String, Object> createEntry(String key, Object value) {
+ return new AbstractMap.SimpleEntry<>(key, value);
+ }
+
+ public static Map.Entry<String, Object> decodeCodes(String value) {
+ StringBuilder codes = new StringBuilder();
+ for (int i = 0; i < value.length() / 4; i++) {
+ int numValue = Integer.parseInt(value.substring(i * 4, (i + 1) * 4), 16);
+ codes.append(' ');
+ switch (numValue >> 14) {
+ case 1:
+ codes.append('C');
+ break;
+ case 2:
+ codes.append('B');
+ break;
+ case 3:
+ codes.append('U');
+ break;
+ default:
+ codes.append('P');
+ break;
+ }
+ codes.append(String.format("%04X", numValue & 0x3FFF));
+ }
+ if (codes.length() > 0) {
+ return createEntry(Position.KEY_DTCS, codes.toString().trim());
+ } else {
+ return null;
+ }
+ }
+
+ public static Map.Entry<String, Object> decodeData(int pid, int value, boolean convert) {
+ switch (pid) {
+ case 0x04:
+ return createEntry(Position.KEY_ENGINE_LOAD, convert ? value * 100 / 255 : value);
+ case 0x05:
+ return createEntry(Position.KEY_COOLANT_TEMP, convert ? value - 40 : value);
+ case 0x0B:
+ return createEntry("mapIntake", value);
+ case 0x0C:
+ return createEntry(Position.KEY_RPM, convert ? value / 4 : value);
+ case 0x0D:
+ return createEntry(Position.KEY_OBD_SPEED, value);
+ case 0x0F:
+ return createEntry("intakeTemp", convert ? value - 40 : value);
+ case 0x11:
+ return createEntry(Position.KEY_THROTTLE, convert ? value * 100 / 255 : value);
+ case 0x21:
+ return createEntry("milDistance", value);
+ case 0x2F:
+ return createEntry(Position.KEY_FUEL_LEVEL, convert ? value * 100 / 255 : value);
+ case 0x31:
+ return createEntry("clearedDistance", value);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/Parser.java b/src/main/java/org/traccar/helper/Parser.java
new file mode 100644
index 000000000..1471ec237
--- /dev/null
+++ b/src/main/java/org/traccar/helper/Parser.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Parser {
+
+ private int position;
+ private final Matcher matcher;
+
+ public Parser(Pattern pattern, String input) {
+ matcher = pattern.matcher(input);
+ }
+
+ public boolean matches() {
+ position = 1;
+ return matcher.matches();
+ }
+
+ public boolean find() {
+ position = 1;
+ return matcher.find();
+ }
+
+ public void skip(int number) {
+ position += number;
+ }
+
+ public boolean hasNext() {
+ return hasNext(1);
+ }
+
+ public boolean hasNext(int number) {
+ String value = matcher.group(position);
+ if (value != null && !value.isEmpty()) {
+ return true;
+ } else {
+ position += number;
+ return false;
+ }
+ }
+
+ public String next() {
+ return matcher.group(position++);
+ }
+
+ public Integer nextInt() {
+ if (hasNext()) {
+ return Integer.parseInt(next());
+ } else {
+ return null;
+ }
+ }
+
+ public int nextInt(int defaultValue) {
+ if (hasNext()) {
+ return Integer.parseInt(next());
+ } else {
+ return defaultValue;
+ }
+ }
+
+ public Integer nextHexInt() {
+ if (hasNext()) {
+ return Integer.parseInt(next(), 16);
+ } else {
+ return null;
+ }
+ }
+
+ public int nextHexInt(int defaultValue) {
+ if (hasNext()) {
+ return Integer.parseInt(next(), 16);
+ } else {
+ return defaultValue;
+ }
+ }
+
+ public Integer nextBinInt() {
+ if (hasNext()) {
+ return Integer.parseInt(next(), 2);
+ } else {
+ return null;
+ }
+ }
+
+ public int nextBinInt(int defaultValue) {
+ if (hasNext()) {
+ return Integer.parseInt(next(), 2);
+ } else {
+ return defaultValue;
+ }
+ }
+
+ public Long nextLong() {
+ if (hasNext()) {
+ return Long.parseLong(next());
+ } else {
+ return null;
+ }
+ }
+
+ public Long nextHexLong() {
+ if (hasNext()) {
+ return Long.parseLong(next(), 16);
+ } else {
+ return null;
+ }
+ }
+
+ public long nextLong(long defaultValue) {
+ return nextLong(10, defaultValue);
+ }
+
+ public long nextLong(int radix, long defaultValue) {
+ if (hasNext()) {
+ return Long.parseLong(next(), radix);
+ } else {
+ return defaultValue;
+ }
+ }
+
+ public Double nextDouble() {
+ if (hasNext()) {
+ return Double.parseDouble(next());
+ } else {
+ return null;
+ }
+ }
+
+ public double nextDouble(double defaultValue) {
+ if (hasNext()) {
+ return Double.parseDouble(next());
+ } else {
+ return defaultValue;
+ }
+ }
+
+ public enum CoordinateFormat {
+ DEG_DEG,
+ DEG_HEM,
+ DEG_MIN_MIN,
+ DEG_MIN_HEM,
+ DEG_MIN_MIN_HEM,
+ HEM_DEG_MIN_MIN,
+ HEM_DEG,
+ HEM_DEG_MIN,
+ HEM_DEG_MIN_HEM
+ }
+
+ public double nextCoordinate(CoordinateFormat format) {
+ double coordinate;
+ String hemisphere = null;
+
+ switch (format) {
+ case DEG_DEG:
+ coordinate = Double.parseDouble(next() + '.' + next());
+ break;
+ case DEG_HEM:
+ coordinate = nextDouble(0);
+ hemisphere = next();
+ break;
+ case DEG_MIN_MIN:
+ coordinate = nextInt(0);
+ coordinate += Double.parseDouble(next() + '.' + next()) / 60;
+ break;
+ case DEG_MIN_MIN_HEM:
+ coordinate = nextInt(0);
+ coordinate += Double.parseDouble(next() + '.' + next()) / 60;
+ hemisphere = next();
+ break;
+ case HEM_DEG:
+ hemisphere = next();
+ coordinate = nextDouble(0);
+ break;
+ case HEM_DEG_MIN:
+ hemisphere = next();
+ coordinate = nextInt(0);
+ coordinate += nextDouble(0) / 60;
+ break;
+ case HEM_DEG_MIN_HEM:
+ hemisphere = next();
+ coordinate = nextInt(0);
+ coordinate += nextDouble(0) / 60;
+ if (hasNext()) {
+ hemisphere = next();
+ }
+ break;
+ case HEM_DEG_MIN_MIN:
+ hemisphere = next();
+ coordinate = nextInt(0);
+ coordinate += Double.parseDouble(next() + '.' + next()) / 60;
+ break;
+ case DEG_MIN_HEM:
+ default:
+ coordinate = nextInt(0);
+ coordinate += nextDouble(0) / 60;
+ hemisphere = next();
+ break;
+ }
+
+ if (hemisphere != null && (hemisphere.equals("S") || hemisphere.equals("W") || hemisphere.equals("-"))) {
+ coordinate = -Math.abs(coordinate);
+ }
+
+ return coordinate;
+ }
+
+ public double nextCoordinate() {
+ return nextCoordinate(CoordinateFormat.DEG_MIN_HEM);
+ }
+
+ public enum DateTimeFormat {
+ HMS,
+ SMH,
+ HMS_YMD,
+ HMS_DMY,
+ SMH_YMD,
+ SMH_DMY,
+ DMY_HMS,
+ DMY_HMSS,
+ YMD_HMS,
+ YMD_HMSS,
+ }
+
+ public Date nextDateTime(DateTimeFormat format, String timeZone) {
+ int year = 0, month = 0, day = 0;
+ int hour = 0, minute = 0, second = 0, millisecond = 0;
+
+ switch (format) {
+ case HMS:
+ hour = nextInt(0);
+ minute = nextInt(0);
+ second = nextInt(0);
+ break;
+ case SMH:
+ second = nextInt(0);
+ minute = nextInt(0);
+ hour = nextInt(0);
+ break;
+ case HMS_YMD:
+ hour = nextInt(0);
+ minute = nextInt(0);
+ second = nextInt(0);
+ year = nextInt(0);
+ month = nextInt(0);
+ day = nextInt(0);
+ break;
+ case HMS_DMY:
+ hour = nextInt(0);
+ minute = nextInt(0);
+ second = nextInt(0);
+ day = nextInt(0);
+ month = nextInt(0);
+ year = nextInt(0);
+ break;
+ case SMH_YMD:
+ second = nextInt(0);
+ minute = nextInt(0);
+ hour = nextInt(0);
+ year = nextInt(0);
+ month = nextInt(0);
+ day = nextInt(0);
+ break;
+ case SMH_DMY:
+ second = nextInt(0);
+ minute = nextInt(0);
+ hour = nextInt(0);
+ day = nextInt(0);
+ month = nextInt(0);
+ year = nextInt(0);
+ break;
+ case DMY_HMS:
+ case DMY_HMSS:
+ day = nextInt(0);
+ month = nextInt(0);
+ year = nextInt(0);
+ hour = nextInt(0);
+ minute = nextInt(0);
+ second = nextInt(0);
+ break;
+ case YMD_HMS:
+ case YMD_HMSS:
+ default:
+ year = nextInt(0);
+ month = nextInt(0);
+ day = nextInt(0);
+ hour = nextInt(0);
+ minute = nextInt(0);
+ second = nextInt(0);
+ break;
+ }
+
+ if (format == DateTimeFormat.YMD_HMSS || format == DateTimeFormat.DMY_HMSS) {
+ millisecond = nextInt(0); // (ddd)
+ }
+
+ if (year >= 0 && year < 100) {
+ year += 2000;
+ }
+
+ DateBuilder dateBuilder;
+ if (format != DateTimeFormat.HMS && format != DateTimeFormat.SMH) {
+ if (timeZone != null) {
+ dateBuilder = new DateBuilder(TimeZone.getTimeZone(timeZone));
+ } else {
+ dateBuilder = new DateBuilder();
+ }
+ dateBuilder.setDate(year, month, day);
+ } else {
+ if (timeZone != null) {
+ dateBuilder = new DateBuilder(new Date(), TimeZone.getTimeZone(timeZone));
+ } else {
+ dateBuilder = new DateBuilder(new Date());
+ }
+ }
+
+ dateBuilder.setTime(hour, minute, second, millisecond);
+
+ return dateBuilder.getDate();
+ }
+
+ public Date nextDateTime(DateTimeFormat format) {
+ return nextDateTime(format, null);
+ }
+
+ public Date nextDateTime() {
+ return nextDateTime(DateTimeFormat.YMD_HMS, null);
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/PatternBuilder.java b/src/main/java/org/traccar/helper/PatternBuilder.java
new file mode 100644
index 000000000..5c4638189
--- /dev/null
+++ b/src/main/java/org/traccar/helper/PatternBuilder.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+public class PatternBuilder {
+
+ private final ArrayList<String> fragments = new ArrayList<>();
+
+ public PatternBuilder optional() {
+ return optional(1);
+ }
+
+ public PatternBuilder optional(int count) {
+ fragments.add(fragments.size() - count, "(?:");
+ fragments.add(")?");
+ return this;
+ }
+
+ public PatternBuilder expression(String s) {
+ s = s.replaceAll("\\|$", "\\\\|"); // special case for delimiter
+
+ fragments.add(s);
+ return this;
+ }
+
+ public PatternBuilder text(String s) {
+ fragments.add(s.replaceAll("([\\\\\\.\\[\\{\\(\\)\\*\\+\\?\\^\\$\\|])", "\\\\$1"));
+ return this;
+ }
+
+ public PatternBuilder number(String s) {
+ s = s.replace("dddd", "d{4}").replace("ddd", "d{3}").replace("dd", "d{2}");
+ s = s.replace("xxxx", "x{4}").replace("xxx", "x{3}").replace("xx", "x{2}");
+
+ s = s.replace("d", "\\d").replace("x", "[0-9a-fA-F]").replaceAll("([\\.])", "\\\\$1");
+ s = s.replaceAll("\\|$", "\\\\|").replaceAll("^\\|", "\\\\|"); // special case for delimiter
+
+ fragments.add(s);
+ return this;
+ }
+
+ public PatternBuilder any() {
+ fragments.add(".*");
+ return this;
+ }
+
+ public PatternBuilder binary(String s) {
+ fragments.add(s.replaceAll("(\\p{XDigit}{2})", "\\\\$1"));
+ return this;
+ }
+
+ public PatternBuilder or() {
+ fragments.add("|");
+ return this;
+ }
+
+ public PatternBuilder groupBegin() {
+ return expression("(?:");
+ }
+
+ public PatternBuilder groupEnd() {
+ return expression(")");
+ }
+
+ public PatternBuilder groupEnd(String s) {
+ return expression(")" + s);
+ }
+
+ public Pattern compile() {
+ return Pattern.compile(toString(), Pattern.DOTALL);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ for (String fragment : fragments) {
+ builder.append(fragment);
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/PatternUtil.java b/src/main/java/org/traccar/helper/PatternUtil.java
new file mode 100644
index 000000000..74813e1d9
--- /dev/null
+++ b/src/main/java/org/traccar/helper/PatternUtil.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.management.ManagementFactory;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+public final class PatternUtil {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PatternUtil.class);
+
+ private PatternUtil() {
+ }
+
+ public static class MatchResult {
+ private String patternMatch;
+ private String patternTail;
+ private String stringMatch;
+ private String stringTail;
+
+ public String getPatternMatch() {
+ return patternMatch;
+ }
+
+ public String getPatternTail() {
+ return patternTail;
+ }
+
+ public String getStringMatch() {
+ return stringMatch;
+ }
+
+ public String getStringTail() {
+ return stringTail;
+ }
+ }
+
+ public static MatchResult checkPattern(String pattern, String input) {
+
+ if (!ManagementFactory.getRuntimeMXBean().getInputArguments().toString().contains("-agentlib:jdwp")) {
+ throw new RuntimeException("PatternUtil usage detected");
+ }
+
+ MatchResult result = new MatchResult();
+
+ for (int i = 0; i < pattern.length(); i++) {
+ try {
+ Matcher matcher = Pattern.compile("(" + pattern.substring(0, i) + ").*").matcher(input);
+ if (matcher.matches()) {
+ result.patternMatch = pattern.substring(0, i);
+ result.patternTail = pattern.substring(i);
+ result.stringMatch = matcher.group(1);
+ result.stringTail = input.substring(matcher.group(1).length());
+ }
+ } catch (PatternSyntaxException error) {
+ LOGGER.warn("Pattern matching error", error);
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/SanitizerModule.java b/src/main/java/org/traccar/helper/SanitizerModule.java
new file mode 100644
index 000000000..af9ac5c2b
--- /dev/null
+++ b/src/main/java/org/traccar/helper/SanitizerModule.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import org.owasp.encoder.Encode;
+
+import java.io.IOException;
+
+public class SanitizerModule extends SimpleModule {
+
+ public static class SanitizerSerializer extends StdSerializer<String> {
+
+ protected SanitizerSerializer() {
+ super(String.class);
+ }
+
+ @Override
+ public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
+ gen.writeString(Encode.forHtml(value));
+ }
+
+ }
+
+ public SanitizerModule() {
+ addSerializer(new SanitizerSerializer());
+ }
+
+}
diff --git a/src/main/java/org/traccar/helper/UnitsConverter.java b/src/main/java/org/traccar/helper/UnitsConverter.java
new file mode 100644
index 000000000..3dd435df4
--- /dev/null
+++ b/src/main/java/org/traccar/helper/UnitsConverter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.helper;
+
+public final class UnitsConverter {
+
+ private static final double KNOTS_TO_KPH_RATIO = 0.539957;
+ private static final double KNOTS_TO_MPH_RATIO = 0.868976;
+ private static final double KNOTS_TO_MPS_RATIO = 1.94384;
+ private static final double KNOTS_TO_CPS_RATIO = 0.0194384449;
+ private static final double METERS_TO_FEET_RATIO = 0.3048;
+ private static final double METERS_TO_MILE_RATIO = 1609.34;
+ private static final long MILLISECONDS_TO_HOURS_RATIO = 3600000;
+ private static final long MILLISECONDS_TO_MINUTES_RATIO = 60000;
+
+ private UnitsConverter() {
+ }
+
+ public static double knotsFromKph(double value) { // km/h
+ return value * KNOTS_TO_KPH_RATIO;
+ }
+
+ public static double kphFromKnots(double value) {
+ return value / KNOTS_TO_KPH_RATIO;
+ }
+
+ public static double knotsFromMph(double value) {
+ return value * KNOTS_TO_MPH_RATIO;
+ }
+
+ public static double mphFromKnots(double value) {
+ return value / KNOTS_TO_MPH_RATIO;
+ }
+
+ public static double knotsFromMps(double value) { // m/s
+ return value * KNOTS_TO_MPS_RATIO;
+ }
+
+ public static double mpsFromKnots(double value) {
+ return value / KNOTS_TO_MPS_RATIO;
+ }
+
+ public static double knotsFromCps(double value) { // cm/s
+ return value * KNOTS_TO_CPS_RATIO;
+ }
+
+ public static double feetFromMeters(double value) {
+ return value / METERS_TO_FEET_RATIO;
+ }
+
+ public static double metersFromFeet(double value) {
+ return value * METERS_TO_FEET_RATIO;
+ }
+
+ public static double milesFromMeters(double value) {
+ return value / METERS_TO_MILE_RATIO;
+ }
+
+ public static double metersFromMiles(double value) {
+ return value * METERS_TO_MILE_RATIO;
+ }
+
+ public static long msFromHours(long value) {
+ return value * MILLISECONDS_TO_HOURS_RATIO;
+ }
+
+ public static long msFromHours(double value) {
+ return (long) (value * MILLISECONDS_TO_HOURS_RATIO);
+ }
+
+ public static long msFromMinutes(long value) {
+ return value * MILLISECONDS_TO_MINUTES_RATIO;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Attribute.java b/src/main/java/org/traccar/model/Attribute.java
new file mode 100644
index 000000000..45d40b3ec
--- /dev/null
+++ b/src/main/java/org/traccar/model/Attribute.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class Attribute extends BaseModel {
+
+ private String description;
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ private String attribute;
+
+ public String getAttribute() {
+ return attribute;
+ }
+
+ public void setAttribute(String attribute) {
+ this.attribute = attribute;
+ }
+
+ private String expression;
+
+ public String getExpression() {
+ return expression;
+ }
+
+ public void setExpression(String expression) {
+ this.expression = expression;
+ }
+
+ private String type;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/BaseModel.java b/src/main/java/org/traccar/model/BaseModel.java
new file mode 100644
index 000000000..8bdb916e8
--- /dev/null
+++ b/src/main/java/org/traccar/model/BaseModel.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class BaseModel {
+
+ private long id;
+
+ public final long getId() {
+ return id;
+ }
+
+ public final void setId(long id) {
+ this.id = id;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Calendar.java b/src/main/java/org/traccar/model/Calendar.java
new file mode 100644
index 000000000..56d3eb74c
--- /dev/null
+++ b/src/main/java/org/traccar/model/Calendar.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Date;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import net.fortuna.ical4j.data.CalendarBuilder;
+import net.fortuna.ical4j.data.ParserException;
+import net.fortuna.ical4j.filter.Filter;
+import net.fortuna.ical4j.filter.PeriodRule;
+import net.fortuna.ical4j.model.DateTime;
+import net.fortuna.ical4j.model.Dur;
+import net.fortuna.ical4j.model.Period;
+import net.fortuna.ical4j.model.component.CalendarComponent;
+import org.apache.commons.collections4.Predicate;
+import org.traccar.database.QueryIgnore;
+
+public class Calendar extends ExtendedModel {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ private byte[] data;
+
+ public byte[] getData() {
+ return data.clone();
+ }
+
+ public void setData(byte[] data) throws IOException, ParserException {
+ CalendarBuilder builder = new CalendarBuilder();
+ calendar = builder.build(new ByteArrayInputStream(data));
+ this.data = data.clone();
+ }
+
+ private net.fortuna.ical4j.model.Calendar calendar;
+
+ @QueryIgnore
+ @JsonIgnore
+ public net.fortuna.ical4j.model.Calendar getCalendar() {
+ return calendar;
+ }
+
+ public boolean checkMoment(Date date) {
+ if (calendar != null) {
+ Period period = new Period(new DateTime(date), new Dur(0, 0, 0, 0));
+ Predicate<CalendarComponent> periodRule = new PeriodRule<>(period);
+ Filter<CalendarComponent> filter = new Filter<>(new Predicate[] {periodRule}, Filter.MATCH_ANY);
+ Collection<CalendarComponent> events = filter.filter(calendar.getComponents(CalendarComponent.VEVENT));
+ if (events != null && !events.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/CellTower.java b/src/main/java/org/traccar/model/CellTower.java
new file mode 100644
index 000000000..6d1dfbd7f
--- /dev/null
+++ b/src/main/java/org/traccar/model/CellTower.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.traccar.Context;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class CellTower {
+
+ public static CellTower from(int mcc, int mnc, int lac, long cid) {
+ CellTower cellTower = new CellTower();
+ cellTower.setMobileCountryCode(mcc);
+ cellTower.setMobileNetworkCode(mnc);
+ cellTower.setLocationAreaCode(lac);
+ cellTower.setCellId(cid);
+ return cellTower;
+ }
+
+ public static CellTower from(int mcc, int mnc, int lac, long cid, int rssi) {
+ CellTower cellTower = CellTower.from(mcc, mnc, lac, cid);
+ cellTower.setSignalStrength(rssi);
+ return cellTower;
+ }
+
+ public static CellTower fromLacCid(int lac, long cid) {
+ return from(
+ Context.getConfig().getInteger("geolocation.mcc"),
+ Context.getConfig().getInteger("geolocation.mnc"), lac, cid);
+ }
+
+ public static CellTower fromCidLac(long cid, int lac) {
+ return fromLacCid(lac, cid);
+ }
+
+ private String radioType;
+
+ public String getRadioType() {
+ return radioType;
+ }
+
+ public void setRadioType(String radioType) {
+ this.radioType = radioType;
+ }
+
+ private Long cellId;
+
+ public Long getCellId() {
+ return cellId;
+ }
+
+ public void setCellId(Long cellId) {
+ this.cellId = cellId;
+ }
+
+ private Integer locationAreaCode;
+
+ public Integer getLocationAreaCode() {
+ return locationAreaCode;
+ }
+
+ public void setLocationAreaCode(Integer locationAreaCode) {
+ this.locationAreaCode = locationAreaCode;
+ }
+
+ private Integer mobileCountryCode;
+
+ public Integer getMobileCountryCode() {
+ return mobileCountryCode;
+ }
+
+ public void setMobileCountryCode(Integer mobileCountryCode) {
+ this.mobileCountryCode = mobileCountryCode;
+ }
+
+ private Integer mobileNetworkCode;
+
+ public Integer getMobileNetworkCode() {
+ return mobileNetworkCode;
+ }
+
+ public void setMobileNetworkCode(Integer mobileNetworkCode) {
+ this.mobileNetworkCode = mobileNetworkCode;
+ }
+
+ private Integer signalStrength;
+
+ public Integer getSignalStrength() {
+ return signalStrength;
+ }
+
+ public void setSignalStrength(Integer signalStrength) {
+ this.signalStrength = signalStrength;
+ }
+
+ public void setOperator(long operator) {
+ String operatorString = String.valueOf(operator);
+ mobileCountryCode = Integer.parseInt(operatorString.substring(0, 3));
+ mobileNetworkCode = Integer.parseInt(operatorString.substring(3));
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Command.java b/src/main/java/org/traccar/model/Command.java
new file mode 100644
index 000000000..336fc61f4
--- /dev/null
+++ b/src/main/java/org/traccar/model/Command.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import org.traccar.database.QueryIgnore;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Command extends Message implements Cloneable {
+
+ public static final String TYPE_CUSTOM = "custom";
+ public static final String TYPE_IDENTIFICATION = "deviceIdentification";
+ public static final String TYPE_POSITION_SINGLE = "positionSingle";
+ public static final String TYPE_POSITION_PERIODIC = "positionPeriodic";
+ public static final String TYPE_POSITION_STOP = "positionStop";
+ public static final String TYPE_ENGINE_STOP = "engineStop";
+ public static final String TYPE_ENGINE_RESUME = "engineResume";
+ public static final String TYPE_ALARM_ARM = "alarmArm";
+ public static final String TYPE_ALARM_DISARM = "alarmDisarm";
+ public static final String TYPE_SET_TIMEZONE = "setTimezone";
+ public static final String TYPE_REQUEST_PHOTO = "requestPhoto";
+ public static final String TYPE_POWER_OFF = "powerOff";
+ public static final String TYPE_REBOOT_DEVICE = "rebootDevice";
+ public static final String TYPE_SEND_SMS = "sendSms";
+ public static final String TYPE_SEND_USSD = "sendUssd";
+ public static final String TYPE_SOS_NUMBER = "sosNumber";
+ public static final String TYPE_SILENCE_TIME = "silenceTime";
+ public static final String TYPE_SET_PHONEBOOK = "setPhonebook";
+ public static final String TYPE_MESSAGE = "message";
+ public static final String TYPE_VOICE_MESSAGE = "voiceMessage";
+ public static final String TYPE_OUTPUT_CONTROL = "outputControl";
+ public static final String TYPE_VOICE_MONITORING = "voiceMonitoring";
+ public static final String TYPE_SET_AGPS = "setAgps";
+ public static final String TYPE_SET_INDICATOR = "setIndicator";
+ public static final String TYPE_CONFIGURATION = "configuration";
+ public static final String TYPE_GET_VERSION = "getVersion";
+ public static final String TYPE_FIRMWARE_UPDATE = "firmwareUpdate";
+ public static final String TYPE_SET_CONNECTION = "setConnection";
+ public static final String TYPE_SET_ODOMETER = "setOdometer";
+ public static final String TYPE_GET_MODEM_STATUS = "getModemStatus";
+ public static final String TYPE_GET_DEVICE_STATUS = "getDeviceStatus";
+
+ public static final String TYPE_MODE_POWER_SAVING = "modePowerSaving";
+ public static final String TYPE_MODE_DEEP_SLEEP = "modeDeepSleep";
+
+ public static final String TYPE_ALARM_GEOFENCE = "movementAlarm";
+ public static final String TYPE_ALARM_BATTERY = "alarmBattery";
+ public static final String TYPE_ALARM_SOS = "alarmSos";
+ public static final String TYPE_ALARM_REMOVE = "alarmRemove";
+ public static final String TYPE_ALARM_CLOCK = "alarmClock";
+ public static final String TYPE_ALARM_SPEED = "alarmSpeed";
+ public static final String TYPE_ALARM_FALL = "alarmFall";
+ public static final String TYPE_ALARM_VIBRATION = "alarmVibration";
+
+ public static final String KEY_UNIQUE_ID = "uniqueId";
+ public static final String KEY_FREQUENCY = "frequency";
+ public static final String KEY_TIMEZONE = "timezone";
+ public static final String KEY_DEVICE_PASSWORD = "devicePassword";
+ public static final String KEY_RADIUS = "radius";
+ public static final String KEY_MESSAGE = "message";
+ public static final String KEY_ENABLE = "enable";
+ public static final String KEY_DATA = "data";
+ public static final String KEY_INDEX = "index";
+ public static final String KEY_PHONE = "phone";
+ public static final String KEY_SERVER = "server";
+ public static final String KEY_PORT = "port";
+
+ @Override
+ public Command clone() throws CloneNotSupportedException {
+ return (Command) super.clone();
+ }
+
+ private boolean textChannel;
+
+ public boolean getTextChannel() {
+ return textChannel;
+ }
+
+ public void setTextChannel(boolean textChannel) {
+ this.textChannel = textChannel;
+ }
+
+ @QueryIgnore
+ @Override
+ public long getDeviceId() {
+ return super.getDeviceId();
+ }
+
+ private String description;
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Device.java b/src/main/java/org/traccar/model/Device.java
new file mode 100644
index 000000000..0c9be932d
--- /dev/null
+++ b/src/main/java/org/traccar/model/Device.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.util.Date;
+import java.util.List;
+
+import org.traccar.database.QueryExtended;
+import org.traccar.database.QueryIgnore;
+
+public class Device extends GroupedModel {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ private String uniqueId;
+
+ public String getUniqueId() {
+ return uniqueId;
+ }
+
+ public void setUniqueId(String uniqueId) {
+ this.uniqueId = uniqueId;
+ }
+
+ public static final String STATUS_UNKNOWN = "unknown";
+ public static final String STATUS_ONLINE = "online";
+ public static final String STATUS_OFFLINE = "offline";
+
+ private String status;
+
+ @QueryIgnore
+ public String getStatus() {
+ return status != null ? status : STATUS_OFFLINE;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ private Date lastUpdate;
+
+ @QueryExtended
+ public Date getLastUpdate() {
+ if (lastUpdate != null) {
+ return new Date(lastUpdate.getTime());
+ } else {
+ return null;
+ }
+ }
+
+ public void setLastUpdate(Date lastUpdate) {
+ if (lastUpdate != null) {
+ this.lastUpdate = new Date(lastUpdate.getTime());
+ } else {
+ this.lastUpdate = null;
+ }
+ }
+
+ private long positionId;
+
+ @QueryIgnore
+ public long getPositionId() {
+ return positionId;
+ }
+
+ public void setPositionId(long positionId) {
+ this.positionId = positionId;
+ }
+
+ private List<Long> geofenceIds;
+
+ @QueryIgnore
+ public List<Long> getGeofenceIds() {
+ return geofenceIds;
+ }
+
+ public void setGeofenceIds(List<Long> geofenceIds) {
+ this.geofenceIds = geofenceIds;
+ }
+
+ private String phone;
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ private String model;
+
+ public String getModel() {
+ return model;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ private String contact;
+
+ public String getContact() {
+ return contact;
+ }
+
+ public void setContact(String contact) {
+ this.contact = contact;
+ }
+
+ private String category;
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ private boolean disabled;
+
+ public boolean getDisabled() {
+ return disabled;
+ }
+
+ public void setDisabled(boolean disabled) {
+ this.disabled = disabled;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/DeviceAccumulators.java b/src/main/java/org/traccar/model/DeviceAccumulators.java
new file mode 100644
index 000000000..8a90826c4
--- /dev/null
+++ b/src/main/java/org/traccar/model/DeviceAccumulators.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class DeviceAccumulators {
+
+ private long deviceId;
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ private Double totalDistance;
+
+ public Double getTotalDistance() {
+ return totalDistance;
+ }
+
+ public void setTotalDistance(Double totalDistance) {
+ this.totalDistance = totalDistance;
+ }
+
+ private Long hours;
+
+ public Long getHours() {
+ return hours;
+ }
+
+ public void setHours(Long hours) {
+ this.hours = hours;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/DeviceState.java b/src/main/java/org/traccar/model/DeviceState.java
new file mode 100644
index 000000000..75d6726ee
--- /dev/null
+++ b/src/main/java/org/traccar/model/DeviceState.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class DeviceState {
+
+ private Boolean motionState;
+
+ public void setMotionState(boolean motionState) {
+ this.motionState = motionState;
+ }
+
+ public Boolean getMotionState() {
+ return motionState;
+ }
+
+ private Position motionPosition;
+
+ public void setMotionPosition(Position motionPosition) {
+ this.motionPosition = motionPosition;
+ }
+
+ public Position getMotionPosition() {
+ return motionPosition;
+ }
+
+ private Boolean overspeedState;
+
+ public void setOverspeedState(boolean overspeedState) {
+ this.overspeedState = overspeedState;
+ }
+
+ public Boolean getOverspeedState() {
+ return overspeedState;
+ }
+
+ private Position overspeedPosition;
+
+ public void setOverspeedPosition(Position overspeedPosition) {
+ this.overspeedPosition = overspeedPosition;
+ }
+
+ public Position getOverspeedPosition() {
+ return overspeedPosition;
+ }
+
+ private long overspeedGeofenceId;
+
+ public void setOverspeedGeofenceId(long overspeedGeofenceId) {
+ this.overspeedGeofenceId = overspeedGeofenceId;
+ }
+
+ public long getOverspeedGeofenceId() {
+ return overspeedGeofenceId;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Driver.java b/src/main/java/org/traccar/model/Driver.java
new file mode 100644
index 000000000..05f52fd4d
--- /dev/null
+++ b/src/main/java/org/traccar/model/Driver.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class Driver extends ExtendedModel {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ private String uniqueId;
+
+ public String getUniqueId() {
+ return uniqueId;
+ }
+
+ public void setUniqueId(String uniqueId) {
+ this.uniqueId = uniqueId;
+ }
+}
diff --git a/src/main/java/org/traccar/model/Event.java b/src/main/java/org/traccar/model/Event.java
new file mode 100644
index 000000000..ee7fcc679
--- /dev/null
+++ b/src/main/java/org/traccar/model/Event.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.util.Date;
+
+public class Event extends Message {
+
+ public Event(String type, long deviceId, long positionId) {
+ this(type, deviceId);
+ setPositionId(positionId);
+ }
+
+ public Event(String type, long deviceId) {
+ setType(type);
+ setDeviceId(deviceId);
+ this.serverTime = new Date();
+ }
+
+ public Event() {
+ }
+
+ public static final String ALL_EVENTS = "allEvents";
+
+ public static final String TYPE_COMMAND_RESULT = "commandResult";
+
+ public static final String TYPE_DEVICE_ONLINE = "deviceOnline";
+ public static final String TYPE_DEVICE_UNKNOWN = "deviceUnknown";
+ public static final String TYPE_DEVICE_OFFLINE = "deviceOffline";
+
+ public static final String TYPE_DEVICE_MOVING = "deviceMoving";
+ public static final String TYPE_DEVICE_STOPPED = "deviceStopped";
+
+ public static final String TYPE_DEVICE_OVERSPEED = "deviceOverspeed";
+ public static final String TYPE_DEVICE_FUEL_DROP = "deviceFuelDrop";
+
+ public static final String TYPE_GEOFENCE_ENTER = "geofenceEnter";
+ public static final String TYPE_GEOFENCE_EXIT = "geofenceExit";
+
+ public static final String TYPE_ALARM = "alarm";
+
+ public static final String TYPE_IGNITION_ON = "ignitionOn";
+ public static final String TYPE_IGNITION_OFF = "ignitionOff";
+
+ public static final String TYPE_MAINTENANCE = "maintenance";
+
+ public static final String TYPE_TEXT_MESSAGE = "textMessage";
+
+ public static final String TYPE_DRIVER_CHANGED = "driverChanged";
+
+ private Date serverTime;
+
+ public Date getServerTime() {
+ return serverTime;
+ }
+
+ public void setServerTime(Date serverTime) {
+ this.serverTime = serverTime;
+ }
+
+ private long positionId;
+
+ public long getPositionId() {
+ return positionId;
+ }
+
+ public void setPositionId(long positionId) {
+ this.positionId = positionId;
+ }
+
+ private long geofenceId = 0;
+
+ public long getGeofenceId() {
+ return geofenceId;
+ }
+
+ public void setGeofenceId(long geofenceId) {
+ this.geofenceId = geofenceId;
+ }
+
+ private long maintenanceId = 0;
+
+ public long getMaintenanceId() {
+ return maintenanceId;
+ }
+
+ public void setMaintenanceId(long maintenanceId) {
+ this.maintenanceId = maintenanceId;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/ExtendedModel.java b/src/main/java/org/traccar/model/ExtendedModel.java
new file mode 100644
index 000000000..8353d0e66
--- /dev/null
+++ b/src/main/java/org/traccar/model/ExtendedModel.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class ExtendedModel extends BaseModel {
+
+ private Map<String, Object> attributes = new LinkedHashMap<>();
+
+ public Map<String, Object> getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Map<String, Object> attributes) {
+ this.attributes = attributes;
+ }
+
+ public void set(String key, Boolean value) {
+ if (value != null) {
+ attributes.put(key, value);
+ }
+ }
+
+ public void set(String key, Byte value) {
+ if (value != null) {
+ attributes.put(key, value.intValue());
+ }
+ }
+
+ public void set(String key, Short value) {
+ if (value != null) {
+ attributes.put(key, value.intValue());
+ }
+ }
+
+ public void set(String key, Integer value) {
+ if (value != null) {
+ attributes.put(key, value);
+ }
+ }
+
+ public void set(String key, Long value) {
+ if (value != null) {
+ attributes.put(key, value);
+ }
+ }
+
+ public void set(String key, Float value) {
+ if (value != null) {
+ attributes.put(key, value.doubleValue());
+ }
+ }
+
+ public void set(String key, Double value) {
+ if (value != null) {
+ attributes.put(key, value);
+ }
+ }
+
+ public void set(String key, String value) {
+ if (value != null && !value.isEmpty()) {
+ attributes.put(key, value);
+ }
+ }
+
+ public void add(Map.Entry<String, Object> entry) {
+ if (entry != null && entry.getValue() != null) {
+ attributes.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public String getString(String key) {
+ if (attributes.containsKey(key)) {
+ return (String) attributes.get(key);
+ } else {
+ return null;
+ }
+ }
+
+ public double getDouble(String key) {
+ if (attributes.containsKey(key)) {
+ return ((Number) attributes.get(key)).doubleValue();
+ } else {
+ return 0.0;
+ }
+ }
+
+ public boolean getBoolean(String key) {
+ if (attributes.containsKey(key)) {
+ return (Boolean) attributes.get(key);
+ } else {
+ return false;
+ }
+ }
+
+ public int getInteger(String key) {
+ if (attributes.containsKey(key)) {
+ return ((Number) attributes.get(key)).intValue();
+ } else {
+ return 0;
+ }
+ }
+
+ public long getLong(String key) {
+ if (attributes.containsKey(key)) {
+ return ((Number) attributes.get(key)).longValue();
+ } else {
+ return 0;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Geofence.java b/src/main/java/org/traccar/model/Geofence.java
new file mode 100644
index 000000000..8560d22e9
--- /dev/null
+++ b/src/main/java/org/traccar/model/Geofence.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.text.ParseException;
+
+import org.traccar.Context;
+import org.traccar.database.QueryIgnore;
+import org.traccar.geofence.GeofenceCircle;
+import org.traccar.geofence.GeofenceGeometry;
+import org.traccar.geofence.GeofencePolygon;
+import org.traccar.geofence.GeofencePolyline;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class Geofence extends ScheduledModel {
+
+ public static final String TYPE_GEOFENCE_CILCLE = "geofenceCircle";
+ public static final String TYPE_GEOFENCE_POLYGON = "geofencePolygon";
+ public static final String TYPE_GEOFENCE_POLYLINE = "geofencePolyline";
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ private String description;
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ private String area;
+
+ public String getArea() {
+ return area;
+ }
+
+ public void setArea(String area) throws ParseException {
+
+ if (area.startsWith("CIRCLE")) {
+ geometry = new GeofenceCircle(area);
+ } else if (area.startsWith("POLYGON")) {
+ geometry = new GeofencePolygon(area);
+ } else if (area.startsWith("LINESTRING")) {
+ final double distance = getDouble("polylineDistance");
+ geometry = new GeofencePolyline(area, distance > 0 ? distance
+ : Context.getConfig().getDouble("geofence.polylineDistance", 25));
+ } else {
+ throw new ParseException("Unknown geometry type", 0);
+ }
+
+ this.area = area;
+ }
+
+ private GeofenceGeometry geometry;
+
+ @QueryIgnore
+ @JsonIgnore
+ public GeofenceGeometry getGeometry() {
+ return geometry;
+ }
+
+ @QueryIgnore
+ @JsonIgnore
+ public void setGeometry(GeofenceGeometry geometry) {
+ area = geometry.toWkt();
+ this.geometry = geometry;
+ }
+}
diff --git a/src/main/java/org/traccar/model/Group.java b/src/main/java/org/traccar/model/Group.java
new file mode 100644
index 000000000..91ea2319d
--- /dev/null
+++ b/src/main/java/org/traccar/model/Group.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class Group extends GroupedModel {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/GroupedModel.java b/src/main/java/org/traccar/model/GroupedModel.java
new file mode 100644
index 000000000..6b1aa75b1
--- /dev/null
+++ b/src/main/java/org/traccar/model/GroupedModel.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class GroupedModel extends ExtendedModel {
+
+ private long groupId;
+
+ public long getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(long groupId) {
+ this.groupId = groupId;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Maintenance.java b/src/main/java/org/traccar/model/Maintenance.java
new file mode 100644
index 000000000..73f67ea96
--- /dev/null
+++ b/src/main/java/org/traccar/model/Maintenance.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class Maintenance extends ExtendedModel {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ private String type;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ private double start;
+
+ public double getStart() {
+ return start;
+ }
+
+ public void setStart(double start) {
+ this.start = start;
+ }
+
+ private double period;
+
+ public double getPeriod() {
+ return period;
+ }
+
+ public void setPeriod(double period) {
+ this.period = period;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/ManagedUser.java b/src/main/java/org/traccar/model/ManagedUser.java
new file mode 100644
index 000000000..03c5ef48d
--- /dev/null
+++ b/src/main/java/org/traccar/model/ManagedUser.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class ManagedUser extends User {
+
+}
diff --git a/src/main/java/org/traccar/model/Message.java b/src/main/java/org/traccar/model/Message.java
new file mode 100644
index 000000000..dad9c20f0
--- /dev/null
+++ b/src/main/java/org/traccar/model/Message.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2013 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class Message extends ExtendedModel {
+
+ private long deviceId;
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ private String type;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/MiscFormatter.java b/src/main/java/org/traccar/model/MiscFormatter.java
new file mode 100644
index 000000000..c6511f063
--- /dev/null
+++ b/src/main/java/org/traccar/model/MiscFormatter.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+
+import java.text.DecimalFormat;
+import java.util.Map;
+
+public final class MiscFormatter {
+
+ private MiscFormatter() {
+ }
+
+ private static final String XML_ROOT_NODE = "info";
+
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
+
+ private static String format(Object value) {
+ if (value instanceof Double || value instanceof Float) {
+ return DECIMAL_FORMAT.format(value);
+ } else {
+ return value.toString();
+ }
+ }
+
+ public static String toXmlString(Map<String, Object> attributes) {
+ StringBuilder result = new StringBuilder();
+
+ result.append("<").append(XML_ROOT_NODE).append(">");
+
+ for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+
+ result.append("<").append(entry.getKey()).append(">");
+ result.append(format(entry.getValue()));
+ result.append("</").append(entry.getKey()).append(">");
+ }
+
+ result.append("</").append(XML_ROOT_NODE).append(">");
+
+ return result.toString();
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Network.java b/src/main/java/org/traccar/model/Network.java
new file mode 100644
index 000000000..2d56950f1
--- /dev/null
+++ b/src/main/java/org/traccar/model/Network.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class Network {
+
+ public Network() {
+ }
+
+ public Network(CellTower cellTower) {
+ addCellTower(cellTower);
+ }
+
+ private Integer homeMobileCountryCode;
+
+ public Integer getHomeMobileCountryCode() {
+ return homeMobileCountryCode;
+ }
+
+ public void setHomeMobileCountryCode(Integer homeMobileCountryCode) {
+ this.homeMobileCountryCode = homeMobileCountryCode;
+ }
+
+ private Integer homeMobileNetworkCode;
+
+ public Integer getHomeMobileNetworkCode() {
+ return homeMobileNetworkCode;
+ }
+
+ public void setHomeMobileNetworkCode(Integer homeMobileNetworkCode) {
+ this.homeMobileNetworkCode = homeMobileNetworkCode;
+ }
+
+ private String radioType = "gsm";
+
+ public String getRadioType() {
+ return radioType;
+ }
+
+ public void setRadioType(String radioType) {
+ this.radioType = radioType;
+ }
+
+ private String carrier;
+
+ public String getCarrier() {
+ return carrier;
+ }
+
+ public void setCarrier(String carrier) {
+ this.carrier = carrier;
+ }
+
+ private Boolean considerIp = false;
+
+ public Boolean getConsiderIp() {
+ return considerIp;
+ }
+
+ public void setConsiderIp(Boolean considerIp) {
+ this.considerIp = considerIp;
+ }
+
+ private Collection<CellTower> cellTowers;
+
+ public Collection<CellTower> getCellTowers() {
+ return cellTowers;
+ }
+
+ public void setCellTowers(Collection<CellTower> cellTowers) {
+ this.cellTowers = cellTowers;
+ }
+
+ public void addCellTower(CellTower cellTower) {
+ if (cellTowers == null) {
+ cellTowers = new ArrayList<>();
+ }
+ cellTowers.add(cellTower);
+ }
+
+ private Collection<WifiAccessPoint> wifiAccessPoints;
+
+ public Collection<WifiAccessPoint> getWifiAccessPoints() {
+ return wifiAccessPoints;
+ }
+
+ public void setWifiAccessPoints(Collection<WifiAccessPoint> wifiAccessPoints) {
+ this.wifiAccessPoints = wifiAccessPoints;
+ }
+
+ public void addWifiAccessPoint(WifiAccessPoint wifiAccessPoint) {
+ if (wifiAccessPoints == null) {
+ wifiAccessPoints = new ArrayList<>();
+ }
+ wifiAccessPoints.add(wifiAccessPoint);
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Notification.java b/src/main/java/org/traccar/model/Notification.java
new file mode 100644
index 000000000..f1983a03a
--- /dev/null
+++ b/src/main/java/org/traccar/model/Notification.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.traccar.database.QueryIgnore;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class Notification extends ScheduledModel {
+
+ private boolean always;
+
+ public boolean getAlways() {
+ return always;
+ }
+
+ public void setAlways(boolean always) {
+ this.always = always;
+ }
+
+ private String type;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+
+ private String notificators;
+
+ public String getNotificators() {
+ return notificators;
+ }
+
+ public void setNotificators(String transports) {
+ this.notificators = transports;
+ }
+
+
+ @JsonIgnore
+ @QueryIgnore
+ public Set<String> getNotificatorsTypes() {
+ final Set<String> result = new HashSet<>();
+ if (notificators != null) {
+ final String[] transportsList = notificators.split(",");
+ for (String transport : transportsList) {
+ result.add(transport.trim());
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Permission.java b/src/main/java/org/traccar/model/Permission.java
new file mode 100644
index 000000000..1006b1c47
--- /dev/null
+++ b/src/main/java/org/traccar/model/Permission.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.traccar.database.DataManager;
+
+public class Permission {
+
+ private Class<?> ownerClass;
+ private long ownerId;
+ private Class<?> propertyClass;
+ private long propertyId;
+
+ public Permission(LinkedHashMap<String, Long> permissionMap) throws ClassNotFoundException {
+ Iterator<Map.Entry<String, Long>> iterator = permissionMap.entrySet().iterator();
+ String owner = iterator.next().getKey();
+ ownerClass = DataManager.getClassByName(owner);
+ String property = iterator.next().getKey();
+ propertyClass = DataManager.getClassByName(property);
+ ownerId = permissionMap.get(owner);
+ propertyId = permissionMap.get(property);
+ }
+
+ public Class<?> getOwnerClass() {
+ return ownerClass;
+ }
+
+ public long getOwnerId() {
+ return ownerId;
+ }
+
+ public Class<?> getPropertyClass() {
+ return propertyClass;
+ }
+
+ public long getPropertyId() {
+ return propertyId;
+ }
+}
diff --git a/src/main/java/org/traccar/model/Position.java b/src/main/java/org/traccar/model/Position.java
new file mode 100644
index 000000000..4b327cbd2
--- /dev/null
+++ b/src/main/java/org/traccar/model/Position.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2012 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.util.Date;
+
+import org.traccar.database.QueryIgnore;
+
+public class Position extends Message {
+
+ public static final String KEY_ORIGINAL = "raw";
+ public static final String KEY_INDEX = "index";
+ public static final String KEY_HDOP = "hdop";
+ public static final String KEY_VDOP = "vdop";
+ public static final String KEY_PDOP = "pdop";
+ public static final String KEY_SATELLITES = "sat"; // in use
+ public static final String KEY_SATELLITES_VISIBLE = "satVisible";
+ public static final String KEY_RSSI = "rssi";
+ public static final String KEY_GPS = "gps";
+ public static final String KEY_ROAMING = "roaming";
+ public static final String KEY_EVENT = "event";
+ public static final String KEY_ALARM = "alarm";
+ public static final String KEY_STATUS = "status";
+ public static final String KEY_ODOMETER = "odometer"; // meters
+ public static final String KEY_ODOMETER_SERVICE = "serviceOdometer"; // meters
+ public static final String KEY_ODOMETER_TRIP = "tripOdometer"; // meters
+ public static final String KEY_HOURS = "hours";
+ public static final String KEY_STEPS = "steps";
+ public static final String KEY_HEART_RATE = "heartRate";
+ public static final String KEY_INPUT = "input";
+ public static final String KEY_OUTPUT = "output";
+ public static final String KEY_IMAGE = "image";
+ public static final String KEY_VIDEO = "video";
+ public static final String KEY_AUDIO = "audio";
+
+ // The units for the below four KEYs currently vary.
+ // The preferred units of measure are specified in the comment for each.
+ public static final String KEY_POWER = "power"; // volts
+ public static final String KEY_BATTERY = "battery"; // volts
+ public static final String KEY_BATTERY_LEVEL = "batteryLevel"; // percentage
+ public static final String KEY_FUEL_LEVEL = "fuel"; // liters
+ public static final String KEY_FUEL_USED = "fuelUsed"; // liters
+ public static final String KEY_FUEL_CONSUMPTION = "fuelConsumption"; // liters/hour
+
+ public static final String KEY_VERSION_FW = "versionFw";
+ public static final String KEY_VERSION_HW = "versionHw";
+ public static final String KEY_TYPE = "type";
+ public static final String KEY_IGNITION = "ignition";
+ public static final String KEY_FLAGS = "flags";
+ public static final String KEY_ANTENNA = "antenna";
+ public static final String KEY_CHARGE = "charge";
+ public static final String KEY_IP = "ip";
+ public static final String KEY_ARCHIVE = "archive";
+ public static final String KEY_DISTANCE = "distance"; // meters
+ public static final String KEY_TOTAL_DISTANCE = "totalDistance"; // meters
+ public static final String KEY_RPM = "rpm";
+ public static final String KEY_VIN = "vin";
+ public static final String KEY_APPROXIMATE = "approximate";
+ public static final String KEY_THROTTLE = "throttle";
+ public static final String KEY_MOTION = "motion";
+ public static final String KEY_ARMED = "armed";
+ public static final String KEY_GEOFENCE = "geofence";
+ public static final String KEY_ACCELERATION = "acceleration";
+ public static final String KEY_DEVICE_TEMP = "deviceTemp"; // celsius
+ public static final String KEY_COOLANT_TEMP = "coolantTemp"; // celsius
+ public static final String KEY_ENGINE_LOAD = "engineLoad";
+ public static final String KEY_OPERATOR = "operator";
+ public static final String KEY_COMMAND = "command";
+ public static final String KEY_BLOCKED = "blocked";
+ public static final String KEY_DOOR = "door";
+ public static final String KEY_AXLE_WEIGHT = "axleWeight";
+
+ public static final String KEY_DTCS = "dtcs";
+ public static final String KEY_OBD_SPEED = "obdSpeed"; // knots
+ public static final String KEY_OBD_ODOMETER = "obdOdometer"; // meters
+
+ public static final String KEY_RESULT = "result";
+
+ public static final String KEY_DRIVER_UNIQUE_ID = "driverUniqueId";
+
+ // Start with 1 not 0
+ public static final String PREFIX_TEMP = "temp";
+ public static final String PREFIX_ADC = "adc";
+ public static final String PREFIX_IO = "io";
+ public static final String PREFIX_COUNT = "count";
+ public static final String PREFIX_IN = "in";
+ public static final String PREFIX_OUT = "out";
+
+ public static final String ALARM_GENERAL = "general";
+ public static final String ALARM_SOS = "sos";
+ public static final String ALARM_VIBRATION = "vibration";
+ public static final String ALARM_MOVEMENT = "movement";
+ public static final String ALARM_LOW_SPEED = "lowspeed";
+ public static final String ALARM_OVERSPEED = "overspeed";
+ public static final String ALARM_FALL_DOWN = "fallDown";
+ public static final String ALARM_LOW_POWER = "lowPower";
+ public static final String ALARM_LOW_BATTERY = "lowBattery";
+ public static final String ALARM_FAULT = "fault";
+ public static final String ALARM_POWER_OFF = "powerOff";
+ public static final String ALARM_POWER_ON = "powerOn";
+ public static final String ALARM_DOOR = "door";
+ public static final String ALARM_LOCK = "lock";
+ public static final String ALARM_UNLOCK = "unlock";
+ public static final String ALARM_GEOFENCE = "geofence";
+ public static final String ALARM_GEOFENCE_ENTER = "geofenceEnter";
+ public static final String ALARM_GEOFENCE_EXIT = "geofenceExit";
+ public static final String ALARM_GPS_ANTENNA_CUT = "gpsAntennaCut";
+ public static final String ALARM_ACCIDENT = "accident";
+ public static final String ALARM_TOW = "tow";
+ public static final String ALARM_IDLE = "idle";
+ public static final String ALARM_HIGH_RPM = "highRpm";
+ public static final String ALARM_ACCELERATION = "hardAcceleration";
+ public static final String ALARM_BRAKING = "hardBraking";
+ public static final String ALARM_CORNERING = "hardCornering";
+ public static final String ALARM_LANE_CHANGE = "laneChange";
+ public static final String ALARM_FATIGUE_DRIVING = "fatigueDriving";
+ public static final String ALARM_POWER_CUT = "powerCut";
+ public static final String ALARM_POWER_RESTORED = "powerRestored";
+ public static final String ALARM_JAMMING = "jamming";
+ public static final String ALARM_TEMPERATURE = "temperature";
+ public static final String ALARM_PARKING = "parking";
+ public static final String ALARM_SHOCK = "shock";
+ public static final String ALARM_BONNET = "bonnet";
+ public static final String ALARM_FOOT_BRAKE = "footBrake";
+ public static final String ALARM_FUEL_LEAK = "fuelLeak";
+ public static final String ALARM_TAMPERING = "tampering";
+ public static final String ALARM_REMOVING = "removing";
+
+ public Position() {
+ }
+
+ public Position(String protocol) {
+ this.protocol = protocol;
+ this.serverTime = new Date();
+ }
+
+ private String protocol;
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ private Date serverTime;
+
+ public Date getServerTime() {
+ return serverTime;
+ }
+
+ public void setServerTime(Date serverTime) {
+ this.serverTime = serverTime;
+ }
+
+ private Date deviceTime;
+
+ public Date getDeviceTime() {
+ return deviceTime;
+ }
+
+ public void setDeviceTime(Date deviceTime) {
+ this.deviceTime = deviceTime;
+ }
+
+ private Date fixTime;
+
+ public Date getFixTime() {
+ return fixTime;
+ }
+
+ public void setFixTime(Date fixTime) {
+ this.fixTime = fixTime;
+ }
+
+ public void setTime(Date time) {
+ setDeviceTime(time);
+ setFixTime(time);
+ }
+
+ private boolean outdated;
+
+ @QueryIgnore
+ public boolean getOutdated() {
+ return outdated;
+ }
+
+ public void setOutdated(boolean outdated) {
+ this.outdated = outdated;
+ }
+
+ private boolean valid;
+
+ public boolean getValid() {
+ return valid;
+ }
+
+ public void setValid(boolean valid) {
+ this.valid = valid;
+ }
+
+ private double latitude;
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = latitude;
+ }
+
+ private double longitude;
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = longitude;
+ }
+
+ private double altitude; // value in meters
+
+ public double getAltitude() {
+ return altitude;
+ }
+
+ public void setAltitude(double altitude) {
+ this.altitude = altitude;
+ }
+
+ private double speed; // value in knots
+
+ public double getSpeed() {
+ return speed;
+ }
+
+ public void setSpeed(double speed) {
+ this.speed = speed;
+ }
+
+ private double course;
+
+ public double getCourse() {
+ return course;
+ }
+
+ public void setCourse(double course) {
+ this.course = course;
+ }
+
+ private String address;
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ private double accuracy;
+
+ public double getAccuracy() {
+ return accuracy;
+ }
+
+ public void setAccuracy(double accuracy) {
+ this.accuracy = accuracy;
+ }
+
+ private Network network;
+
+ public Network getNetwork() {
+ return network;
+ }
+
+ public void setNetwork(Network network) {
+ this.network = network;
+ }
+
+ @Override
+ @QueryIgnore
+ public String getType() {
+ return super.getType();
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/ScheduledModel.java b/src/main/java/org/traccar/model/ScheduledModel.java
new file mode 100644
index 000000000..9e6a4b9a6
--- /dev/null
+++ b/src/main/java/org/traccar/model/ScheduledModel.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class ScheduledModel extends ExtendedModel {
+
+ private long calendarId;
+
+ public long getCalendarId() {
+ return calendarId;
+ }
+
+ public void setCalendarId(long calendarId) {
+ this.calendarId = calendarId;
+ }
+}
diff --git a/src/main/java/org/traccar/model/Server.java b/src/main/java/org/traccar/model/Server.java
new file mode 100644
index 000000000..ad37e7078
--- /dev/null
+++ b/src/main/java/org/traccar/model/Server.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import org.traccar.database.QueryIgnore;
+
+public class Server extends ExtendedModel {
+
+ @QueryIgnore
+ public String getVersion() {
+ return getClass().getPackage().getImplementationVersion();
+ }
+
+ public void setVersion(String version) {
+ }
+
+ private boolean registration;
+
+ public boolean getRegistration() {
+ return registration;
+ }
+
+ public void setRegistration(boolean registration) {
+ this.registration = registration;
+ }
+
+ private boolean readonly;
+
+ public boolean getReadonly() {
+ return readonly;
+ }
+
+ public void setReadonly(boolean readonly) {
+ this.readonly = readonly;
+ }
+
+ private boolean deviceReadonly;
+
+ public boolean getDeviceReadonly() {
+ return deviceReadonly;
+ }
+
+ public void setDeviceReadonly(boolean deviceReadonly) {
+ this.deviceReadonly = deviceReadonly;
+ }
+
+ private String map;
+
+ public String getMap() {
+ return map;
+ }
+
+ public void setMap(String map) {
+ this.map = map;
+ }
+
+ private String bingKey;
+
+ public String getBingKey() {
+ return bingKey;
+ }
+
+ public void setBingKey(String bingKey) {
+ this.bingKey = bingKey;
+ }
+
+ private String mapUrl;
+
+ public String getMapUrl() {
+ return mapUrl;
+ }
+
+ public void setMapUrl(String mapUrl) {
+ this.mapUrl = mapUrl;
+ }
+
+ private double latitude;
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = latitude;
+ }
+
+ private double longitude;
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = longitude;
+ }
+
+ private int zoom;
+
+ public int getZoom() {
+ return zoom;
+ }
+
+ public void setZoom(int zoom) {
+ this.zoom = zoom;
+ }
+
+ private boolean twelveHourFormat;
+
+ public boolean getTwelveHourFormat() {
+ return twelveHourFormat;
+ }
+
+ public void setTwelveHourFormat(boolean twelveHourFormat) {
+ this.twelveHourFormat = twelveHourFormat;
+ }
+
+ private boolean forceSettings;
+
+ public boolean getForceSettings() {
+ return forceSettings;
+ }
+
+ public void setForceSettings(boolean forceSettings) {
+ this.forceSettings = forceSettings;
+ }
+
+ private String coordinateFormat;
+
+ public String getCoordinateFormat() {
+ return coordinateFormat;
+ }
+
+ public void setCoordinateFormat(String coordinateFormat) {
+ this.coordinateFormat = coordinateFormat;
+ }
+
+ private boolean limitCommands;
+
+ public boolean getLimitCommands() {
+ return limitCommands;
+ }
+
+ public void setLimitCommands(boolean limitCommands) {
+ this.limitCommands = limitCommands;
+ }
+
+ private String poiLayer;
+
+ public String getPoiLayer() {
+ return poiLayer;
+ }
+
+ public void setPoiLayer(String poiLayer) {
+ this.poiLayer = poiLayer;
+ }
+}
diff --git a/src/main/java/org/traccar/model/Statistics.java b/src/main/java/org/traccar/model/Statistics.java
new file mode 100644
index 000000000..cb72c91dd
--- /dev/null
+++ b/src/main/java/org/traccar/model/Statistics.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import java.util.Date;
+
+public class Statistics extends ExtendedModel {
+
+ private Date captureTime;
+
+ public Date getCaptureTime() {
+ return captureTime;
+ }
+
+ public void setCaptureTime(Date captureTime) {
+ this.captureTime = captureTime;
+ }
+
+ private int activeUsers;
+
+ public int getActiveUsers() {
+ return activeUsers;
+ }
+
+ public void setActiveUsers(int activeUsers) {
+ this.activeUsers = activeUsers;
+ }
+
+ private int activeDevices;
+
+ public int getActiveDevices() {
+ return activeDevices;
+ }
+
+ public void setActiveDevices(int activeDevices) {
+ this.activeDevices = activeDevices;
+ }
+
+ private int requests;
+
+ public int getRequests() {
+ return requests;
+ }
+
+ public void setRequests(int requests) {
+ this.requests = requests;
+ }
+
+ private int messagesReceived;
+
+ public int getMessagesReceived() {
+ return messagesReceived;
+ }
+
+ public void setMessagesReceived(int messagesReceived) {
+ this.messagesReceived = messagesReceived;
+ }
+
+ private int messagesStored;
+
+ public int getMessagesStored() {
+ return messagesStored;
+ }
+
+ public void setMessagesStored(int messagesStored) {
+ this.messagesStored = messagesStored;
+ }
+
+ private int mailSent;
+
+ public int getMailSent() {
+ return mailSent;
+ }
+
+ public void setMailSent(int mailSent) {
+ this.mailSent = mailSent;
+ }
+
+ private int smsSent;
+
+ public int getSmsSent() {
+ return smsSent;
+ }
+
+ public void setSmsSent(int smsSent) {
+ this.smsSent = smsSent;
+ }
+
+ private int geocoderRequests;
+
+ public int getGeocoderRequests() {
+ return geocoderRequests;
+ }
+
+ public void setGeocoderRequests(int geocoderRequests) {
+ this.geocoderRequests = geocoderRequests;
+ }
+
+ private int geolocationRequests;
+
+ public int getGeolocationRequests() {
+ return geolocationRequests;
+ }
+
+ public void setGeolocationRequests(int geolocationRequests) {
+ this.geolocationRequests = geolocationRequests;
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/Typed.java b/src/main/java/org/traccar/model/Typed.java
new file mode 100644
index 000000000..313ec7bcd
--- /dev/null
+++ b/src/main/java/org/traccar/model/Typed.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 Gabor Somogyi (gabor.g.somogyi@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+public class Typed {
+
+ private String type;
+
+ public Typed(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+}
diff --git a/src/main/java/org/traccar/model/User.java b/src/main/java/org/traccar/model/User.java
new file mode 100644
index 000000000..976b6aac0
--- /dev/null
+++ b/src/main/java/org/traccar/model/User.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import org.traccar.database.QueryExtended;
+import org.traccar.database.QueryIgnore;
+import org.traccar.helper.Hashing;
+
+import java.util.Date;
+
+public class User extends ExtendedModel {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ private String login;
+
+ public String getLogin() {
+ return login;
+ }
+
+ public void setLogin(String login) {
+ this.login = login;
+ }
+
+ private String email;
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email.trim();
+ }
+
+ private String phone;
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ private boolean readonly;
+
+ public boolean getReadonly() {
+ return readonly;
+ }
+
+ public void setReadonly(boolean readonly) {
+ this.readonly = readonly;
+ }
+
+ private boolean administrator;
+
+ public boolean getAdministrator() {
+ return administrator;
+ }
+
+ public void setAdministrator(boolean administrator) {
+ this.administrator = administrator;
+ }
+
+ private String map;
+
+ public String getMap() {
+ return map;
+ }
+
+ public void setMap(String map) {
+ this.map = map;
+ }
+
+ private double latitude;
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = latitude;
+ }
+
+ private double longitude;
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = longitude;
+ }
+
+ private int zoom;
+
+ public int getZoom() {
+ return zoom;
+ }
+
+ public void setZoom(int zoom) {
+ this.zoom = zoom;
+ }
+
+ private boolean twelveHourFormat;
+
+ public boolean getTwelveHourFormat() {
+ return twelveHourFormat;
+ }
+
+ public void setTwelveHourFormat(boolean twelveHourFormat) {
+ this.twelveHourFormat = twelveHourFormat;
+ }
+
+ private String coordinateFormat;
+
+ public String getCoordinateFormat() {
+ return coordinateFormat;
+ }
+
+ public void setCoordinateFormat(String coordinateFormat) {
+ this.coordinateFormat = coordinateFormat;
+ }
+
+ private boolean disabled;
+
+ public boolean getDisabled() {
+ return disabled;
+ }
+
+ public void setDisabled(boolean disabled) {
+ this.disabled = disabled;
+ }
+
+ private Date expirationTime;
+
+ public Date getExpirationTime() {
+ return expirationTime;
+ }
+
+ public void setExpirationTime(Date expirationTime) {
+ this.expirationTime = expirationTime;
+ }
+
+ private int deviceLimit;
+
+ public int getDeviceLimit() {
+ return deviceLimit;
+ }
+
+ public void setDeviceLimit(int deviceLimit) {
+ this.deviceLimit = deviceLimit;
+ }
+
+ private int userLimit;
+
+ public int getUserLimit() {
+ return userLimit;
+ }
+
+ public void setUserLimit(int userLimit) {
+ this.userLimit = userLimit;
+ }
+
+ private boolean deviceReadonly;
+
+ public boolean getDeviceReadonly() {
+ return deviceReadonly;
+ }
+
+ public void setDeviceReadonly(boolean deviceReadonly) {
+ this.deviceReadonly = deviceReadonly;
+ }
+
+ private String token;
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ if (token != null && !token.isEmpty()) {
+ if (!token.matches("^[a-zA-Z0-9-]{16,}$")) {
+ throw new IllegalArgumentException("Illegal token");
+ }
+ this.token = token;
+ } else {
+ this.token = null;
+ }
+ }
+
+ private boolean limitCommands;
+
+ public boolean getLimitCommands() {
+ return limitCommands;
+ }
+
+ public void setLimitCommands(boolean limitCommands) {
+ this.limitCommands = limitCommands;
+ }
+
+ private String poiLayer;
+
+ public String getPoiLayer() {
+ return poiLayer;
+ }
+
+ public void setPoiLayer(String poiLayer) {
+ this.poiLayer = poiLayer;
+ }
+
+ @QueryIgnore
+ public String getPassword() {
+ return null;
+ }
+
+ public void setPassword(String password) {
+ if (password != null && !password.isEmpty()) {
+ Hashing.HashingResult hashingResult = Hashing.createHash(password);
+ hashedPassword = hashingResult.getHash();
+ salt = hashingResult.getSalt();
+ }
+ }
+
+ private String hashedPassword;
+
+ @JsonIgnore
+ @QueryExtended
+ public String getHashedPassword() {
+ return hashedPassword;
+ }
+
+ public void setHashedPassword(String hashedPassword) {
+ this.hashedPassword = hashedPassword;
+ }
+
+ private String salt;
+
+ @JsonIgnore
+ @QueryExtended
+ public String getSalt() {
+ return salt;
+ }
+
+ public void setSalt(String salt) {
+ this.salt = salt;
+ }
+
+ public boolean isPasswordValid(String password) {
+ return Hashing.validatePassword(password, hashedPassword, salt);
+ }
+
+}
diff --git a/src/main/java/org/traccar/model/WifiAccessPoint.java b/src/main/java/org/traccar/model/WifiAccessPoint.java
new file mode 100644
index 000000000..87a77f3c0
--- /dev/null
+++ b/src/main/java/org/traccar/model/WifiAccessPoint.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class WifiAccessPoint {
+
+ public static WifiAccessPoint from(String macAddress, int signalStrength) {
+ WifiAccessPoint wifiAccessPoint = new WifiAccessPoint();
+ wifiAccessPoint.setMacAddress(macAddress);
+ wifiAccessPoint.setSignalStrength(signalStrength);
+ return wifiAccessPoint;
+ }
+
+ public static WifiAccessPoint from(String macAddress, int signalStrength, int channel) {
+ WifiAccessPoint wifiAccessPoint = from(macAddress, signalStrength);
+ wifiAccessPoint.setChannel(channel);
+ return wifiAccessPoint;
+ }
+
+ private String macAddress;
+
+ public String getMacAddress() {
+ return macAddress;
+ }
+
+ public void setMacAddress(String macAddress) {
+ this.macAddress = macAddress;
+ }
+
+ private Integer signalStrength;
+
+ public Integer getSignalStrength() {
+ return signalStrength;
+ }
+
+ public void setSignalStrength(Integer signalStrength) {
+ this.signalStrength = signalStrength;
+ }
+
+ private Integer channel;
+
+ public Integer getChannel() {
+ return channel;
+ }
+
+ public void setChannel(Integer channel) {
+ this.channel = channel;
+ }
+
+}
diff --git a/src/main/java/org/traccar/notification/EventForwarder.java b/src/main/java/org/traccar/notification/EventForwarder.java
new file mode 100644
index 000000000..c0010ebbd
--- /dev/null
+++ b/src/main/java/org/traccar/notification/EventForwarder.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notification;
+
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Geofence;
+import org.traccar.model.Maintenance;
+import org.traccar.model.Position;
+
+import javax.ws.rs.client.AsyncInvoker;
+import javax.ws.rs.client.Invocation;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class EventForwarder {
+
+ private final String url;
+ private final String header;
+
+ public EventForwarder() {
+ url = Context.getConfig().getString("event.forward.url", "http://localhost/");
+ header = Context.getConfig().getString("event.forward.header");
+ }
+
+ private static final String KEY_POSITION = "position";
+ private static final String KEY_EVENT = "event";
+ private static final String KEY_GEOFENCE = "geofence";
+ private static final String KEY_DEVICE = "device";
+ private static final String KEY_MAINTENANCE = "maintenance";
+ private static final String KEY_USERS = "users";
+
+ public final void forwardEvent(Event event, Position position, Set<Long> users) {
+
+ Invocation.Builder requestBuilder = Context.getClient().target(url).request();
+
+ if (header != null && !header.isEmpty()) {
+ for (String line: header.split("\\r?\\n")) {
+ String[] values = line.split(":", 2);
+ requestBuilder.header(values[0].trim(), values[1].trim());
+ }
+ }
+
+ executeRequest(event, position, users, requestBuilder.async());
+ }
+
+ protected Map<String, Object> preparePayload(Event event, Position position, Set<Long> users) {
+ Map<String, Object> data = new HashMap<>();
+ data.put(KEY_EVENT, event);
+ if (position != null) {
+ data.put(KEY_POSITION, position);
+ }
+ Device device = Context.getIdentityManager().getById(event.getDeviceId());
+ if (device != null) {
+ data.put(KEY_DEVICE, device);
+ }
+ if (event.getGeofenceId() != 0) {
+ Geofence geofence = Context.getGeofenceManager().getById(event.getGeofenceId());
+ if (geofence != null) {
+ data.put(KEY_GEOFENCE, geofence);
+ }
+ }
+ if (event.getMaintenanceId() != 0) {
+ Maintenance maintenance = Context.getMaintenancesManager().getById(event.getMaintenanceId());
+ if (maintenance != null) {
+ data.put(KEY_MAINTENANCE, maintenance);
+ }
+ }
+ data.put(KEY_USERS, Context.getUsersManager().getItems(users));
+ return data;
+ }
+
+ protected abstract void executeRequest(
+ Event event, Position position, Set<Long> users, AsyncInvoker invoker);
+
+}
diff --git a/src/main/java/org/traccar/notification/FullMessage.java b/src/main/java/org/traccar/notification/FullMessage.java
new file mode 100644
index 000000000..f66537c6e
--- /dev/null
+++ b/src/main/java/org/traccar/notification/FullMessage.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notification;
+
+public class FullMessage {
+
+ private String subject;
+ private String body;
+
+ public FullMessage(String subject, String body) {
+ this.subject = subject;
+ this.body = body;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public String getBody() {
+ return body;
+ }
+}
diff --git a/src/main/java/org/traccar/notification/JsonTypeEventForwarder.java b/src/main/java/org/traccar/notification/JsonTypeEventForwarder.java
new file mode 100644
index 000000000..55d926fc8
--- /dev/null
+++ b/src/main/java/org/traccar/notification/JsonTypeEventForwarder.java
@@ -0,0 +1,18 @@
+package org.traccar.notification;
+
+import java.util.Set;
+
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+import javax.ws.rs.client.AsyncInvoker;
+import javax.ws.rs.client.Entity;
+
+public class JsonTypeEventForwarder extends EventForwarder {
+
+ @Override
+ protected void executeRequest(Event event, Position position, Set<Long> users, AsyncInvoker invoker) {
+ invoker.post(Entity.json(preparePayload(event, position, users)));
+ }
+
+}
diff --git a/src/main/java/org/traccar/notification/MessageException.java b/src/main/java/org/traccar/notification/MessageException.java
new file mode 100644
index 000000000..710b927b0
--- /dev/null
+++ b/src/main/java/org/traccar/notification/MessageException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notification;
+
+public class MessageException extends Exception {
+
+ public MessageException(Throwable cause) {
+ super(cause);
+ }
+
+ public MessageException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/main/java/org/traccar/notification/NotificationFormatter.java b/src/main/java/org/traccar/notification/NotificationFormatter.java
new file mode 100644
index 000000000..2f8100226
--- /dev/null
+++ b/src/main/java/org/traccar/notification/NotificationFormatter.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notification;
+
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
+import java.util.Locale;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.tools.generic.DateTool;
+import org.apache.velocity.tools.generic.NumberTool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.model.User;
+import org.traccar.reports.ReportUtils;
+
+public final class NotificationFormatter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NotificationFormatter.class);
+
+ private NotificationFormatter() {
+ }
+
+ public static VelocityContext prepareContext(long userId, Event event, Position position) {
+
+ User user = Context.getPermissionsManager().getUser(userId);
+ Device device = Context.getIdentityManager().getById(event.getDeviceId());
+
+ VelocityContext velocityContext = new VelocityContext();
+ velocityContext.put("user", user);
+ velocityContext.put("device", device);
+ velocityContext.put("event", event);
+ if (position != null) {
+ velocityContext.put("position", position);
+ velocityContext.put("speedUnit", ReportUtils.getSpeedUnit(userId));
+ velocityContext.put("distanceUnit", ReportUtils.getDistanceUnit(userId));
+ velocityContext.put("volumeUnit", ReportUtils.getVolumeUnit(userId));
+ }
+ if (event.getGeofenceId() != 0) {
+ velocityContext.put("geofence", Context.getGeofenceManager().getById(event.getGeofenceId()));
+ }
+ if (event.getMaintenanceId() != 0) {
+ velocityContext.put("maintenance", Context.getMaintenancesManager().getById(event.getMaintenanceId()));
+ }
+ String driverUniqueId = event.getString(Position.KEY_DRIVER_UNIQUE_ID);
+ if (driverUniqueId != null) {
+ velocityContext.put("driver", Context.getDriversManager().getDriverByUniqueId(driverUniqueId));
+ }
+ velocityContext.put("webUrl", Context.getVelocityEngine().getProperty("web.url"));
+ velocityContext.put("dateTool", new DateTool());
+ velocityContext.put("numberTool", new NumberTool());
+ velocityContext.put("timezone", ReportUtils.getTimezone(userId));
+ velocityContext.put("locale", Locale.getDefault());
+ return velocityContext;
+ }
+
+ public static Template getTemplate(Event event, String path) {
+
+ String templateFilePath;
+ Template template;
+
+ try {
+ templateFilePath = Paths.get(path, event.getType() + ".vm").toString();
+ template = Context.getVelocityEngine().getTemplate(templateFilePath, StandardCharsets.UTF_8.name());
+ } catch (ResourceNotFoundException error) {
+ LOGGER.warn("Notification template error", error);
+ templateFilePath = Paths.get(path, "unknown.vm").toString();
+ template = Context.getVelocityEngine().getTemplate(templateFilePath, StandardCharsets.UTF_8.name());
+ }
+ return template;
+ }
+
+ public static FullMessage formatFullMessage(long userId, Event event, Position position) {
+ VelocityContext velocityContext = prepareContext(userId, event, position);
+ String formattedMessage = formatMessage(velocityContext, userId, event, position, "full");
+
+ return new FullMessage((String) velocityContext.get("subject"), formattedMessage);
+ }
+
+ public static String formatShortMessage(long userId, Event event, Position position) {
+ return formatMessage(null, userId, event, position, "short");
+ }
+
+ private static String formatMessage(VelocityContext vc, Long userId, Event event, Position position,
+ String templatePath) {
+
+ VelocityContext velocityContext = vc;
+ if (velocityContext == null) {
+ velocityContext = prepareContext(userId, event, position);
+ }
+ StringWriter writer = new StringWriter();
+ getTemplate(event, templatePath).merge(velocityContext, writer);
+
+ return writer.toString();
+ }
+
+}
diff --git a/src/main/java/org/traccar/notification/NotificatorManager.java b/src/main/java/org/traccar/notification/NotificatorManager.java
new file mode 100644
index 000000000..a4080a38d
--- /dev/null
+++ b/src/main/java/org/traccar/notification/NotificatorManager.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notification;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Typed;
+import org.traccar.notificators.NotificatorFirebase;
+import org.traccar.notificators.NotificatorMail;
+import org.traccar.notificators.NotificatorNull;
+import org.traccar.notificators.Notificator;
+import org.traccar.notificators.NotificatorSms;
+import org.traccar.notificators.NotificatorWeb;
+
+public final class NotificatorManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NotificatorManager.class);
+
+ private static final Notificator NULL_NOTIFICATOR = new NotificatorNull();
+
+ private final Map<String, Notificator> notificators = new HashMap<>();
+
+ public NotificatorManager() {
+ final String[] types = Context.getConfig().getString("notificator.types", "").split(",");
+ for (String type : types) {
+ String defaultNotificator = "";
+ switch (type) {
+ case "web":
+ defaultNotificator = NotificatorWeb.class.getCanonicalName();
+ break;
+ case "mail":
+ defaultNotificator = NotificatorMail.class.getCanonicalName();
+ break;
+ case "sms":
+ defaultNotificator = NotificatorSms.class.getCanonicalName();
+ break;
+ case "firebase":
+ defaultNotificator = NotificatorFirebase.class.getCanonicalName();
+ break;
+ default:
+ break;
+ }
+ final String className = Context.getConfig()
+ .getString("notificator." + type + ".class", defaultNotificator);
+ try {
+ notificators.put(type, (Notificator) Class.forName(className).newInstance());
+ } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
+ LOGGER.warn("Unable to load notificator class for " + type + " " + className + " " + e.getMessage());
+ }
+ }
+ }
+
+ public Notificator getNotificator(String type) {
+ final Notificator notificator = notificators.get(type);
+ if (notificator == null) {
+ LOGGER.warn("No notificator configured for type : " + type);
+ return NULL_NOTIFICATOR;
+ }
+ return notificator;
+ }
+
+ public Set<Typed> getAllNotificatorTypes() {
+ Set<Typed> result = new HashSet<>();
+ for (String notificator : notificators.keySet()) {
+ result.add(new Typed(notificator));
+ }
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/notification/PropertiesProvider.java b/src/main/java/org/traccar/notification/PropertiesProvider.java
new file mode 100644
index 000000000..f0078feef
--- /dev/null
+++ b/src/main/java/org/traccar/notification/PropertiesProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notification;
+
+import org.traccar.config.Config;
+import org.traccar.model.ExtendedModel;
+
+public class PropertiesProvider {
+
+ private Config config;
+
+ private ExtendedModel extendedModel;
+
+ public PropertiesProvider(Config config) {
+ this.config = config;
+ }
+
+ public PropertiesProvider(ExtendedModel extendedModel) {
+ this.extendedModel = extendedModel;
+ }
+
+ public String getString(String key) {
+ if (config != null) {
+ return config.getString(key);
+ } else {
+ return extendedModel.getString(key);
+ }
+ }
+
+ public String getString(String key, String defaultValue) {
+ String value = getString(key);
+ if (value == null) {
+ value = defaultValue;
+ }
+ return value;
+ }
+
+ public int getInteger(String key, int defaultValue) {
+ if (config != null) {
+ return config.getInteger(key, defaultValue);
+ } else {
+ Object result = extendedModel.getAttributes().get(key);
+ if (result != null) {
+ return result instanceof String ? Integer.parseInt((String) result) : (Integer) result;
+ } else {
+ return defaultValue;
+ }
+ }
+ }
+
+ public Boolean getBoolean(String key) {
+ if (config != null) {
+ if (config.hasKey(key)) {
+ return config.getBoolean(key);
+ } else {
+ return null;
+ }
+ } else {
+ Object result = extendedModel.getAttributes().get(key);
+ if (result != null) {
+ return result instanceof String ? Boolean.valueOf((String) result) : (Boolean) result;
+ } else {
+ return null;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/notificators/Notificator.java b/src/main/java/org/traccar/notificators/Notificator.java
new file mode 100644
index 000000000..5e40971c6
--- /dev/null
+++ b/src/main/java/org/traccar/notificators/Notificator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notificators;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.notification.MessageException;
+
+public abstract class Notificator {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Notificator.class);
+
+ public void sendAsync(final long userId, final Event event, final Position position) {
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ sendSync(userId, event, position);
+ } catch (MessageException | InterruptedException error) {
+ LOGGER.warn("Event send error", error);
+ }
+ }
+ }).start();
+ }
+
+ public abstract void sendSync(long userId, Event event, Position position)
+ throws MessageException, InterruptedException;
+
+}
diff --git a/src/main/java/org/traccar/notificators/NotificatorFirebase.java b/src/main/java/org/traccar/notificators/NotificatorFirebase.java
new file mode 100644
index 000000000..75d325de2
--- /dev/null
+++ b/src/main/java/org/traccar/notificators/NotificatorFirebase.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notificators;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.model.User;
+import org.traccar.notification.NotificationFormatter;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.InvocationCallback;
+
+public class NotificatorFirebase extends Notificator {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NotificatorFirebase.class);
+
+ private static final String URL = "https://fcm.googleapis.com/fcm/send";
+
+ private String key;
+
+ public static class Notification {
+ @JsonProperty("body")
+ private String body;
+ }
+
+ public static class Message {
+ @JsonProperty("registration_ids")
+ private String[] tokens;
+ @JsonProperty("notification")
+ private Notification notification;
+ }
+
+ public NotificatorFirebase() {
+ key = Context.getConfig().getString("notificator.firebase.key");
+ }
+
+ @Override
+ public void sendSync(long userId, Event event, Position position) {
+ final User user = Context.getPermissionsManager().getUser(userId);
+ if (user.getAttributes().containsKey("notificationTokens")) {
+
+ Notification notification = new Notification();
+ notification.body = NotificationFormatter.formatShortMessage(userId, event, position).trim();
+
+ Message message = new Message();
+ message.tokens = user.getString("notificationTokens").split("[, ]");
+ message.notification = notification;
+
+ Context.getClient().target(URL).request()
+ .header("Authorization", "key=" + key)
+ .async().post(Entity.json(message), new InvocationCallback<Object>() {
+ @Override
+ public void completed(Object o) {
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ LOGGER.warn("Firebase notification error", throwable);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void sendAsync(long userId, Event event, Position position) {
+ sendSync(userId, event, position);
+ }
+
+}
diff --git a/src/main/java/org/traccar/notificators/NotificatorMail.java b/src/main/java/org/traccar/notificators/NotificatorMail.java
new file mode 100644
index 000000000..6b9774c58
--- /dev/null
+++ b/src/main/java/org/traccar/notificators/NotificatorMail.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notificators;
+
+import org.traccar.Context;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.notification.FullMessage;
+import org.traccar.notification.MessageException;
+import org.traccar.notification.NotificationFormatter;
+
+import javax.mail.MessagingException;
+
+public final class NotificatorMail extends Notificator {
+
+ @Override
+ public void sendSync(long userId, Event event, Position position) throws MessageException {
+ try {
+ FullMessage message = NotificationFormatter.formatFullMessage(userId, event, position);
+ Context.getMailManager().sendMessage(userId, message.getSubject(), message.getBody());
+ } catch (MessagingException e) {
+ throw new MessageException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/notificators/NotificatorNull.java b/src/main/java/org/traccar/notificators/NotificatorNull.java
new file mode 100644
index 000000000..9364336be
--- /dev/null
+++ b/src/main/java/org/traccar/notificators/NotificatorNull.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notificators;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+public final class NotificatorNull extends Notificator {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NotificatorNull.class);
+
+ @Override
+ public void sendAsync(long userId, Event event, Position position) {
+ LOGGER.warn("You are using null notificatior, please check your configuration, notification not sent");
+ }
+
+ @Override
+ public void sendSync(long userId, Event event, Position position) {
+ LOGGER.warn("You are using null notificatior, please check your configuration, notification not sent");
+ }
+
+}
diff --git a/src/main/java/org/traccar/notificators/NotificatorSms.java b/src/main/java/org/traccar/notificators/NotificatorSms.java
new file mode 100644
index 000000000..d5c791eae
--- /dev/null
+++ b/src/main/java/org/traccar/notificators/NotificatorSms.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notificators;
+
+import org.traccar.Context;
+import org.traccar.Main;
+import org.traccar.database.StatisticsManager;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.model.User;
+import org.traccar.notification.MessageException;
+import org.traccar.notification.NotificationFormatter;
+import org.traccar.sms.SmsManager;
+
+public final class NotificatorSms extends Notificator {
+
+ private final SmsManager smsManager;
+
+ public NotificatorSms() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
+ final String smsClass = Context.getConfig().getString("notificator.sms.manager.class", "");
+ if (smsClass.length() > 0) {
+ smsManager = (SmsManager) Class.forName(smsClass).newInstance();
+ } else {
+ smsManager = Context.getSmsManager();
+ }
+ }
+
+ @Override
+ public void sendAsync(long userId, Event event, Position position) {
+ final User user = Context.getPermissionsManager().getUser(userId);
+ if (user.getPhone() != null) {
+ Main.getInjector().getInstance(StatisticsManager.class).registerSms();
+ smsManager.sendMessageAsync(user.getPhone(),
+ NotificationFormatter.formatShortMessage(userId, event, position), false);
+ }
+ }
+
+ @Override
+ public void sendSync(long userId, Event event, Position position) throws MessageException, InterruptedException {
+ final User user = Context.getPermissionsManager().getUser(userId);
+ if (user.getPhone() != null) {
+ Main.getInjector().getInstance(StatisticsManager.class).registerSms();
+ smsManager.sendMessageSync(user.getPhone(),
+ NotificationFormatter.formatShortMessage(userId, event, position), false);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/notificators/NotificatorWeb.java b/src/main/java/org/traccar/notificators/NotificatorWeb.java
new file mode 100644
index 000000000..1d11c0b46
--- /dev/null
+++ b/src/main/java/org/traccar/notificators/NotificatorWeb.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notificators;
+
+import org.traccar.Context;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+public final class NotificatorWeb extends Notificator {
+
+ @Override
+ public void sendSync(long userId, Event event, Position position) {
+ Context.getConnectionManager().updateEvent(userId, event);
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AdmProtocol.java b/src/main/java/org/traccar/protocol/AdmProtocol.java
new file mode 100644
index 000000000..08f932ceb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AdmProtocol.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+import java.nio.ByteOrder;
+
+public class AdmProtocol extends BaseProtocol {
+
+ public AdmProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_GET_DEVICE_STATUS,
+ Command.TYPE_CUSTOM);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 2, 1, -3, 0, true));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new AdmProtocolEncoder());
+ pipeline.addLast(new AdmProtocolDecoder(AdmProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AdmProtocolDecoder.java b/src/main/java/org/traccar/protocol/AdmProtocolDecoder.java
new file mode 100644
index 000000000..52d1439ed
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AdmProtocolDecoder.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class AdmProtocolDecoder extends BaseProtocolDecoder {
+
+ public AdmProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int CMD_RESPONSE_SIZE = 0x84;
+ public static final int MSG_IMEI = 0x03;
+ public static final int MSG_PHOTO = 0x0A;
+ public static final int MSG_ADM5 = 0x01;
+
+ private Position decodeData(Channel channel, SocketAddress remoteAddress, ByteBuf buf, int type) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (BitUtil.to(type, 2) == 0) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_VERSION_FW, buf.readUnsignedByte());
+ position.set(Position.KEY_INDEX, buf.readUnsignedShortLE());
+
+ int status = buf.readUnsignedShortLE();
+ position.set(Position.KEY_STATUS, status);
+ position.setValid(!BitUtil.check(status, 5));
+ position.setLatitude(buf.readFloatLE());
+ position.setLongitude(buf.readFloatLE());
+ position.setCourse(buf.readUnsignedShortLE() * 0.1);
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShortLE() * 0.1));
+
+ position.set(Position.KEY_ACCELERATION, buf.readUnsignedByte() * 0.1);
+ position.setAltitude(buf.readShortLE());
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte() * 0.1);
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte() & 0x0f);
+
+ position.setTime(new Date(buf.readUnsignedIntLE() * 1000));
+
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() * 0.001);
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.001);
+
+ if (BitUtil.check(type, 2)) {
+ buf.readUnsignedByte(); // vib
+ buf.readUnsignedByte(); // vib_count
+
+ int out = buf.readUnsignedByte();
+ for (int i = 0; i <= 3; i++) {
+ position.set(Position.PREFIX_OUT + (i + 1), BitUtil.check(out, i) ? 1 : 0);
+ }
+
+ buf.readUnsignedByte(); // in_alarm
+ }
+
+ if (BitUtil.check(type, 3)) {
+ for (int i = 1; i <= 6; i++) {
+ position.set(Position.PREFIX_ADC + i, buf.readUnsignedShortLE() * 0.001);
+ }
+ }
+
+ if (BitUtil.check(type, 4)) {
+ for (int i = 1; i <= 2; i++) {
+ position.set(Position.PREFIX_COUNT + i, buf.readUnsignedIntLE());
+ }
+ }
+
+ if (BitUtil.check(type, 5)) {
+ for (int i = 1; i <= 3; i++) {
+ buf.readUnsignedShortLE(); // fuel level
+ }
+ for (int i = 1; i <= 3; i++) {
+ position.set(Position.PREFIX_TEMP + i, buf.readUnsignedByte());
+ }
+ }
+
+ if (BitUtil.check(type, 6)) {
+ int endIndex = buf.readerIndex() + buf.readUnsignedByte();
+ while (buf.readerIndex() < endIndex) {
+ int mask = buf.readUnsignedByte();
+ long value;
+ switch (BitUtil.from(mask, 6)) {
+ case 3:
+ value = buf.readLongLE();
+ break;
+ case 2:
+ value = buf.readUnsignedIntLE();
+ break;
+ case 1:
+ value = buf.readUnsignedShortLE();
+ break;
+ default:
+ value = buf.readUnsignedByte();
+ break;
+ }
+ int index = BitUtil.to(mask, 6);
+ switch (index) {
+ case 1:
+ position.set(Position.PREFIX_TEMP + 1, value);
+ break;
+ case 2:
+ position.set("humidity", value);
+ break;
+ case 3:
+ position.set("illumination", value);
+ break;
+ case 4:
+ position.set(Position.KEY_BATTERY, value);
+ break;
+ default:
+ position.set("can" + index, value);
+ break;
+ }
+ }
+ }
+
+ if (BitUtil.check(type, 7)) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ }
+
+ return position;
+ }
+
+ return null;
+ }
+
+ private Position parseCommandResponse(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ int responseTextLength = buf.bytesBefore((byte) 0);
+ if (responseTextLength < 0) {
+ responseTextLength = CMD_RESPONSE_SIZE - 3;
+ }
+ position.set(Position.KEY_RESULT, buf.readSlice(responseTextLength).toString(StandardCharsets.UTF_8));
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedShortLE(); // device id
+
+ int size = buf.readUnsignedByte();
+ if (size != CMD_RESPONSE_SIZE) {
+ int type = buf.readUnsignedByte();
+ if (type == MSG_IMEI) {
+ getDeviceSession(channel, remoteAddress, buf.readSlice(15).toString(StandardCharsets.UTF_8));
+ } else {
+ return decodeData(channel, remoteAddress, buf, type);
+ }
+ } else {
+ return parseCommandResponse(channel, remoteAddress, buf);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AdmProtocolEncoder.java b/src/main/java/org/traccar/protocol/AdmProtocolEncoder.java
new file mode 100644
index 000000000..e76bc2ddc
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AdmProtocolEncoder.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Anatoliy Golubev (darth.naihil@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class AdmProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_GET_DEVICE_STATUS:
+ return formatCommand(command, "STATUS\r\n");
+
+ case Command.TYPE_CUSTOM:
+ return formatCommand(command, "{%s}\r\n", Command.KEY_DATA);
+
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AisProtocol.java b/src/main/java/org/traccar/protocol/AisProtocol.java
new file mode 100644
index 000000000..3b9cad7c8
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AisProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AisProtocol extends BaseProtocol {
+
+ public AisProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new AisProtocolDecoder(AisProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AisProtocolDecoder.java b/src/main/java/org/traccar/protocol/AisProtocolDecoder.java
new file mode 100644
index 000000000..8970f3d4a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AisProtocolDecoder.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitBuffer;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class AisProtocolDecoder extends BaseProtocolDecoder {
+
+ public AisProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("!AIVDM,")
+ .number("(d+),") // count
+ .number("(d+),") // index
+ .number("(d+)?,") // id
+ .expression(".,") // radio channel
+ .expression("([^,]+),") // payload
+ .any()
+ .compile();
+
+ private Position decodePayload(Channel channel, SocketAddress remoteAddress, BitBuffer buf) {
+
+ int type = buf.readUnsigned(6);
+ if (type == 1 || type == 2 || type == 3 || type == 18) {
+
+ buf.readUnsigned(2);
+ int mmsi = buf.readUnsigned(30);
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(mmsi));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date());
+
+ if (type == 18) {
+ buf.readUnsigned(8); // reserved
+ } else {
+ position.set(Position.KEY_STATUS, buf.readUnsigned(4));
+ position.set("turn", buf.readSigned(8));
+ }
+
+ position.setSpeed(buf.readUnsigned(10) * 0.1);
+ position.setValid(buf.readUnsigned(1) != 0);
+ position.setLongitude(buf.readSigned(28) * 0.0001 / 60.0);
+ position.setLatitude(buf.readSigned(27) * 0.0001 / 60.0);
+ position.setCourse(buf.readUnsigned(12) * 0.1);
+
+ position.set("heading", buf.readUnsigned(9));
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String[] sentences = ((String) msg).split("\\r\\n");
+
+ List<Position> positions = new ArrayList<>();
+ Map<Integer, BitBuffer> buffers = new HashMap<>();
+
+ for (String sentence : sentences) {
+ if (!sentence.isEmpty()) {
+ Parser parser = new Parser(PATTERN, sentence);
+ if (parser.matches()) {
+
+ int count = parser.nextInt(0);
+ int index = parser.nextInt(0);
+ int id = parser.nextInt(0);
+
+ Position position = null;
+
+ if (count == 1) {
+ BitBuffer bits = new BitBuffer();
+ bits.writeEncoded(parser.next().getBytes(StandardCharsets.US_ASCII));
+ position = decodePayload(channel, remoteAddress, bits);
+ } else {
+ BitBuffer bits = buffers.get(id);
+ if (bits == null) {
+ bits = new BitBuffer();
+ buffers.put(id, bits);
+ }
+ bits.writeEncoded(parser.next().getBytes(StandardCharsets.US_ASCII));
+ if (count == index) {
+ position = decodePayload(channel, remoteAddress, bits);
+ buffers.remove(id);
+ }
+ }
+
+ if (position != null) {
+ positions.add(position);
+ }
+
+ }
+ }
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AlematicsFrameDecoder.java b/src/main/java/org/traccar/protocol/AlematicsFrameDecoder.java
new file mode 100644
index 000000000..be7666657
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AlematicsFrameDecoder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import org.traccar.NetworkMessage;
+
+public class AlematicsFrameDecoder extends LineBasedFrameDecoder {
+
+ private static final int MESSAGE_MINIMUM_LENGTH = 2;
+
+ public AlematicsFrameDecoder(int maxFrameLength) {
+ super(maxFrameLength);
+ }
+
+ // example of heartbeat: FA F8 00 07 00 03 15 AD 4E 78 3A D2
+
+ @Override
+ protected Object decode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < MESSAGE_MINIMUM_LENGTH) {
+ return null;
+ }
+
+ if (buf.getUnsignedShort(buf.readerIndex()) == 0xFAF8) {
+ ByteBuf heartbeat = buf.readRetainedSlice(12);
+ if (ctx != null && ctx.channel() != null) {
+ ctx.channel().writeAndFlush(new NetworkMessage(heartbeat, ctx.channel().remoteAddress()));
+ }
+ }
+
+ return super.decode(ctx, buf);
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AlematicsProtocol.java b/src/main/java/org/traccar/protocol/AlematicsProtocol.java
new file mode 100644
index 000000000..8da2356b9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AlematicsProtocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AlematicsProtocol extends BaseProtocol {
+
+ public AlematicsProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new AlematicsFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new AlematicsProtocolDecoder(AlematicsProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AlematicsProtocolDecoder.java b/src/main/java/org/traccar/protocol/AlematicsProtocolDecoder.java
new file mode 100644
index 000000000..25ccf6856
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AlematicsProtocolDecoder.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class AlematicsProtocolDecoder extends BaseProtocolDecoder {
+
+ public AlematicsProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$T,")
+ .number("(d+),") // type
+ .number("(d+),") // index
+ .number("(d+),") // id
+ .number("(dddd)(dd)(dd)") // gps date
+ .number("(dd)(dd)(dd),") // gps time
+ .number("(dddd)(dd)(dd)") // device date
+ .number("(dd)(dd)(dd),") // device time
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(-?d+),") // altitude
+ .number("(d+.d),") // hdop
+ .number("(d+),") // satellites
+ .number("(d+),") // input
+ .number("(d+),") // output
+ .number("(d+.d+),") // adc
+ .number("(d+.d+),") // power
+ .number("(d+),") // odometer
+ .groupBegin()
+ .text("0,$S,")
+ .expression("(.*)") // text message
+ .or()
+ .number("(d+),") // extra mask
+ .expression("(.*)") // extra data
+ .or()
+ .any()
+ .groupEnd()
+ .compile();
+
+ private void decodeExtras(Position position, Parser parser) {
+
+ int mask = parser.nextInt();
+ String[] data = parser.next().split(",");
+
+ int index = 0;
+
+ if (BitUtil.check(mask, 0)) {
+ index++; // pulse counter 3
+ }
+
+ if (BitUtil.check(mask, 1)) {
+ position.set(Position.KEY_POWER, Integer.parseInt(data[index++]));
+ }
+
+ if (BitUtil.check(mask, 2)) {
+ position.set(Position.KEY_BATTERY, Integer.parseInt(data[index++]));
+ }
+
+ if (BitUtil.check(mask, 3)) {
+ position.set(Position.KEY_OBD_SPEED, Integer.parseInt(data[index++]));
+ }
+
+ if (BitUtil.check(mask, 4)) {
+ position.set(Position.KEY_RPM, Integer.parseInt(data[index++]));
+ }
+
+ if (BitUtil.check(mask, 5)) {
+ position.set(Position.KEY_RSSI, Integer.parseInt(data[index++]));
+ }
+
+ if (BitUtil.check(mask, 6)) {
+ index++; // pulse counter 2
+ }
+
+ if (BitUtil.check(mask, 7)) {
+ index++; // magnetic rotation sensor rpm
+ }
+
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ position.set(Position.KEY_TYPE, parser.nextInt());
+ position.set(Position.KEY_INDEX, parser.nextInt());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setFixTime(parser.nextDateTime());
+ position.setDeviceTime(parser.nextDateTime());
+
+ position.setValid(true);
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt(0)));
+ position.setCourse(parser.nextInt(0));
+ position.setAltitude(parser.nextInt(0));
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_INPUT, parser.nextInt());
+ position.set(Position.KEY_OUTPUT, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.KEY_POWER, parser.nextDouble());
+ position.set(Position.KEY_ODOMETER, parser.nextInt());
+
+ if (parser.hasNext()) {
+ position.set("text", parser.next());
+ } else if (parser.hasNext()) {
+ decodeExtras(position, parser);
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AnytrekProtocol.java b/src/main/java/org/traccar/protocol/AnytrekProtocol.java
new file mode 100644
index 000000000..4ab5833f7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AnytrekProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AnytrekProtocol extends BaseProtocol {
+
+ public AnytrekProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 2, 2, 2, 0));
+ pipeline.addLast(new AnytrekProtocolDecoder(AnytrekProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AnytrekProtocolDecoder.java b/src/main/java/org/traccar/protocol/AnytrekProtocolDecoder.java
new file mode 100644
index 000000000..c48f59c90
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AnytrekProtocolDecoder.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class AnytrekProtocolDecoder extends BaseProtocolDecoder {
+
+ public AnytrekProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress, int type) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeShort(0x7878);
+ response.writeShortLE(1 + 1 + 2 + 1 + 2); // length
+ response.writeByte(type);
+ response.writeByte(0); // error
+ response.writeShortLE(0); // report interval
+ response.writeByte(0); // clear alarm
+ response.writeShortLE(0); // checksum
+ response.writeByte('\r');
+ response.writeByte('\n');
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ buf.readUnsignedShortLE(); // size
+ int type = buf.readUnsignedByte();
+
+ String imei = ByteBufUtil.hexDump(buf.readSlice(8)).substring(2);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_VERSION_FW, buf.readUnsignedShortLE());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.01);
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_SATELLITES, BitUtil.to(buf.readUnsignedByte(), 4));
+
+ double latitude = buf.readUnsignedIntLE() / 1800000.0;
+ double longitude = buf.readUnsignedIntLE() / 1800000.0;
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+
+ int flags = buf.readUnsignedShortLE();
+ position.setCourse(BitUtil.to(flags, 10));
+ position.setValid(BitUtil.check(flags, 12));
+
+ if (!BitUtil.check(flags, 10)) {
+ latitude = -latitude;
+ }
+ if (BitUtil.check(flags, 11)) {
+ longitude = -longitude;
+ }
+
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+
+ buf.readUnsignedIntLE(); // info index
+ buf.readUnsignedIntLE(); // setting index
+
+ flags = buf.readUnsignedByte();
+ position.set(Position.KEY_CHARGE, BitUtil.check(flags, 0));
+ position.set(Position.KEY_IGNITION, BitUtil.check(flags, 1));
+ position.set(Position.KEY_ALARM, BitUtil.check(flags, 4) ? Position.ALARM_GENERAL : null);
+
+ buf.readUnsignedShortLE(); // charge current
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+
+ sendResponse(channel, remoteAddress, type);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ApelProtocol.java b/src/main/java/org/traccar/protocol/ApelProtocol.java
new file mode 100644
index 000000000..382aa16af
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ApelProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import java.nio.ByteOrder;
+public class ApelProtocol extends BaseProtocol {
+
+ public ApelProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 2, 2, 4, 0, true));
+ pipeline.addLast(new ApelProtocolDecoder(ApelProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ApelProtocolDecoder.java b/src/main/java/org/traccar/protocol/ApelProtocolDecoder.java
new file mode 100644
index 000000000..c95a0366a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ApelProtocolDecoder.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class ApelProtocolDecoder extends BaseProtocolDecoder {
+
+ private long lastIndex;
+ private long newIndex;
+
+ public ApelProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final short MSG_NULL = 0;
+ public static final short MSG_REQUEST_TRACKER_ID = 10;
+ public static final short MSG_TRACKER_ID = 11;
+ public static final short MSG_TRACKER_ID_EXT = 12;
+ public static final short MSG_DISCONNECT = 20;
+ public static final short MSG_REQUEST_PASSWORD = 30;
+ public static final short MSG_PASSWORD = 31;
+ public static final short MSG_REQUEST_STATE_FULL_INFO = 90;
+ public static final short MSG_STATE_FULL_INFO_T104 = 92;
+ public static final short MSG_REQUEST_CURRENT_GPS_DATA = 100;
+ public static final short MSG_CURRENT_GPS_DATA = 101;
+ public static final short MSG_REQUEST_SENSORS_STATE = 110;
+ public static final short MSG_SENSORS_STATE = 111;
+ public static final short MSG_SENSORS_STATE_T100 = 112;
+ public static final short MSG_SENSORS_STATE_T100_4 = 113;
+ public static final short MSG_REQUEST_LAST_LOG_INDEX = 120;
+ public static final short MSG_LAST_LOG_INDEX = 121;
+ public static final short MSG_REQUEST_LOG_RECORDS = 130;
+ public static final short MSG_LOG_RECORDS = 131;
+ public static final short MSG_EVENT = 141;
+ public static final short MSG_TEXT = 150;
+ public static final short MSG_ACK_ALARM = 160;
+ public static final short MSG_SET_TRACKER_MODE = 170;
+ public static final short MSG_GPRS_COMMAND = 180;
+
+ private void sendSimpleMessage(Channel channel, short type) {
+ ByteBuf request = Unpooled.buffer(8);
+ request.writeShortLE(type);
+ request.writeShortLE(0);
+ request.writeIntLE(Checksum.crc32(request.nioBuffer(0, 4)));
+ channel.writeAndFlush(new NetworkMessage(request, channel.remoteAddress()));
+ }
+
+ private void requestArchive(Channel channel) {
+ if (lastIndex == 0) {
+ lastIndex = newIndex;
+ } else if (newIndex > lastIndex) {
+ ByteBuf request = Unpooled.buffer(14);
+ request.writeShortLE(MSG_REQUEST_LOG_RECORDS);
+ request.writeShortLE(6);
+ request.writeIntLE((int) lastIndex);
+ request.writeShortLE(512);
+ request.writeIntLE(Checksum.crc32(request.nioBuffer(0, 10)));
+ channel.writeAndFlush(new NetworkMessage(request, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+ int type = buf.readUnsignedShortLE();
+ boolean alarm = (type & 0x8000) != 0;
+ type = type & 0x7FFF;
+ buf.readUnsignedShortLE(); // length
+
+ if (alarm) {
+ sendSimpleMessage(channel, MSG_ACK_ALARM);
+ }
+
+ if (type == MSG_TRACKER_ID) {
+ return null; // unsupported authentication type
+ }
+
+ if (type == MSG_TRACKER_ID_EXT) {
+
+ buf.readUnsignedIntLE(); // id
+ int length = buf.readUnsignedShortLE();
+ buf.skipBytes(length);
+ length = buf.readUnsignedShortLE();
+ getDeviceSession(channel, remoteAddress, buf.readSlice(length).toString(StandardCharsets.US_ASCII));
+
+ } else if (type == MSG_LAST_LOG_INDEX) {
+
+ long index = buf.readUnsignedIntLE();
+ if (index > 0) {
+ newIndex = index;
+ requestArchive(channel);
+ }
+
+ } else if (type == MSG_CURRENT_GPS_DATA || type == MSG_STATE_FULL_INFO_T104 || type == MSG_LOG_RECORDS) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ int recordCount = 1;
+ if (type == MSG_LOG_RECORDS) {
+ recordCount = buf.readUnsignedShortLE();
+ }
+
+ for (int j = 0; j < recordCount; j++) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int subtype = type;
+ if (type == MSG_LOG_RECORDS) {
+ position.set(Position.KEY_ARCHIVE, true);
+ lastIndex = buf.readUnsignedIntLE() + 1;
+ position.set(Position.KEY_INDEX, lastIndex);
+
+ subtype = buf.readUnsignedShortLE();
+ if (subtype != MSG_CURRENT_GPS_DATA && subtype != MSG_STATE_FULL_INFO_T104) {
+ buf.skipBytes(buf.readUnsignedShortLE());
+ continue;
+ }
+ buf.readUnsignedShortLE(); // length
+ }
+
+ position.setTime(new Date(buf.readUnsignedIntLE() * 1000));
+ position.setLatitude(buf.readIntLE() * 180.0 / 0x7FFFFFFF);
+ position.setLongitude(buf.readIntLE() * 180.0 / 0x7FFFFFFF);
+
+ if (subtype == MSG_STATE_FULL_INFO_T104) {
+ int speed = buf.readUnsignedByte();
+ position.setValid(speed != 255);
+ position.setSpeed(UnitsConverter.knotsFromKph(speed));
+ position.set(Position.KEY_HDOP, buf.readByte());
+ } else {
+ int speed = buf.readShortLE();
+ position.setValid(speed != -1);
+ position.setSpeed(UnitsConverter.knotsFromKph(speed * 0.01));
+ }
+
+ position.setCourse(buf.readShortLE() * 0.01);
+ position.setAltitude(buf.readShortLE());
+
+ if (subtype == MSG_STATE_FULL_INFO_T104) {
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ position.set(Position.KEY_EVENT, buf.readUnsignedShortLE());
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ position.set(Position.KEY_INPUT, buf.readUnsignedByte());
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedByte());
+
+ for (int i = 1; i <= 8; i++) {
+ position.set(Position.PREFIX_ADC + i, buf.readUnsignedShortLE());
+ }
+
+ position.set(Position.PREFIX_COUNT + 1, buf.readUnsignedIntLE());
+ position.set(Position.PREFIX_COUNT + 2, buf.readUnsignedIntLE());
+ position.set(Position.PREFIX_COUNT + 3, buf.readUnsignedIntLE());
+ }
+
+ positions.add(position);
+ }
+
+ buf.readUnsignedIntLE(); // crc
+
+ if (type == MSG_LOG_RECORDS) {
+ requestArchive(channel);
+ } else {
+ sendSimpleMessage(channel, MSG_REQUEST_LAST_LOG_INDEX);
+ }
+
+ return positions;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AplicomFrameDecoder.java b/src/main/java/org/traccar/protocol/AplicomFrameDecoder.java
new file mode 100644
index 000000000..56fca27c7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AplicomFrameDecoder.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2013 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class AplicomFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ // Skip Alive message
+ while (buf.isReadable() && Character.isDigit(buf.getByte(buf.readerIndex()))) {
+ buf.readByte();
+ }
+
+ // Check minimum length
+ if (buf.readableBytes() < 11) {
+ return null;
+ }
+
+ // Read flags
+ int version = buf.getUnsignedByte(buf.readerIndex() + 1);
+ int offset = 1 + 1 + 3;
+ if ((version & 0x80) != 0) {
+ offset += 4;
+ }
+
+ // Get data length
+ int length = buf.getUnsignedShort(buf.readerIndex() + offset);
+ offset += 2;
+ if ((version & 0x40) != 0) {
+ offset += 3;
+ }
+ length += offset; // add header
+
+ // Return buffer
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AplicomProtocol.java b/src/main/java/org/traccar/protocol/AplicomProtocol.java
new file mode 100644
index 000000000..2b9dbf97c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AplicomProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AplicomProtocol extends BaseProtocol {
+
+ public AplicomProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new AplicomFrameDecoder());
+ pipeline.addLast(new AplicomProtocolDecoder(AplicomProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AplicomProtocolDecoder.java b/src/main/java/org/traccar/protocol/AplicomProtocolDecoder.java
new file mode 100644
index 000000000..215aa0211
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AplicomProtocolDecoder.java
@@ -0,0 +1,702 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Date;
+
+public class AplicomProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AplicomProtocolDecoder.class);
+
+ public AplicomProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final long IMEI_BASE_TC65_V20 = 0x1437207000000L;
+ private static final long IMEI_BASE_TC65_V28 = 358244010000000L;
+ private static final long IMEI_BASE_TC65I_V11 = 0x14143B4000000L;
+
+ private static boolean validateImei(long imei) {
+ return Checksum.luhn(imei / 10) == imei % 10;
+ }
+
+ private static long imeiFromUnitId(long unitId) {
+
+ if (unitId == 0) {
+
+ return 0;
+
+ } else {
+
+ // Try TC65i
+ long imei = IMEI_BASE_TC65I_V11 + unitId;
+ if (validateImei(imei)) {
+ return imei;
+ }
+
+ // Try TC65 v2.8
+ imei = IMEI_BASE_TC65_V28 + ((unitId + 0xA8180) & 0xFFFFFF);
+ if (validateImei(imei)) {
+ return imei;
+ }
+
+ // Try TC65 v2.0
+ imei = IMEI_BASE_TC65_V20 + unitId;
+ if (validateImei(imei)) {
+ return imei;
+ }
+
+ }
+
+ return unitId;
+ }
+
+ private static final int DEFAULT_SELECTOR_D = 0x0002fC;
+ private static final int DEFAULT_SELECTOR_E = 0x007ffc;
+ private static final int DEFAULT_SELECTOR_F = 0x0007fd;
+
+ private static final int EVENT_DATA = 119;
+
+ private void decodeEventData(Position position, ByteBuf buf, int event) {
+ switch (event) {
+ case 2:
+ case 40:
+ buf.readUnsignedByte();
+ break;
+ case 9:
+ buf.readUnsignedMedium();
+ break;
+ case 31:
+ case 32:
+ buf.readUnsignedShort();
+ break;
+ case 38:
+ buf.skipBytes(4 * 9);
+ break;
+ case 113:
+ buf.readUnsignedInt();
+ buf.readUnsignedByte();
+ break;
+ case 119:
+ position.set("eventData", ByteBufUtil.hexDump(
+ buf, buf.readerIndex(), Math.min(buf.readableBytes(), 1024)));
+ break;
+ case 121:
+ case 142:
+ buf.readLong();
+ break;
+ case 130:
+ buf.readUnsignedInt(); // incorrect
+ break;
+ case 188:
+ decodeEB(position, buf);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void decodeCanData(ByteBuf buf, Position position) {
+
+ buf.readUnsignedMedium(); // packet identifier
+ position.set(Position.KEY_VERSION_FW, buf.readUnsignedByte());
+ int count = buf.readUnsignedByte();
+ buf.readUnsignedByte(); // batch count
+ buf.readUnsignedShort(); // selector bit
+ buf.readUnsignedInt(); // timestamp
+
+ buf.skipBytes(8);
+
+ ArrayList<ByteBuf> values = new ArrayList<>(count);
+
+ for (int i = 0; i < count; i++) {
+ values.add(buf.readSlice(8));
+ }
+
+ for (int i = 0; i < count; i++) {
+ ByteBuf value = values.get(i);
+ switch (buf.readInt()) {
+ case 0x20D:
+ position.set(Position.KEY_RPM, value.readShortLE());
+ position.set("dieselTemperature", value.readShortLE() * 0.1);
+ position.set("batteryVoltage", value.readShortLE() * 0.01);
+ position.set("supplyAirTempDep1", value.readShortLE() * 0.1);
+ break;
+ case 0x30D:
+ position.set("activeAlarm", ByteBufUtil.hexDump(value));
+ break;
+ case 0x40C:
+ position.set("airTempDep1", value.readShortLE() * 0.1);
+ position.set("airTempDep2", value.readShortLE() * 0.1);
+ break;
+ case 0x40D:
+ position.set("coldUnitState", ByteBufUtil.hexDump(value));
+ break;
+ case 0x50C:
+ position.set("defrostTempDep1", value.readShortLE() * 0.1);
+ position.set("defrostTempDep2", value.readShortLE() * 0.1);
+ break;
+ case 0x50D:
+ position.set("condenserPressure", value.readShortLE() * 0.1);
+ position.set("suctionPressure", value.readShortLE() * 0.1);
+ break;
+ case 0x58C:
+ value.readByte();
+ value.readShort(); // index
+ switch (value.readByte()) {
+ case 0x01:
+ position.set("setpointZone1", value.readIntLE() * 0.1);
+ break;
+ case 0x02:
+ position.set("setpointZone2", value.readIntLE() * 0.1);
+ break;
+ case 0x05:
+ position.set("unitType", value.readIntLE());
+ break;
+ case 0x13:
+ position.set("dieselHours", value.readIntLE() / 60 / 60);
+ break;
+ case 0x14:
+ position.set("electricHours", value.readIntLE() / 60 / 60);
+ break;
+ case 0x17:
+ position.set("serviceIndicator", value.readIntLE());
+ break;
+ case 0x18:
+ position.set("softwareVersion", value.readIntLE() * 0.01);
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ LOGGER.warn("Aplicom CAN decoding error", new UnsupportedOperationException());
+ break;
+ }
+ }
+ }
+
+ private void decodeD(Position position, ByteBuf buf, int selector, int event) {
+
+ if ((selector & 0x0008) != 0) {
+ position.setValid((buf.readUnsignedByte() & 0x40) != 0);
+ } else {
+ getLastLocation(position, null);
+ }
+
+ if ((selector & 0x0004) != 0) {
+ position.setDeviceTime(new Date(buf.readUnsignedInt() * 1000));
+ }
+
+ if ((selector & 0x0008) != 0) {
+ position.setFixTime(new Date(buf.readUnsignedInt() * 1000));
+ if (position.getDeviceTime() == null) {
+ position.setDeviceTime(position.getFixTime());
+ }
+ position.setLatitude(buf.readInt() / 1000000.0);
+ position.setLongitude(buf.readInt() / 1000000.0);
+ position.set(Position.KEY_SATELLITES_VISIBLE, buf.readUnsignedByte());
+ }
+
+ if ((selector & 0x0010) != 0) {
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.set("maximumSpeed", buf.readUnsignedByte());
+ position.setCourse(buf.readUnsignedByte() * 2.0);
+ }
+
+ if ((selector & 0x0040) != 0) {
+ position.set(Position.KEY_INPUT, buf.readUnsignedByte());
+ }
+
+ if ((selector & 0x0020) != 0) {
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 3, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 4, buf.readUnsignedShort());
+ }
+
+ if ((selector & 0x8000) != 0) {
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.001);
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.001);
+ }
+
+ // Pulse rate 1
+ if ((selector & 0x10000) != 0) {
+ buf.readUnsignedShort();
+ buf.readUnsignedInt();
+ }
+
+ // Pulse rate 2
+ if ((selector & 0x20000) != 0) {
+ buf.readUnsignedShort();
+ buf.readUnsignedInt();
+ }
+
+ if ((selector & 0x0080) != 0) {
+ position.set("trip1", buf.readUnsignedInt());
+ }
+
+ if ((selector & 0x0100) != 0) {
+ position.set("trip2", buf.readUnsignedInt());
+ }
+
+ if ((selector & 0x0040) != 0) {
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedByte());
+ }
+
+ if ((selector & 0x0200) != 0) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID,
+ String.valueOf(((long) buf.readUnsignedShort()) << 32) + buf.readUnsignedInt());
+ }
+
+ if ((selector & 0x0400) != 0) {
+ buf.readUnsignedByte(); // Keypad
+ }
+
+ if ((selector & 0x0800) != 0) {
+ position.setAltitude(buf.readShort());
+ }
+
+ if ((selector & 0x2000) != 0) {
+ buf.readUnsignedShort(); // snapshot counter
+ }
+
+ if ((selector & 0x4000) != 0) {
+ buf.skipBytes(8); // state flags
+ }
+
+ if ((selector & 0x80000) != 0) {
+ buf.skipBytes(11); // cell info
+ }
+
+ if ((selector & 0x1000) != 0) {
+ decodeEventData(position, buf, event);
+ }
+
+ if (Context.getConfig().getBoolean(getProtocolName() + ".can")
+ && buf.isReadable() && (selector & 0x1000) != 0 && event == EVENT_DATA) {
+ decodeCanData(buf, position);
+ }
+ }
+
+ private void decodeE(Position position, ByteBuf buf, int selector) {
+
+ if ((selector & 0x0008) != 0) {
+ position.set("tachographEvent", buf.readUnsignedShort());
+ }
+
+ if ((selector & 0x0004) != 0) {
+ getLastLocation(position, new Date(buf.readUnsignedInt() * 1000));
+ } else {
+ getLastLocation(position, null);
+ }
+
+ if ((selector & 0x0010) != 0) {
+ String time = buf.readUnsignedByte() + "s " + buf.readUnsignedByte() + "m " + buf.readUnsignedByte() + "h "
+ + buf.readUnsignedByte() + "M " + buf.readUnsignedByte() + "D " + buf.readUnsignedByte() + "Y "
+ + buf.readByte() + "m " + buf.readByte() + "h";
+ position.set("tachographTime", time);
+ }
+
+ position.set("workState", buf.readUnsignedByte());
+ position.set("driver1State", buf.readUnsignedByte());
+ position.set("driver2State", buf.readUnsignedByte());
+
+ if ((selector & 0x0020) != 0) {
+ position.set("tachographStatus", buf.readUnsignedByte());
+ }
+
+ if ((selector & 0x0040) != 0) {
+ position.set(Position.KEY_OBD_SPEED, buf.readUnsignedShort() / 256.0);
+ }
+
+ if ((selector & 0x0080) != 0) {
+ position.set(Position.KEY_OBD_ODOMETER, buf.readUnsignedInt() * 5);
+ }
+
+ if ((selector & 0x0100) != 0) {
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedInt() * 5);
+ }
+
+ if ((selector & 0x8000) != 0) {
+ position.set("kFactor", buf.readUnsignedShort() * 0.001 + " pulses/m");
+ }
+
+ if ((selector & 0x0200) != 0) {
+ position.set(Position.KEY_RPM, buf.readUnsignedShort() * 0.125);
+ }
+
+ if ((selector & 0x0400) != 0) {
+ position.set("extraInfo", buf.readUnsignedShort());
+ }
+
+ if ((selector & 0x0800) != 0) {
+ position.set(Position.KEY_VIN, buf.readSlice(18).toString(StandardCharsets.US_ASCII).trim());
+ }
+
+ if ((selector & 0x2000) != 0) {
+ buf.readUnsignedByte(); // card 1 type
+ buf.readUnsignedByte(); // card 1 country code
+ String card = buf.readSlice(20).toString(StandardCharsets.US_ASCII).trim();
+ if (!card.isEmpty()) {
+ position.set("card1", card);
+ }
+ }
+
+ if ((selector & 0x4000) != 0) {
+ buf.readUnsignedByte(); // card 2 type
+ buf.readUnsignedByte(); // card 2 country code
+ String card = buf.readSlice(20).toString(StandardCharsets.US_ASCII).trim();
+ if (!card.isEmpty()) {
+ position.set("card2", card);
+ }
+ }
+
+ if ((selector & 0x10000) != 0) {
+ int count = buf.readUnsignedByte();
+ for (int i = 1; i <= count; i++) {
+ position.set("driver" + i, buf.readSlice(22).toString(StandardCharsets.US_ASCII).trim());
+ position.set("driverTime" + i, buf.readUnsignedInt());
+ }
+ }
+ }
+
+ private void decodeH(Position position, ByteBuf buf, int selector) {
+
+ if ((selector & 0x0004) != 0) {
+ getLastLocation(position, new Date(buf.readUnsignedInt() * 1000));
+ } else {
+ getLastLocation(position, null);
+ }
+
+ if ((selector & 0x0040) != 0) {
+ buf.readUnsignedInt(); // reset time
+ }
+
+ if ((selector & 0x2000) != 0) {
+ buf.readUnsignedShort(); // snapshot counter
+ }
+
+ int index = 1;
+ while (buf.readableBytes() > 0) {
+
+ position.set("h" + index + "Index", buf.readUnsignedByte());
+
+ buf.readUnsignedShort(); // length
+
+ int n = buf.readUnsignedByte();
+ int m = buf.readUnsignedByte();
+
+ position.set("h" + index + "XLength", n);
+ position.set("h" + index + "YLength", m);
+
+ if ((selector & 0x0008) != 0) {
+ position.set("h" + index + "XType", buf.readUnsignedByte());
+ position.set("h" + index + "YType", buf.readUnsignedByte());
+ position.set("h" + index + "Parameters", buf.readUnsignedByte());
+ }
+
+ boolean percentageFormat = (selector & 0x0020) != 0;
+
+ StringBuilder data = new StringBuilder();
+ for (int i = 0; i < n * m; i++) {
+ if (percentageFormat) {
+ data.append(buf.readUnsignedByte() * 0.5).append("%").append(" ");
+ } else {
+ data.append(buf.readUnsignedShort()).append(" ");
+ }
+ }
+
+ position.set("h" + index + "Data", data.toString().trim());
+
+ position.set("h" + index + "Total", buf.readUnsignedInt());
+
+ if ((selector & 0x0010) != 0) {
+ int k = buf.readUnsignedByte();
+
+ data = new StringBuilder();
+ for (int i = 1; i < n; i++) {
+ if (k == 1) {
+ data.append(buf.readByte()).append(" ");
+ } else if (k == 2) {
+ data.append(buf.readShort()).append(" ");
+ }
+ }
+ position.set("h" + index + "XLimits", data.toString().trim());
+
+ data = new StringBuilder();
+ for (int i = 1; i < m; i++) {
+ if (k == 1) {
+ data.append(buf.readByte()).append(" ");
+ } else if (k == 2) {
+ data.append(buf.readShort()).append(" ");
+ }
+ }
+ position.set("h" + index + "YLimits", data.toString().trim());
+ }
+
+ index += 1;
+ }
+ }
+
+ private void decodeEB(Position position, ByteBuf buf) {
+
+ if (buf.readByte() != (byte) 'E' || buf.readByte() != (byte) 'B') {
+ return;
+ }
+
+ position.set(Position.KEY_VERSION_FW, buf.readUnsignedByte());
+ position.set(Position.KEY_EVENT, buf.readUnsignedShort());
+ position.set("dataValidity", buf.readUnsignedByte());
+ position.set("towed", buf.readUnsignedByte());
+ buf.readUnsignedShort(); // length
+
+ while (buf.readableBytes() > 0) {
+ buf.readUnsignedByte(); // towed position
+ int type = buf.readUnsignedByte();
+ int length = buf.readUnsignedByte();
+ int end = buf.readerIndex() + length;
+
+ switch (type) {
+ case 0x01:
+ position.set("brakeFlags", ByteBufUtil.hexDump(buf.readSlice(length)));
+ break;
+ case 0x02:
+ position.set("wheelSpeed", buf.readUnsignedShort() / 256.0);
+ position.set("wheelSpeedDifference", buf.readUnsignedShort() / 256.0 - 125.0);
+ position.set("lateralAcceleration", buf.readUnsignedByte() / 10.0 - 12.5);
+ position.set("vehicleSpeed", buf.readUnsignedShort() / 256.0);
+ break;
+ case 0x03:
+ position.set(Position.KEY_AXLE_WEIGHT, buf.readUnsignedShort() * 2);
+ break;
+ case 0x04:
+ position.set("tyrePressure", buf.readUnsignedByte() * 10);
+ position.set("pneumaticPressure", buf.readUnsignedByte() * 5);
+ break;
+ case 0x05:
+ position.set("brakeLining", buf.readUnsignedByte() * 0.4);
+ position.set("brakeTemperature", buf.readUnsignedByte() * 10);
+ break;
+ case 0x06:
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt() * 5L);
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedInt() * 5L);
+ position.set(Position.KEY_ODOMETER_SERVICE, (buf.readUnsignedInt() - 2105540607) * 5L);
+ break;
+ case 0x0A:
+ position.set("absStatusCounter", buf.readUnsignedShort());
+ position.set("atvbStatusCounter", buf.readUnsignedShort());
+ position.set("vdcActiveCounter", buf.readUnsignedShort());
+ break;
+ case 0x0B:
+ position.set("brakeMinMaxData", ByteBufUtil.hexDump(buf.readSlice(length)));
+ break;
+ case 0x0C:
+ position.set("missingPgn", ByteBufUtil.hexDump(buf.readSlice(length)));
+ break;
+ case 0x0D:
+ buf.readUnsignedByte();
+ position.set("towedDetectionStatus", buf.readUnsignedInt());
+ buf.skipBytes(17); // vin
+ break;
+ case 0x0E:
+ default:
+ break;
+ }
+
+ buf.readerIndex(end);
+ }
+ }
+
+ private void decodeF(Position position, ByteBuf buf, int selector) {
+
+ Date deviceTime = null;
+
+ if ((selector & 0x0004) != 0) {
+ deviceTime = new Date(buf.readUnsignedInt() * 1000);
+ }
+
+ getLastLocation(position, deviceTime);
+
+ buf.readUnsignedByte(); // data validity
+
+ if ((selector & 0x0008) != 0) {
+ position.set(Position.KEY_RPM, buf.readUnsignedShort());
+ position.set("rpmMax", buf.readUnsignedShort());
+ position.set("rpmMin", buf.readUnsignedShort());
+ }
+
+ if ((selector & 0x0010) != 0) {
+ position.set("engineTemp", buf.readShort());
+ position.set("engineTempMax", buf.readShort());
+ position.set("engineTempMin", buf.readShort());
+ }
+
+ if ((selector & 0x0020) != 0) {
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromHours(buf.readUnsignedInt()));
+ position.set("serviceDistance", buf.readInt());
+ position.set("driverActivity", buf.readUnsignedByte());
+ position.set(Position.KEY_THROTTLE, buf.readUnsignedByte());
+ position.set(Position.KEY_FUEL_LEVEL, buf.readUnsignedByte());
+ }
+
+ if ((selector & 0x0040) != 0) {
+ position.set(Position.KEY_FUEL_USED, buf.readUnsignedInt());
+ }
+
+ if ((selector & 0x0080) != 0) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+ }
+
+ if ((selector & 0x0100) != 0) {
+ position.set(Position.KEY_OBD_SPEED, buf.readUnsignedByte());
+ position.set("speedMax", buf.readUnsignedByte());
+ position.set("speedMin", buf.readUnsignedByte());
+ position.set("hardBraking", buf.readUnsignedByte());
+ }
+
+ if ((selector & 0x0200) != 0) {
+ position.set("tachographSpeed", buf.readUnsignedByte());
+ position.set("driver1State", buf.readUnsignedByte());
+ position.set("driver2State", buf.readUnsignedByte());
+ position.set("tachographStatus", buf.readUnsignedByte());
+ position.set("overspeedCount", buf.readUnsignedByte());
+ }
+
+ if ((selector & 0x0800) != 0) {
+ position.set(Position.KEY_HOURS, buf.readUnsignedInt() * 0.05);
+ position.set(Position.KEY_RPM, buf.readUnsignedShort() * 0.125);
+ position.set(Position.KEY_OBD_SPEED, buf.readUnsignedShort() / 256.0);
+ position.set(Position.KEY_FUEL_USED, buf.readUnsignedInt() * 0.5);
+ position.set(Position.KEY_FUEL_LEVEL, buf.readUnsignedByte() * 0.4);
+ }
+
+ if ((selector & 0x1000) != 0) {
+ position.set("ambientTemperature", buf.readUnsignedShort() * 0.03125 - 273);
+ buf.readUnsignedShort(); // fuel rate
+ position.set("fuelEconomy", buf.readUnsignedShort() / 512.0);
+ position.set(Position.KEY_FUEL_CONSUMPTION, buf.readUnsignedInt() * 0.001);
+ buf.readUnsignedByte(); // pto drive engagement
+ }
+
+ if ((selector & 0x2000) != 0) {
+ buf.skipBytes(buf.readUnsignedByte()); // driver identification
+ }
+
+ if ((selector & 0x4000) != 0) {
+ position.set("torque", buf.readUnsignedByte());
+ position.set("brakePressure1", buf.readUnsignedByte() * 8);
+ position.set("brakePressure2", buf.readUnsignedByte() * 8);
+ position.set("grossWeight", buf.readUnsignedShort() * 10);
+ position.set("exhaustFluid", buf.readUnsignedByte() * 0.4);
+ buf.readUnsignedByte(); // retarder torque mode
+ position.set("retarderTorque", buf.readUnsignedByte());
+ position.set("retarderSelection", buf.readUnsignedByte() * 0.4);
+ buf.skipBytes(8); // tell tale status block 1
+ buf.skipBytes(8); // tell tale status block 2
+ buf.skipBytes(8); // tell tale status block 3
+ buf.skipBytes(8); // tell tale status block 4
+ }
+
+ if ((selector & 0x8000) != 0) {
+ position.set("parkingBrakeStatus", buf.readUnsignedByte());
+ position.set("doorStatus", buf.readUnsignedByte());
+ buf.skipBytes(8); // status per door
+ position.set("alternatorStatus", buf.readUnsignedByte());
+ position.set("selectedGear", buf.readUnsignedByte());
+ position.set("currentGear", buf.readUnsignedByte());
+ buf.skipBytes(4 * 2); // air suspension pressure
+ }
+
+ if ((selector & 0x0400) != 0) {
+ int count = buf.readUnsignedByte();
+ for (int i = 0; i < count; i++) {
+ position.set("axle" + i, buf.readUnsignedShort());
+ }
+ }
+
+ }
+
+ @Override
+ protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ char protocol = (char) buf.readByte();
+ int version = buf.readUnsignedByte();
+
+ String imei;
+ if ((version & 0x80) != 0) {
+ imei = String.valueOf((buf.readUnsignedInt() << (3 * 8)) | buf.readUnsignedMedium());
+ } else {
+ imei = String.valueOf(imeiFromUnitId(buf.readUnsignedMedium()));
+ }
+
+ buf.readUnsignedShort(); // length
+
+ int selector = DEFAULT_SELECTOR_D;
+ if (protocol == 'E') {
+ selector = DEFAULT_SELECTOR_E;
+ } else if (protocol == 'F') {
+ selector = DEFAULT_SELECTOR_F;
+ }
+ if ((version & 0x40) != 0) {
+ selector = buf.readUnsignedMedium();
+ }
+
+ Position position = new Position(getProtocolName());
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int event = buf.readUnsignedByte();
+ position.set(Position.KEY_EVENT, event);
+ position.set("eventInfo", buf.readUnsignedByte());
+
+ if (protocol == 'D') {
+ decodeD(position, buf, selector, event);
+ } else if (protocol == 'E') {
+ decodeE(position, buf, selector);
+ } else if (protocol == 'H') {
+ decodeH(position, buf, selector);
+ } else if (protocol == 'F') {
+ decodeF(position, buf, selector);
+ } else {
+ return null;
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AppelloProtocol.java b/src/main/java/org/traccar/protocol/AppelloProtocol.java
new file mode 100644
index 000000000..1ca4168e4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AppelloProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AppelloProtocol extends BaseProtocol {
+
+ public AppelloProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new AppelloProtocolDecoder(AppelloProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AppelloProtocolDecoder.java b/src/main/java/org/traccar/protocol/AppelloProtocolDecoder.java
new file mode 100644
index 000000000..47e329234
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AppelloProtocolDecoder.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class AppelloProtocolDecoder extends BaseProtocolDecoder {
+
+ public AppelloProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("FOLLOWIT,") // brand
+ .number("(d+),") // imei
+ .groupBegin()
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .or()
+ .text("UTCTIME,")
+ .groupEnd()
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // satellites
+ .number("(-?d+),") // altitude
+ .expression("([FL]),") // gps state
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String imei = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.hasNext(6)) {
+ position.setTime(parser.nextDateTime());
+ } else {
+ getLastLocation(position, null);
+ }
+
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+
+ position.setAltitude(parser.nextDouble(0));
+
+ position.setValid(parser.next().equals("F"));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AppletProtocol.java b/src/main/java/org/traccar/protocol/AppletProtocol.java
new file mode 100644
index 000000000..2297dca03
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AppletProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AppletProtocol extends BaseProtocol {
+
+ public AppletProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(16384));
+ pipeline.addLast(new AppletProtocolDecoder(AppletProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AppletProtocolDecoder.java b/src/main/java/org/traccar/protocol/AppletProtocolDecoder.java
new file mode 100644
index 000000000..7a995cc24
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AppletProtocolDecoder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.traccar.BaseHttpProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+
+import java.net.SocketAddress;
+
+public class AppletProtocolDecoder extends BaseHttpProtocolDecoder {
+
+ public AppletProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, request.headers().get("From"));
+ if (deviceSession != null) {
+ sendResponse(channel, HttpResponseStatus.OK);
+ } else {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AquilaProtocol.java b/src/main/java/org/traccar/protocol/AquilaProtocol.java
new file mode 100644
index 000000000..5ca1ec091
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AquilaProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AquilaProtocol extends BaseProtocol {
+
+ public AquilaProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new AquilaProtocolDecoder(AquilaProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AquilaProtocolDecoder.java b/src/main/java/org/traccar/protocol/AquilaProtocolDecoder.java
new file mode 100644
index 000000000..57af5e366
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AquilaProtocolDecoder.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class AquilaProtocolDecoder extends BaseProtocolDecoder {
+
+ public AquilaProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_A = new PatternBuilder()
+ .text("$$")
+ .expression("[^,]*,") // client
+ .number("(d+),") // device serial number
+ .number("(d+),") // event
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .groupBegin()
+ .number("(d+),") // gsm
+ .number("(d+),") // speed
+ .number("(d+),") // distance
+ .groupBegin()
+ .number("d+,") // driver code
+ .number("(d+),") // fuel
+ .number("([01]),") // io 1
+ .number("[01],") // case open switch
+ .number("[01],") // over speed start
+ .number("[01],") // over speed end
+ .number("(?:d+,){3}") // reserved
+ .number("([01]),") // power status
+ .number("([01]),") // io 2
+ .number("d+,") // reserved
+ .number("([01]),") // ignition
+ .number("[01],") // ignition off event
+ .number("(?:d+,){7}") // reserved
+ .number("[01],") // corner packet
+ .number("(?:d+,){8}") // reserved
+ .number("([01]),") // course bit 0
+ .number("([01]),") // course bit 1
+ .number("([01]),") // course bit 2
+ .number("([01]),") // course bit 3
+ .or()
+ .number("(d+),") // course
+ .number("(?:d+,){3}") // reserved
+ .number("[01],") // over speed start
+ .number("[01],") // over speed end
+ .number("(?:d+,){3}") // reserved
+ .number("([01]),") // power status
+ .number("(?:d+,){2}") // reserved
+ .number("[01],") // ignition on event
+ .number("([01]),") // ignition
+ .number("[01],") // ignition off event
+ .number("(?:d+,){5}") // reserved
+ .number("[01],") // low battery
+ .number("[01],") // corner packet
+ .number("(?:d+,){6}") // reserved
+ .number("[01],") // hard acceleration
+ .number("[01],") // hard braking
+ .number("[01],[01],[01],[01],") // course bits
+ .number("(d+),") // external voltage
+ .number("(d+),") // internal voltage
+ .number("(?:d+,){6}") // reserved
+ .expression("P([^,]+),") // obd
+ .expression("D([^,]+),") // dtcs
+ .number("-?d+,") // accelerometer x
+ .number("-?d+,") // accelerometer y
+ .number("-?d+,") // accelerometer z
+ .number("d+,") // delta distance
+ .or()
+ .number("(d+),") // course
+ .number("(d+),") // satellites
+ .number("(d+.d+),") // hdop
+ .number("(?:d+,){2}") // reserved
+ .number("(d+),") // adc 1
+ .number("([01]),") // di 1
+ .number("[01],") // case open
+ .number("[01],") // over speed start
+ .number("[01],") // over speed end
+ .number("(?:[01],){2}") // reserved
+ .number("[01],") // immobilizer
+ .number("([01]),") // power status
+ .number("([01]),") // di 2
+ .number("(?:[01],){2}") // reserved
+ .number("([01]),") // ignition
+ .number("(?:[01],){6}") // reserved
+ .number("[01],") // low battery
+ .number("[01],") // corner packet
+ .number("(?:[01],){4}") // reserved
+ .number("[01],") // do 1
+ .number("[01],") // reserved
+ .number("[01],") // hard acceleration
+ .number("[01],") // hard braking
+ .number("(?:[01],){4}") // reserved
+ .number("(d+),") // external voltage
+ .number("(d+),") // internal voltage
+ .groupEnd()
+ .or()
+ .number("(d+),") // sensor id
+ .expression("([^,]+),") // sensor data
+ .groupEnd()
+ .text("*")
+ .number("xx") // checksum
+ .compile();
+
+ private Position decodeA(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_A, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_EVENT, parser.nextInt(0));
+
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(parser.next().equals("A"));
+
+ if (parser.hasNext(3)) {
+ position.set(Position.KEY_RSSI, parser.nextInt(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+ }
+
+ if (parser.hasNext(9)) {
+
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt());
+ position.set(Position.PREFIX_IN + 1, parser.next());
+ position.set(Position.KEY_CHARGE, parser.next().equals("1"));
+ position.set(Position.PREFIX_IN + 2, parser.next());
+
+ position.set(Position.KEY_IGNITION, parser.nextInt(0) == 1);
+
+ int course = (parser.nextInt(0) << 3) + (parser.nextInt(0) << 2)
+ + (parser.nextInt(0) << 1) + parser.nextInt(0);
+ if (course > 0 && course <= 8) {
+ position.setCourse((course - 1) * 45);
+ }
+
+ } else if (parser.hasNext(7)) {
+
+ position.setCourse(parser.nextInt(0));
+
+ position.set(Position.KEY_CHARGE, parser.next().equals("1"));
+ position.set(Position.KEY_IGNITION, parser.nextInt(0) == 1);
+ position.set(Position.KEY_POWER, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY, parser.nextInt(0));
+
+ String obd = parser.next();
+ position.set("obd", obd.substring(1, obd.length() - 1));
+
+ String dtcs = parser.next();
+ position.set(Position.KEY_DTCS, dtcs.substring(1, dtcs.length() - 1).replace('|', ' '));
+
+ } else if (parser.hasNext(10)) {
+
+ position.setCourse(parser.nextInt(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_HDOP, parser.nextDouble(0));
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt(0));
+ position.set(Position.PREFIX_IN + 1, parser.nextInt(0));
+ position.set(Position.KEY_CHARGE, parser.next().equals("1"));
+ position.set(Position.PREFIX_IN + 2, parser.nextInt(0));
+ position.set(Position.KEY_IGNITION, parser.nextInt(0) == 1);
+ position.set(Position.KEY_POWER, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY, parser.nextInt(0));
+
+ } else if (parser.hasNext(2)) {
+
+ position.set("sensorId", parser.nextInt());
+ position.set("sensorData", parser.next());
+
+ }
+
+ return position;
+ }
+
+ private static final Pattern PATTERN_B_1 = new PatternBuilder()
+ .text("$")
+ .expression("[^,]+,") // header
+ .expression("[^,]+,") // client
+ .expression("[^,]+,") // firmware version
+ .expression(".{2},") // packet type
+ .number("d+,") // message id
+ .expression("[LH],") // status
+ .number("(d+),") // imei
+ .expression("[^,]+,") // registration number
+ .number("([01]),") // validity
+ .number("(dd)(dd)(dddd),") // date (ddmmyyyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // latitude
+ .expression("([NS]),")
+ .number("(-?d+.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // satellites
+ .number("(-?d+.d+),") // altitude
+ .number("(d+.d+),") // pdop
+ .number("(d+.d+),") // hdop
+ .expression("[^,]+,") // operator
+ .number("([01]),") // ignition
+ .number("([01]),") // charge
+ .number("(d+.d+),") // power
+ .number("(d+.d+),") // battery
+ .number("([01]),") // emergency
+ .expression("[CO],") // tamper
+ .number("(d+),") // rssi
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(x+),") // lac
+ .number("(x+),") // cid
+ .number("(d+),(x+),(x+),") // cell 1
+ .number("(d+),(x+),(x+),") // cell 2
+ .number("(d+),(x+),(x+),") // cell 3
+ .number("(d+),(x+),(x+),") // cell 4
+ .number("([01])+,") // inputs
+ .number("([01])+,") // outputs
+ .number("d+,") // frame number
+ .number("(d+.d+),") // adc1
+ .number("(d+.d+),") // adc2
+ .number("d+,") // delta distance
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_B_2 = new PatternBuilder()
+ .text("$")
+ .expression("[^,]+,") // header
+ .expression("[^,]+,") // client
+ .expression("(.{3}),") // message type
+ .number("(d+),") // imei
+ .expression(".{2},") // packet type
+ .number("(dd)(dd)(dddd)") // date (ddmmyyyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(-?d+.d+),") // latitude
+ .expression("([NS]),")
+ .number("(-?d+.d+),") // longitude
+ .expression("([EW]),")
+ .number("(-?d+.d+),") // altitude
+ .number("(d+.d+),") // speed
+ .any()
+ .compile();
+
+ private Position decodeB2(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_B_2, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String type = parser.next();
+ String id = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setAltitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+
+ if (type.equals("EMR") && channel != null) {
+ String password = Context.getIdentityManager().lookupAttributeString(
+ deviceSession.getDeviceId(), getProtocolName() + ".password", "aquila123", true);
+ channel.writeAndFlush(new NetworkMessage(
+ "#set$" + id + "@" + password + "#EMR_MODE:0*", remoteAddress));
+ }
+
+ return position;
+ }
+
+ private Position decodeB1(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_B_1, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String id = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.nextInt() == 1);
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setCourse(parser.nextInt());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+
+ position.setAltitude(parser.nextDouble());
+
+ position.set(Position.KEY_PDOP, parser.nextDouble());
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.set(Position.KEY_IGNITION, parser.nextInt() == 1);
+ position.set(Position.KEY_CHARGE, parser.nextInt() == 1);
+ position.set(Position.KEY_POWER, parser.nextDouble());
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+
+ if (parser.nextInt() == 1) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ }
+
+ Network network = new Network();
+
+ int rssi = parser.nextInt();
+ int mcc = parser.nextInt();
+ int mnc = parser.nextInt();
+
+ network.addCellTower(CellTower.from(mcc, mnc, parser.nextHexInt(), parser.nextHexInt(), rssi));
+ for (int i = 0; i < 4; i++) {
+ rssi = parser.nextInt();
+ network.addCellTower(CellTower.from(mcc, mnc, parser.nextHexInt(), parser.nextHexInt(), rssi));
+ }
+
+ position.setNetwork(network);
+
+ position.set(Position.KEY_INPUT, parser.nextBinInt());
+ position.set(Position.KEY_OUTPUT, parser.nextBinInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.PREFIX_ADC + 2, parser.nextDouble());
+
+ return position;
+ }
+
+ private Position decodeB(Channel channel, SocketAddress remoteAddress, String sentence) {
+ if (sentence.contains("EMR") || sentence.contains("SEM")) {
+ return decodeB2(channel, remoteAddress, sentence);
+ } else {
+ return decodeB1(channel, remoteAddress, sentence);
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("$$")) {
+ return decodeA(channel, remoteAddress, sentence);
+ } else {
+ return decodeB(channel, remoteAddress, sentence);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Ardi01Protocol.java b/src/main/java/org/traccar/protocol/Ardi01Protocol.java
new file mode 100644
index 000000000..f7826430f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Ardi01Protocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Ardi01Protocol extends BaseProtocol {
+
+ public Ardi01Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Ardi01ProtocolDecoder(Ardi01Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Ardi01ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Ardi01ProtocolDecoder.java
new file mode 100644
index 000000000..85e9ecfde
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Ardi01ProtocolDecoder.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Ardi01ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Ardi01ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(d+),") // imei
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+.d+),") // latitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(-?d+.?d*),") // altitude
+ .number("(d+),") // satellites
+ .number("(d+),") // event
+ .number("(d+),") // battery
+ .number("(-?d+)") // temperature
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLongitude(parser.nextDouble(0));
+ position.setLatitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ int satellites = parser.nextInt(0);
+ position.setValid(satellites >= 3);
+ position.set(Position.KEY_SATELLITES, satellites);
+
+ position.set(Position.KEY_EVENT, parser.next());
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt(0));
+ position.set(Position.PREFIX_TEMP + 1, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ArknavProtocol.java b/src/main/java/org/traccar/protocol/ArknavProtocol.java
new file mode 100644
index 000000000..3b485e4a5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ArknavProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ArknavProtocol extends BaseProtocol {
+
+ public ArknavProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '\r'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new ArknavProtocolDecoder(ArknavProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ArknavProtocolDecoder.java b/src/main/java/org/traccar/protocol/ArknavProtocolDecoder.java
new file mode 100644
index 000000000..4982e02fc
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ArknavProtocolDecoder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class ArknavProtocolDecoder extends BaseProtocolDecoder {
+
+ public ArknavProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(d+),") // imei
+ .expression(".{6},") // id code
+ .number("ddd,") // status
+ .number("Lddd,") // version
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(d+.?d*),") // hdop
+ .number("(dd):(dd):(dd) ") // time (hh:mm:ss)
+ .number("(dd)-(dd)-(dd),") // date (dd-mm-yy)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_HDOP, parser.nextDouble(0));
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ArknavX8Protocol.java b/src/main/java/org/traccar/protocol/ArknavX8Protocol.java
new file mode 100644
index 000000000..a29bc1ad3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ArknavX8Protocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ArknavX8Protocol extends BaseProtocol {
+
+ public ArknavX8Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, ';'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new ArknavX8ProtocolDecoder(ArknavX8Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ArknavX8ProtocolDecoder.java b/src/main/java/org/traccar/protocol/ArknavX8ProtocolDecoder.java
new file mode 100644
index 000000000..b570f5423
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ArknavX8ProtocolDecoder.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class ArknavX8ProtocolDecoder extends BaseProtocolDecoder {
+
+ public ArknavX8ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_1G = new PatternBuilder()
+ .expression("(..),") // type
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+)([NS]),") // latitude
+ .number("(d+)(dd.d+)([EW]),") // longitude
+ .number("(d+.d+),") // speed
+ .number("(d+),") // course
+ .number("(d+.d+),") // hdop
+ .number("(d+)") // status
+ .compile();
+
+ private static final Pattern PATTERN_2G = new PatternBuilder()
+ .expression("..,") // type
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d+),") // satellites
+ .number("(d+.d+),") // altitude
+ .number("(d+.d+),") // power
+ .number("(d+.d+),") // battery
+ .number("(d+.d+)") // odometer
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.charAt(2) != ',') {
+ getDeviceSession(channel, remoteAddress, sentence.substring(0, 15));
+ return null;
+ }
+
+ switch (sentence.substring(0, 2)) {
+ case "1G":
+ case "1R":
+ case "1M":
+ return decode1G(channel, remoteAddress, sentence);
+ case "2G":
+ return decode2G(channel, remoteAddress, sentence);
+ default:
+ return null;
+ }
+ }
+
+ private Position decode1G(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_1G, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_TYPE, parser.next());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_HDOP, parser.nextDouble(0));
+ position.set(Position.KEY_STATUS, parser.next());
+
+ return position;
+ }
+
+ private Position decode2G(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_2G, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, parser.nextDateTime());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.setAltitude(parser.nextDouble());
+ position.set(Position.KEY_POWER, parser.nextDouble());
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1852 / 3600);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ArnaviProtocol.java b/src/main/java/org/traccar/protocol/ArnaviProtocol.java
new file mode 100644
index 000000000..afe491865
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ArnaviProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ArnaviProtocol extends BaseProtocol {
+
+ public ArnaviProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new ArnaviProtocolDecoder(ArnaviProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ArnaviProtocolDecoder.java b/src/main/java/org/traccar/protocol/ArnaviProtocolDecoder.java
new file mode 100644
index 000000000..7996cf429
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ArnaviProtocolDecoder.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class ArnaviProtocolDecoder extends BaseProtocolDecoder {
+
+ public ArnaviProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$AV,")
+ .number("Vd,") // type
+ .number("(d+),") // device id
+ .number("(d+),") // index
+ .number("(d+),") // power
+ .number("(d+),") // battery
+ .number("-?d+,")
+ .expression("[01],") // movement
+ .expression("([01]),") // ignition
+ .number("(d+),") // input
+ .number("d+,d+,") // input 1
+ .number("d+,d+,").optional() // input 2
+ .expression("[01],") // fix type
+ .number("(d+),") // satellites
+ .groupBegin()
+ .number("(d+.d+)?,") // altitude
+ .number("(?:d+.d+)?,") // geoid height
+ .groupEnd("?")
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(dd)(dd.d+)([NS]),") // latitude
+ .number("(ddd)(dd.d+)([EW]),") // longitude
+ .number("(d+.d+),") // speed
+ .number("(d+.d+),") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_INDEX, parser.nextInt());
+ position.set(Position.KEY_POWER, parser.nextInt() * 0.01);
+ position.set(Position.KEY_BATTERY, parser.nextInt() * 0.01);
+ position.set(Position.KEY_IGNITION, parser.nextInt() == 1);
+ position.set(Position.KEY_INPUT, parser.nextInt());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+
+ position.setAltitude(parser.nextDouble(0));
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble());
+ position.setCourse(parser.nextDouble());
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AstraProtocol.java b/src/main/java/org/traccar/protocol/AstraProtocol.java
new file mode 100644
index 000000000..12b0dfb68
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AstraProtocol.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AstraProtocol extends BaseProtocol {
+
+ public AstraProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 1, 2, -3, 0));
+ pipeline.addLast(new AstraProtocolDecoder(AstraProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new AstraProtocolDecoder(AstraProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AstraProtocolDecoder.java b/src/main/java/org/traccar/protocol/AstraProtocolDecoder.java
new file mode 100644
index 000000000..e6f546b9f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AstraProtocolDecoder.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+
+public class AstraProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AstraProtocolDecoder.class);
+
+ public AstraProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_HEARTBEAT = 0x1A;
+ public static final int MSG_DATA = 0x10;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(Unpooled.wrappedBuffer(new byte[] {0x06}), remoteAddress));
+ }
+
+ buf.readUnsignedByte(); // protocol
+ buf.readUnsignedShort(); // length
+
+ String imei = String.format("%08d", buf.readUnsignedInt()) + String.format("%07d", buf.readUnsignedMedium());
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ while (buf.readableBytes() > 2) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedByte(); // index
+
+ position.setValid(true);
+ position.setLatitude(buf.readInt() * 0.000001);
+ position.setLongitude(buf.readInt() * 0.000001);
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(1980, 1, 6).addMillis(buf.readUnsignedInt() * 1000L);
+ position.setTime(dateBuilder.getDate());
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte() * 2));
+ position.setCourse(buf.readUnsignedByte() * 2);
+
+ int reason = buf.readUnsignedMedium();
+ position.set(Position.KEY_EVENT, reason);
+
+ int status = buf.readUnsignedShort();
+ position.set(Position.KEY_STATUS, status);
+
+ position.set(Position.PREFIX_IO + 1, buf.readUnsignedByte());
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedByte());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte());
+ position.set(Position.KEY_POWER, buf.readUnsignedByte());
+
+ buf.readUnsignedByte(); // max journey speed
+ buf.skipBytes(6); // accelerometer
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedShort());
+ buf.readUnsignedShort(); // journey idle time
+
+ position.setAltitude(buf.readUnsignedByte() * 20);
+
+ int quality = buf.readUnsignedByte();
+ position.set(Position.KEY_SATELLITES, quality & 0xf);
+ position.set(Position.KEY_RSSI, quality >> 4);
+
+ buf.readUnsignedByte(); // geofence events
+
+ if (BitUtil.check(status, 8)) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, buf.readSlice(7).toString(StandardCharsets.US_ASCII));
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedMedium() * 1000);
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromHours(buf.readUnsignedShort()));
+ }
+
+ if (BitUtil.check(status, 6)) {
+ LOGGER.warn("Extension data is not supported");
+ return position;
+ }
+
+ positions.add(position);
+
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/At2000FrameDecoder.java b/src/main/java/org/traccar/protocol/At2000FrameDecoder.java
new file mode 100644
index 000000000..5fa82a5f7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/At2000FrameDecoder.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+import org.traccar.NetworkMessage;
+
+public class At2000FrameDecoder extends BaseFrameDecoder {
+
+ private static final int BLOCK_LENGTH = 16;
+ private static final int ACK_LENGTH = 496;
+
+ private boolean firstPacket = true;
+
+ private ByteBuf currentBuffer;
+ private int acknowledgedBytes;
+
+ private void sendResponse(Channel channel) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer(2 * BLOCK_LENGTH);
+ response.writeByte(At2000ProtocolDecoder.MSG_ACKNOWLEDGEMENT);
+ response.writeMedium(1);
+ response.writeByte(0x00); // success
+ response.writerIndex(2 * BLOCK_LENGTH);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 5) {
+ return null;
+ }
+
+ int length;
+ if (firstPacket) {
+ firstPacket = false;
+ length = buf.getUnsignedMediumLE(buf.readerIndex() + 2);
+ } else {
+ length = buf.getUnsignedMediumLE(buf.readerIndex() + 1);
+ }
+
+ length += BLOCK_LENGTH;
+ if (length % BLOCK_LENGTH != 0) {
+ length = (length / BLOCK_LENGTH + 1) * BLOCK_LENGTH;
+ }
+
+ if ((buf.readableBytes() >= length || buf.readableBytes() % ACK_LENGTH == 0)
+ && (buf != currentBuffer || buf.readableBytes() > acknowledgedBytes)) {
+ sendResponse(channel);
+ currentBuffer = buf;
+ acknowledgedBytes = buf.readableBytes();
+ }
+
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/At2000Protocol.java b/src/main/java/org/traccar/protocol/At2000Protocol.java
new file mode 100644
index 000000000..5894f3eab
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/At2000Protocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class At2000Protocol extends BaseProtocol {
+
+ public At2000Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new At2000FrameDecoder());
+ pipeline.addLast(new At2000ProtocolDecoder(At2000Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/At2000ProtocolDecoder.java b/src/main/java/org/traccar/protocol/At2000ProtocolDecoder.java
new file mode 100644
index 000000000..43798eb67
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/At2000ProtocolDecoder.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DataConverter;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class At2000ProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final int BLOCK_LENGTH = 16;
+
+ public At2000ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_ACKNOWLEDGEMENT = 0x00;
+ public static final int MSG_DEVICE_ID = 0x01;
+ public static final int MSG_TRACK_REQUEST = 0x88;
+ public static final int MSG_TRACK_RESPONSE = 0x89;
+ public static final int MSG_SESSION_END = 0x0c;
+
+ private Cipher cipher;
+
+ private static void sendRequest(Channel channel) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer(BLOCK_LENGTH);
+ response.writeByte(MSG_TRACK_REQUEST);
+ response.writeMedium(0);
+ response.writerIndex(BLOCK_LENGTH);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0x01) {
+ buf.readUnsignedByte(); // codec id
+ }
+
+ int type = buf.readUnsignedByte();
+ buf.readUnsignedMediumLE(); // length
+ buf.skipBytes(BLOCK_LENGTH - 1 - 3);
+
+ if (type == MSG_DEVICE_ID) {
+
+ String imei = buf.readSlice(15).toString(StandardCharsets.US_ASCII);
+ if (getDeviceSession(channel, remoteAddress, imei) != null) {
+
+ byte[] iv = new byte[BLOCK_LENGTH];
+ buf.readBytes(iv);
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+
+ SecretKeySpec keySpec = new SecretKeySpec(
+ DataConverter.parseHex("000102030405060708090a0b0c0d0e0f"), "AES");
+
+ cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+ byte[] data = new byte[BLOCK_LENGTH];
+ buf.readBytes(data);
+ cipher.update(data);
+
+ }
+
+ } else if (type == MSG_TRACK_RESPONSE) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (buf.capacity() <= BLOCK_LENGTH) {
+ return null; // empty message
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ byte[] data = new byte[buf.capacity() - BLOCK_LENGTH];
+ buf.readBytes(data);
+ buf = Unpooled.wrappedBuffer(cipher.update(data));
+ try {
+ while (buf.readableBytes() >= 63) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedShortLE(); // index
+ buf.readUnsignedShortLE(); // reserved
+
+ position.setValid(true);
+
+ position.setTime(new Date(buf.readLongLE() * 1000));
+
+ position.setLatitude(buf.readFloatLE());
+ position.setLongitude(buf.readFloatLE());
+ position.setAltitude(buf.readFloatLE());
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readFloatLE()));
+ position.setCourse(buf.readFloatLE());
+
+ buf.readUnsignedIntLE(); // geozone event
+ buf.readUnsignedIntLE(); // io events
+ buf.readUnsignedIntLE(); // geozone value
+ buf.readUnsignedIntLE(); // io values
+ buf.readUnsignedShortLE(); // operator
+
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShortLE());
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() * 0.001);
+
+ buf.readUnsignedShortLE(); // cid
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ buf.readUnsignedByte(); // current profile
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte());
+ position.set(Position.PREFIX_TEMP + 1, buf.readUnsignedByte());
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ positions.add(position);
+
+ }
+ } finally {
+ buf.release();
+ }
+
+ return positions;
+
+ }
+
+ if (type == MSG_DEVICE_ID) {
+ sendRequest(channel);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AtrackFrameDecoder.java b/src/main/java/org/traccar/protocol/AtrackFrameDecoder.java
new file mode 100644
index 000000000..f071e2d97
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AtrackFrameDecoder.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+import org.traccar.helper.BufferUtil;
+
+import java.nio.charset.StandardCharsets;
+
+public class AtrackFrameDecoder extends BaseFrameDecoder {
+
+ private static final int KEEPALIVE_LENGTH = 12;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() >= 2) {
+
+ if (buf.getUnsignedShort(buf.readerIndex()) == 0xfe02) {
+
+ if (buf.readableBytes() >= KEEPALIVE_LENGTH) {
+ return buf.readRetainedSlice(KEEPALIVE_LENGTH);
+ }
+
+ } else if (buf.getUnsignedShort(buf.readerIndex()) == 0x4050 && buf.getByte(buf.readerIndex() + 2) != ',') {
+
+ if (buf.readableBytes() > 6) {
+ int length = buf.getUnsignedShort(buf.readerIndex() + 4) + 4 + 2;
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+ }
+
+ } else {
+
+ int lengthStart = buf.indexOf(buf.readerIndex() + 3, buf.writerIndex(), (byte) ',') + 1;
+ if (lengthStart > 0) {
+ int lengthEnd = buf.indexOf(lengthStart, buf.writerIndex(), (byte) ',');
+ if (lengthEnd > 0) {
+ int length = lengthEnd + Integer.parseInt(buf.toString(
+ lengthStart, lengthEnd - lengthStart, StandardCharsets.US_ASCII));
+ if (buf.readableBytes() > length && buf.getByte(buf.readerIndex() + length) == '\n') {
+ length += 1;
+ }
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+ }
+ } else {
+ int endIndex = BufferUtil.indexOf("\r\n", buf);
+ if (endIndex > 0) {
+ return buf.readRetainedSlice(endIndex - buf.readerIndex() + 2);
+ }
+ }
+
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AtrackProtocol.java b/src/main/java/org/traccar/protocol/AtrackProtocol.java
new file mode 100644
index 000000000..8e5cfe9ff
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AtrackProtocol.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class AtrackProtocol extends BaseProtocol {
+
+ public AtrackProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new AtrackFrameDecoder());
+ pipeline.addLast(new AtrackProtocolEncoder());
+ pipeline.addLast(new AtrackProtocolDecoder(AtrackProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new AtrackProtocolEncoder());
+ pipeline.addLast(new AtrackProtocolDecoder(AtrackProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AtrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/AtrackProtocolDecoder.java
new file mode 100644
index 000000000..71bb6791c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AtrackProtocolDecoder.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AtrackProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final int MIN_DATA_LENGTH = 40;
+
+ private boolean longDate;
+ private boolean decimalFuel;
+ private boolean custom;
+ private String form;
+
+ private final Map<Integer, String> alarmMap = new HashMap<>();
+
+ public AtrackProtocolDecoder(Protocol protocol) {
+ super(protocol);
+
+ longDate = Context.getConfig().getBoolean(getProtocolName() + ".longDate");
+ decimalFuel = Context.getConfig().getBoolean(getProtocolName() + ".decimalFuel");
+
+ custom = Context.getConfig().getBoolean(getProtocolName() + ".custom");
+ form = Context.getConfig().getString(getProtocolName() + ".form");
+ if (form != null) {
+ custom = true;
+ }
+
+ for (String pair : Context.getConfig().getString(getProtocolName() + ".alarmMap", "").split(",")) {
+ if (!pair.isEmpty()) {
+ alarmMap.put(
+ Integer.parseInt(pair.substring(0, pair.indexOf('='))), pair.substring(pair.indexOf('=') + 1));
+ }
+ }
+ }
+
+ public void setLongDate(boolean longDate) {
+ this.longDate = longDate;
+ }
+
+ public void setCustom(boolean custom) {
+ this.custom = custom;
+ }
+
+ private static void sendResponse(Channel channel, SocketAddress remoteAddress, long rawId, int index) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer(12);
+ response.writeShort(0xfe02);
+ response.writeLong(rawId);
+ response.writeShort(index);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ private static String readString(ByteBuf buf) {
+ String result = null;
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0);
+ if (index > buf.readerIndex()) {
+ result = buf.readSlice(index - buf.readerIndex()).toString(StandardCharsets.US_ASCII);
+ }
+ buf.readByte();
+ return result;
+ }
+
+ private void readTextCustomData(Position position, String data, String form) {
+ CellTower cellTower = new CellTower();
+ String[] keys = form.substring(1).split("%");
+ String[] values = data.split(",|\r\n");
+ for (int i = 0; i < Math.min(keys.length, values.length); i++) {
+ switch (keys[i]) {
+ case "SA":
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(values[i]));
+ break;
+ case "MV":
+ position.set(Position.KEY_POWER, Integer.parseInt(values[i]) * 0.1);
+ break;
+ case "BV":
+ position.set(Position.KEY_BATTERY, Integer.parseInt(values[i]) * 0.1);
+ break;
+ case "GQ":
+ cellTower.setSignalStrength(Integer.parseInt(values[i]));
+ break;
+ case "CE":
+ cellTower.setCellId(Long.parseLong(values[i]));
+ break;
+ case "LC":
+ cellTower.setLocationAreaCode(Integer.parseInt(values[i]));
+ break;
+ case "CN":
+ if (values[i].length() > 3) {
+ cellTower.setMobileCountryCode(Integer.parseInt(values[i].substring(0, 3)));
+ cellTower.setMobileNetworkCode(Integer.parseInt(values[i].substring(3)));
+ }
+ break;
+ case "PC":
+ position.set(Position.PREFIX_COUNT + 1, Integer.parseInt(values[i]));
+ break;
+ case "AT":
+ position.setAltitude(Integer.parseInt(values[i]));
+ break;
+ case "RP":
+ position.set(Position.KEY_RPM, Integer.parseInt(values[i]));
+ break;
+ case "GS":
+ position.set(Position.KEY_RSSI, Integer.parseInt(values[i]));
+ break;
+ case "DT":
+ position.set(Position.KEY_ARCHIVE, Integer.parseInt(values[i]) == 1);
+ break;
+ case "VN":
+ position.set(Position.KEY_VIN, values[i]);
+ break;
+ case "TR":
+ position.set(Position.KEY_THROTTLE, Integer.parseInt(values[i]));
+ break;
+ case "ET":
+ position.set(Position.PREFIX_TEMP + 1, Integer.parseInt(values[i]));
+ break;
+ case "FL":
+ position.set(Position.KEY_FUEL_LEVEL, Integer.parseInt(values[i]));
+ break;
+ case "FC":
+ position.set(Position.KEY_FUEL_CONSUMPTION, Integer.parseInt(values[i]));
+ break;
+ case "AV1":
+ position.set(Position.PREFIX_ADC + 1, Integer.parseInt(values[i]));
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (cellTower.getMobileCountryCode() != null
+ && cellTower.getMobileNetworkCode() != null
+ && cellTower.getCellId() != null
+ && cellTower.getLocationAreaCode() != null) {
+ position.setNetwork(new Network(cellTower));
+ } else if (cellTower.getSignalStrength() != null) {
+ position.set(Position.KEY_RSSI, cellTower.getSignalStrength());
+ }
+ }
+
+ private void readBinaryCustomData(Position position, ByteBuf buf, String form) {
+ CellTower cellTower = new CellTower();
+ String[] keys = form.substring(1).split("%");
+ for (String key : keys) {
+ switch (key) {
+ case "SA":
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ break;
+ case "MV":
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.1);
+ break;
+ case "BV":
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.1);
+ break;
+ case "GQ":
+ cellTower.setSignalStrength((int) buf.readUnsignedByte());
+ break;
+ case "CE":
+ cellTower.setCellId(buf.readUnsignedInt());
+ break;
+ case "LC":
+ cellTower.setLocationAreaCode(buf.readUnsignedShort());
+ break;
+ case "CN":
+ int combinedMobileCodes = (int) (buf.readUnsignedInt() % 100000); // cccnn
+ cellTower.setMobileCountryCode(combinedMobileCodes / 100);
+ cellTower.setMobileNetworkCode(combinedMobileCodes % 100);
+ break;
+ case "RL":
+ buf.readUnsignedByte(); // rxlev
+ break;
+ case "PC":
+ position.set(Position.PREFIX_COUNT + 1, buf.readUnsignedInt());
+ break;
+ case "AT":
+ position.setAltitude(buf.readUnsignedInt());
+ break;
+ case "RP":
+ position.set(Position.KEY_RPM, buf.readUnsignedShort());
+ break;
+ case "GS":
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ break;
+ case "DT":
+ position.set(Position.KEY_ARCHIVE, buf.readUnsignedByte() == 1);
+ break;
+ case "VN":
+ position.set(Position.KEY_VIN, readString(buf));
+ break;
+ case "MF":
+ buf.readUnsignedShort(); // mass air flow rate
+ break;
+ case "EL":
+ buf.readUnsignedByte(); // engine load
+ break;
+ case "TR":
+ position.set(Position.KEY_THROTTLE, buf.readUnsignedByte());
+ break;
+ case "ET":
+ position.set(Position.PREFIX_TEMP + 1, buf.readUnsignedShort());
+ break;
+ case "FL":
+ position.set(Position.KEY_FUEL_LEVEL, buf.readUnsignedByte());
+ break;
+ case "ML":
+ buf.readUnsignedByte(); // mil status
+ break;
+ case "FC":
+ position.set(Position.KEY_FUEL_CONSUMPTION, buf.readUnsignedInt());
+ break;
+ case "CI":
+ readString(buf); // format string
+ break;
+ case "AV1":
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort());
+ break;
+ case "NC":
+ readString(buf); // gsm neighbor cell info
+ break;
+ case "SM":
+ buf.readUnsignedShort(); // max speed between reports
+ break;
+ case "GL":
+ readString(buf); // google link
+ break;
+ case "MA":
+ readString(buf); // mac address
+ break;
+ case "PD":
+ buf.readUnsignedByte(); // pending code status
+ break;
+ case "CD":
+ readString(buf); // sim cid
+ break;
+ case "CM":
+ buf.readLong(); // imsi
+ break;
+ case "GN":
+ buf.skipBytes(60); // g sensor data
+ break;
+ case "GV":
+ buf.skipBytes(6); // maximum g force
+ break;
+ case "ME":
+ buf.readLong(); // imei
+ break;
+ case "IA":
+ buf.readUnsignedByte(); // intake air temperature
+ break;
+ case "MP":
+ buf.readUnsignedByte(); // manifold absolute pressure
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (cellTower.getMobileCountryCode() != null
+ && cellTower.getMobileNetworkCode() != null
+ && cellTower.getCellId() != null && cellTower.getCellId() != 0
+ && cellTower.getLocationAreaCode() != null) {
+ position.setNetwork(new Network(cellTower));
+ } else if (cellTower.getSignalStrength() != null) {
+ position.set(Position.KEY_RSSI, cellTower.getSignalStrength());
+ }
+ }
+
+ private static final Pattern PATTERN_INFO = new PatternBuilder()
+ .text("$INFO=")
+ .number("(d+),") // unit id
+ .expression("([^,]+),") // model
+ .expression("([^,]+),") // firmware version
+ .number("d+,") // imei
+ .number("d+,") // imsi
+ .number("d+,") // sim card id
+ .number("(d+),") // power
+ .number("(d+),") // battery
+ .number("(d+),") // satellites
+ .number("d+,") // gsm status
+ .number("(d+),") // rssi
+ .number("d+,") // connection status
+ .number("d+") // antenna status
+ .any()
+ .compile();
+
+ private Position decodeInfo(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Position position = new Position(getProtocolName());
+
+ getLastLocation(position, null);
+
+ DeviceSession deviceSession;
+
+ if (sentence.startsWith("$INFO")) {
+
+ Parser parser = new Parser(PATTERN_INFO, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+
+ position.set("model", parser.next());
+ position.set(Position.KEY_VERSION_FW, parser.next());
+ position.set(Position.KEY_POWER, parser.nextInt() * 0.1);
+ position.set(Position.KEY_BATTERY, parser.nextInt() * 0.1);
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_RSSI, parser.nextInt());
+
+ } else {
+
+ deviceSession = getDeviceSession(channel, remoteAddress);
+
+ position.set(Position.KEY_RESULT, sentence);
+
+ }
+
+ if (deviceSession == null) {
+ return null;
+ } else {
+ position.setDeviceId(deviceSession.getDeviceId());
+ return position;
+ }
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(d+),") // date and time
+ .number("d+,") // rtc date and time
+ .number("d+,") // device date and time
+ .number("(-?d+),") // longitude
+ .number("(-?d+),") // latitude
+ .number("(d+),") // course
+ .number("(d+),") // report id
+ .number("(d+.?d*),") // odometer
+ .number("(d+),") // hdop
+ .number("(d+),") // inputs
+ .number("(d+),") // speed
+ .number("(d+),") // outputs
+ .number("(d+),") // adc
+ .number("([^,]+)?,") // driver
+ .number("(d+),") // temp1
+ .number("(d+),") // temp2
+ .expression("[^,]*,") // text message
+ .expression("(.*)") // custom data
+ .optional(2)
+ .compile();
+
+ private List<Position> decodeText(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ int startIndex = 0;
+ for (int i = 0; i < 4; i++) {
+ startIndex = sentence.indexOf(',', startIndex + 1);
+ }
+ int endIndex = sentence.indexOf(',', startIndex + 1);
+
+ String imei = sentence.substring(startIndex + 1, endIndex);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+ String[] lines = sentence.substring(endIndex + 1).split("\r\n");
+
+ for (String line : lines) {
+ Position position = decodeTextLine(deviceSession, line);
+ if (position != null) {
+ positions.add(position);
+ }
+ }
+
+ return positions;
+ }
+
+
+ private Position decodeTextLine(DeviceSession deviceSession, String sentence) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(true);
+
+ String time = parser.next();
+ if (time.length() >= 14) {
+ try {
+ DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ position.setTime(dateFormat.parse(time));
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ position.setTime(new Date(Long.parseLong(time) * 1000));
+ }
+
+ position.setLongitude(parser.nextInt() * 0.000001);
+ position.setLatitude(parser.nextInt() * 0.000001);
+ position.setCourse(parser.nextInt());
+
+ position.set(Position.KEY_EVENT, parser.nextInt());
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 100);
+ position.set(Position.KEY_HDOP, parser.nextInt() * 0.1);
+ position.set(Position.KEY_INPUT, parser.nextInt());
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt()));
+
+ position.set(Position.KEY_OUTPUT, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt());
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+ }
+
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt());
+ position.set(Position.PREFIX_TEMP + 2, parser.nextInt());
+
+ if (custom) {
+ String data = parser.next();
+ String form = this.form;
+ if (form == null) {
+ form = data.substring(0, data.indexOf(',')).substring("%CI".length());
+ data = data.substring(data.indexOf(',') + 1);
+ }
+ readTextCustomData(position, data, form);
+ }
+
+ return position;
+ }
+
+ private List<Position> decodeBinary(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ buf.skipBytes(2); // prefix
+ buf.readUnsignedShort(); // checksum
+ buf.readUnsignedShort(); // length
+ int index = buf.readUnsignedShort();
+
+ long id = buf.readLong();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(id));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ sendResponse(channel, remoteAddress, id, index);
+
+ List<Position> positions = new LinkedList<>();
+
+ while (buf.readableBytes() >= MIN_DATA_LENGTH) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (longDate) {
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(buf.readUnsignedShort(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ buf.skipBytes(7 + 7);
+
+ } else {
+ position.setFixTime(new Date(buf.readUnsignedInt() * 1000));
+ position.setDeviceTime(new Date(buf.readUnsignedInt() * 1000));
+ buf.readUnsignedInt(); // send time
+ }
+
+ position.setValid(true);
+ position.setLongitude(buf.readInt() * 0.000001);
+ position.setLatitude(buf.readInt() * 0.000001);
+ position.setCourse(buf.readUnsignedShort());
+
+ int type = buf.readUnsignedByte();
+ position.set(Position.KEY_TYPE, type);
+ position.set(Position.KEY_ALARM, alarmMap.get(type));
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt() * 100);
+ position.set(Position.KEY_HDOP, buf.readUnsignedShort() * 0.1);
+ position.set(Position.KEY_INPUT, buf.readUnsignedByte());
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedByte());
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort() * 0.001);
+
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, readString(buf));
+
+ position.set(Position.PREFIX_TEMP + 1, buf.readShort() * 0.1);
+ position.set(Position.PREFIX_TEMP + 2, buf.readShort() * 0.1);
+
+ String message = readString(buf);
+ if (message != null && !message.isEmpty()) {
+ Pattern pattern = Pattern.compile("FULS:F=(\\p{XDigit}+) t=(\\p{XDigit}+) N=(\\p{XDigit}+)");
+ Matcher matcher = pattern.matcher(message);
+ if (matcher.find()) {
+ int value = Integer.parseInt(matcher.group(3), decimalFuel ? 10 : 16);
+ position.set(Position.KEY_FUEL_LEVEL, value * 0.1);
+ } else {
+ position.set("message", message);
+ }
+ }
+
+ if (custom) {
+ String form = this.form;
+ if (form == null) {
+ form = readString(buf).trim().substring("%CI".length());
+ }
+ readBinaryCustomData(position, buf, form);
+ }
+
+ positions.add(position);
+
+ }
+
+ return positions;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (buf.getUnsignedShort(buf.readerIndex()) == 0xfe02) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(buf.retain(), remoteAddress)); // keep-alive message
+ }
+ return null;
+ } else if (buf.getByte(buf.readerIndex()) == '$') {
+ return decodeInfo(channel, remoteAddress, buf.toString(StandardCharsets.US_ASCII).trim());
+ } else if (buf.getByte(buf.readerIndex() + 2) == ',') {
+ return decodeText(channel, remoteAddress, buf.toString(StandardCharsets.US_ASCII).trim());
+ } else {
+ return decodeBinary(channel, remoteAddress, buf);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AtrackProtocolEncoder.java b/src/main/java/org/traccar/protocol/AtrackProtocolEncoder.java
new file mode 100644
index 000000000..1e085cb26
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AtrackProtocolEncoder.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class AtrackProtocolEncoder extends BaseProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return Unpooled.copiedBuffer(
+ command.getString(Command.KEY_DATA) + "\r\n", StandardCharsets.US_ASCII);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AuroProtocol.java b/src/main/java/org/traccar/protocol/AuroProtocol.java
new file mode 100644
index 000000000..b8ebdaa75
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AuroProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AuroProtocol extends BaseProtocol {
+
+ public AuroProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new AuroProtocolDecoder(AuroProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AuroProtocolDecoder.java b/src/main/java/org/traccar/protocol/AuroProtocolDecoder.java
new file mode 100644
index 000000000..d7916147b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AuroProtocolDecoder.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class AuroProtocolDecoder extends BaseProtocolDecoder {
+
+ public AuroProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("M(dddd)") // index
+ .number("Td+") // phone
+ .number("I(d+)") // imei
+ .number("Ed+W")
+ .text("*****")
+ .number("d{8}d{4}") // local time
+ .expression(".{8}#.{8}")
+ .number("d{10}") // status
+ .number("([-+])(ddd)(dd)(dddd)") // longitude
+ .number("([-+])(ddd)(dd)(dddd)") // latitude
+ .number("(dd)(dd)(dddd)") // date (ddmmyyyy)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number("(ddd)") // course
+ .number("d{6}")
+ .number("(ddd)") // speed
+ .number("d")
+ .number("(dd)") // battery
+ .expression("([01])") // charging
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ position.set(Position.KEY_INDEX, parser.nextInt(0));
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(true);
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN_MIN));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN_MIN));
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setCourse(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+
+ position.set(Position.KEY_BATTERY, parser.nextInt(0));
+ position.set(Position.KEY_CHARGE, parser.nextInt(0) == 1);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AustinNbProtocol.java b/src/main/java/org/traccar/protocol/AustinNbProtocol.java
new file mode 100644
index 000000000..32bfc0aae
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AustinNbProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AustinNbProtocol extends BaseProtocol {
+
+ public AustinNbProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new AustinNbProtocolDecoder(AustinNbProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AustinNbProtocolDecoder.java b/src/main/java/org/traccar/protocol/AustinNbProtocolDecoder.java
new file mode 100644
index 000000000..dc6f3d280
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AustinNbProtocolDecoder.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+public class AustinNbProtocolDecoder extends BaseProtocolDecoder {
+
+ public AustinNbProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(d+);") // imei
+ .number("(dddd)-(dd)-(dd) ") // date
+ .number("(dd):(dd):(dd);") // time
+ .number("(-?d+,d+);") // latitude
+ .number("(-?d+,d+);") // longitude
+ .number("(d+);") // azimuth
+ .number("(d+);") // angle
+ .number("(d+);") // range
+ .number("(d+);") // out of range
+ .expression("(.*)") // operator
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.YMD_HMS, TimeZone.getDefault().getID()));
+
+ position.setValid(true);
+ position.setLatitude(Double.parseDouble(parser.next().replace(',', '.')));
+ position.setLongitude(Double.parseDouble(parser.next().replace(',', '.')));
+ position.setCourse(parser.nextInt());
+ position.set("angle", parser.nextInt());
+ position.set("range", parser.nextInt());
+ position.set("outOfRange", parser.nextInt());
+ position.set("carrier", parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AutoFonFrameDecoder.java b/src/main/java/org/traccar/protocol/AutoFonFrameDecoder.java
new file mode 100644
index 000000000..69f28133f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AutoFonFrameDecoder.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2015 Vitaly Litvak (vitavaque@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class AutoFonFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ // Check minimum length
+ if (buf.readableBytes() < 12) {
+ return null;
+ }
+
+ int length;
+ switch (buf.getUnsignedByte(buf.readerIndex())) {
+ case AutoFonProtocolDecoder.MSG_LOGIN:
+ length = 12;
+ break;
+ case AutoFonProtocolDecoder.MSG_LOCATION:
+ length = 78;
+ break;
+ case AutoFonProtocolDecoder.MSG_HISTORY:
+ length = 257;
+ break;
+ case AutoFonProtocolDecoder.MSG_45_LOGIN:
+ length = 19;
+ break;
+ case AutoFonProtocolDecoder.MSG_45_LOCATION:
+ length = 34;
+ break;
+ default:
+ length = 0;
+ break;
+ }
+
+ // Check length and return buffer
+ if (length != 0 && buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AutoFonProtocol.java b/src/main/java/org/traccar/protocol/AutoFonProtocol.java
new file mode 100644
index 000000000..08b5edc7d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AutoFonProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AutoFonProtocol extends BaseProtocol {
+
+ public AutoFonProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new AutoFonFrameDecoder());
+ pipeline.addLast(new AutoFonProtocolDecoder(AutoFonProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AutoFonProtocolDecoder.java b/src/main/java/org/traccar/protocol/AutoFonProtocolDecoder.java
new file mode 100644
index 000000000..aa05ca2d7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AutoFonProtocolDecoder.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2015 Vitaly Litvak (vitavaque@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+
+public class AutoFonProtocolDecoder extends BaseProtocolDecoder {
+
+ public AutoFonProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_LOGIN = 0x10;
+ public static final int MSG_LOCATION = 0x11;
+ public static final int MSG_HISTORY = 0x12;
+
+ public static final int MSG_45_LOGIN = 0x41;
+ public static final int MSG_45_LOCATION = 0x02;
+
+ private static double convertCoordinate(int raw) {
+ int degrees = raw / 1000000;
+ double minutes = (raw % 1000000) / 10000.0;
+ return degrees + minutes / 60;
+ }
+
+ private static double convertCoordinate(short degrees, int minutes) {
+ double value = degrees + BitUtil.from(minutes, 4) / 600000.0;
+ if (BitUtil.check(minutes, 0)) {
+ return value;
+ } else {
+ return -value;
+ }
+ }
+
+ private Position decodePosition(DeviceSession deviceSession, ByteBuf buf, boolean history) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (!history) {
+ buf.readUnsignedByte(); // interval
+ buf.skipBytes(8); // settings
+ }
+ position.set(Position.KEY_STATUS, buf.readUnsignedByte());
+ if (!history) {
+ buf.readUnsignedShort();
+ }
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte());
+ buf.skipBytes(6); // time
+
+ if (!history) {
+ for (int i = 0; i < 2; i++) {
+ buf.skipBytes(5); // time
+ buf.readUnsignedShort(); // interval
+ buf.skipBytes(5); // mode
+ }
+ }
+
+ position.set(Position.PREFIX_TEMP + 1, buf.readByte());
+
+ int rssi = buf.readUnsignedByte();
+ CellTower cellTower = CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedShort(),
+ buf.readUnsignedShort(), buf.readUnsignedShort(), rssi);
+ position.setNetwork(new Network(cellTower));
+
+ int valid = buf.readUnsignedByte();
+ position.setValid((valid & 0xc0) != 0);
+ position.set(Position.KEY_SATELLITES, valid & 0x3f);
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDateReverse(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ position.setLatitude(convertCoordinate(buf.readInt()));
+ position.setLongitude(convertCoordinate(buf.readInt()));
+ position.setAltitude(buf.readShort());
+ position.setSpeed(buf.readUnsignedByte());
+ position.setCourse(buf.readUnsignedByte() * 2.0);
+
+ position.set(Position.KEY_HDOP, buf.readUnsignedShort());
+
+ buf.readUnsignedShort(); // reserved
+ buf.readUnsignedByte(); // checksum
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int type = buf.readUnsignedByte();
+
+ if (type == MSG_LOGIN || type == MSG_45_LOGIN) {
+
+ if (type == MSG_LOGIN) {
+ buf.readUnsignedByte(); // hardware version
+ buf.readUnsignedByte(); // software version
+ }
+
+ String imei = ByteBufUtil.hexDump(buf.readSlice(8)).substring(1);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+
+ if (deviceSession != null && channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeBytes("resp_crc=".getBytes(StandardCharsets.US_ASCII));
+ response.writeByte(buf.getByte(buf.writerIndex() - 1));
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+
+ return null;
+
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (type == MSG_LOCATION) {
+
+ return decodePosition(deviceSession, buf, false);
+
+ } else if (type == MSG_HISTORY) {
+
+ int count = buf.readUnsignedByte() & 0x0f;
+ buf.readUnsignedShort(); // total count
+ List<Position> positions = new LinkedList<>();
+
+ for (int i = 0; i < count; i++) {
+ positions.add(decodePosition(deviceSession, buf, true));
+ }
+
+ return positions;
+
+ } else if (type == MSG_45_LOCATION) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ short status = buf.readUnsignedByte();
+ if (BitUtil.check(status, 7)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ position.set(Position.KEY_BATTERY, BitUtil.to(status, 7));
+
+ buf.skipBytes(2); // remaining time
+
+ position.set(Position.PREFIX_TEMP + 1, buf.readByte());
+
+ buf.skipBytes(2); // timer (interval and units)
+ buf.readByte(); // mode
+ buf.readByte(); // gprs sending interval
+
+ buf.skipBytes(6); // mcc, mnc, lac, cid
+
+ int valid = buf.readUnsignedByte();
+ position.setValid(BitUtil.from(valid, 6) != 0);
+ position.set(Position.KEY_SATELLITES, BitUtil.from(valid, 6));
+
+ int time = buf.readUnsignedMedium();
+ int date = buf.readUnsignedMedium();
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(time / 10000, time / 100 % 100, time % 100)
+ .setDateReverse(date / 10000, date / 100 % 100, date % 100);
+ position.setTime(dateBuilder.getDate());
+
+ position.setLatitude(convertCoordinate(buf.readUnsignedByte(), buf.readUnsignedMedium()));
+ position.setLongitude(convertCoordinate(buf.readUnsignedByte(), buf.readUnsignedMedium()));
+ position.setSpeed(buf.readUnsignedByte());
+ position.setCourse(buf.readUnsignedShort());
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AutoGradeProtocol.java b/src/main/java/org/traccar/protocol/AutoGradeProtocol.java
new file mode 100644
index 000000000..c6dbb681e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AutoGradeProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AutoGradeProtocol extends BaseProtocol {
+
+ public AutoGradeProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, ')'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new AutoGradeProtocolDecoder(AutoGradeProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AutoGradeProtocolDecoder.java b/src/main/java/org/traccar/protocol/AutoGradeProtocolDecoder.java
new file mode 100644
index 000000000..5052450b5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AutoGradeProtocolDecoder.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class AutoGradeProtocolDecoder extends BaseProtocolDecoder {
+
+ public AutoGradeProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("(")
+ .number("d{12}") // index
+ .number("(d{15})") // imei
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .expression("([AV])") // validity
+ .number("(d+)(dd.d+)([NS])") // latitude
+ .number("(d+)(dd.d+)([EW])") // longitude
+ .number("([d.]{5})") // speed
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number("([d.]{6})") // course
+ .expression("(.)") // status
+ .number("A(xxxx)")
+ .number("B(xxxx)")
+ .number("C(xxxx)")
+ .number("D(xxxx)")
+ .number("E(xxxx)")
+ .number("K(xxxx)")
+ .number("L(xxxx)")
+ .number("M(xxxx)")
+ .number("N(xxxx)")
+ .number("O(xxxx)")
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+
+ dateBuilder.setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.setCourse(parser.nextDouble(0));
+
+ int status = parser.next().charAt(0);
+ position.set(Position.KEY_STATUS, status);
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 0));
+
+ for (int i = 1; i <= 5; i++) {
+ position.set(Position.PREFIX_ADC + i, parser.next());
+ }
+
+ for (int i = 1; i <= 5; i++) {
+ position.set("can" + i, parser.next());
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AutoTrackProtocol.java b/src/main/java/org/traccar/protocol/AutoTrackProtocol.java
new file mode 100644
index 000000000..6aa7558bf
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AutoTrackProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import java.nio.ByteOrder;
+public class AutoTrackProtocol extends BaseProtocol {
+
+ public AutoTrackProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 5, 2, 2, 0, true));
+ pipeline.addLast(new AutoTrackProtocolDecoder(AutoTrackProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AutoTrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/AutoTrackProtocolDecoder.java
new file mode 100644
index 000000000..3c1fd256b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AutoTrackProtocolDecoder.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+
+public class AutoTrackProtocolDecoder extends BaseProtocolDecoder {
+
+ public AutoTrackProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_LOGIN_REQUEST = 51;
+ public static final int MSG_LOGIN_CONFIRM = 101;
+ public static final int MSG_TELEMETRY_1 = 52;
+ public static final int MSG_TELEMETRY_2 = 66;
+ public static final int MSG_TELEMETRY_3 = 67;
+ public static final int MSG_KEEP_ALIVE = 114;
+ public static final int MSG_TELEMETRY_CONFIRM = 123;
+
+ private Position decodeTelemetry(
+ Channel channel, SocketAddress remoteAddress, DeviceSession deviceSession, ByteBuf buf) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(1009843200000L + buf.readUnsignedIntLE() * 1000)); // seconds since 2002
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ position.set(Position.KEY_FUEL_USED, buf.readUnsignedIntLE());
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShortLE()));
+ buf.readUnsignedShortLE(); // max speed
+
+ position.set(Position.KEY_INPUT, buf.readUnsignedShortLE());
+ buf.readUnsignedIntLE(); // di 3 count
+ buf.readUnsignedIntLE(); // di 4 count
+
+ for (int i = 0; i < 5; i++) {
+ position.set(Position.PREFIX_ADC + (i + 1), buf.readUnsignedShortLE());
+ }
+
+ position.setCourse(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_STATUS, buf.readUnsignedShortLE());
+ position.set(Position.KEY_EVENT, buf.readUnsignedShortLE());
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, buf.readLongLE());
+
+ int index = buf.readUnsignedShortLE();
+
+ buf.readUnsignedShortLE(); // checksum
+
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeInt(0xF1F1F1F1); // sync
+ response.writeByte(MSG_TELEMETRY_CONFIRM);
+ response.writeShortLE(2); // length
+ response.writeShortLE(index);
+ response.writeShort(Checksum.crc16(Checksum.CRC16_XMODEM, response.nioBuffer()));
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(4); // sync
+ int type = buf.readUnsignedByte();
+ buf.readUnsignedShortLE(); // length
+
+ switch (type) {
+ case MSG_LOGIN_REQUEST:
+ String imei = ByteBufUtil.hexDump(buf.readBytes(8));
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+ int fuelConst = buf.readUnsignedShortLE();
+ int tripConst = buf.readUnsignedShortLE();
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeInt(0xF1F1F1F1); // sync
+ response.writeByte(MSG_LOGIN_CONFIRM);
+ response.writeShortLE(12); // length
+ response.writeBytes(ByteBufUtil.decodeHexDump(imei));
+ response.writeShortLE(fuelConst);
+ response.writeShortLE(tripConst);
+ response.writeShort(Checksum.crc16(Checksum.CRC16_XMODEM, response.nioBuffer()));
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ return null;
+ case MSG_TELEMETRY_1:
+ case MSG_TELEMETRY_2:
+ case MSG_TELEMETRY_3:
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+ return decodeTelemetry(channel, remoteAddress, deviceSession, buf);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AvemaProtocol.java b/src/main/java/org/traccar/protocol/AvemaProtocol.java
new file mode 100644
index 000000000..dbfab4dea
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AvemaProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class AvemaProtocol extends BaseProtocol {
+
+ public AvemaProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new AvemaProtocolDecoder(AvemaProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/AvemaProtocolDecoder.java b/src/main/java/org/traccar/protocol/AvemaProtocolDecoder.java
new file mode 100644
index 000000000..16a31162a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/AvemaProtocolDecoder.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class AvemaProtocolDecoder extends BaseProtocolDecoder {
+
+ public AvemaProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(d+),") // device id
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+.d+),") // latitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(-?d+),") // altitude
+ .number("(d+),") // satellites
+ .number("(d+),") // event
+ .number("(d+.d+),") // odometer
+ .number("(d+),") // input
+ .number("(d+.d+)V,") // adc 1
+ .number("(d+.d+)V,") // adc 2
+ .number("(d+),") // output
+ .number("(d),") // roaming
+ .number("(d+),") // rssi
+ .number("d,") // communication system
+ .number("(ddd)") // mcc
+ .number("(dd),") // mnc
+ .number("(x+),") // lac
+ .number("(x+),") // cid
+ .number("([^,]+)?") // rfid
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(true);
+ position.setTime(parser.nextDateTime());
+ position.setLongitude(parser.nextDouble());
+ position.setLatitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt()));
+ position.setCourse(parser.nextInt());
+ position.setAltitude(parser.nextInt());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_EVENT, parser.nextInt());
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+ position.set(Position.KEY_INPUT, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.PREFIX_ADC + 2, parser.nextDouble());
+ position.set(Position.KEY_OUTPUT, parser.nextInt());
+ position.set(Position.KEY_ROAMING, parser.nextInt() == 1);
+
+ int rssi = parser.nextInt();
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(), parser.nextInt(), parser.nextHexInt(), parser.nextHexInt(), rssi)));
+
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Avl301Protocol.java b/src/main/java/org/traccar/protocol/Avl301Protocol.java
new file mode 100644
index 000000000..71fc7cb26
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Avl301Protocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Avl301Protocol extends BaseProtocol {
+
+ public Avl301Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(256, 2, 1, -3, 0));
+ pipeline.addLast(new Avl301ProtocolDecoder(Avl301Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Avl301ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Avl301ProtocolDecoder.java
new file mode 100644
index 000000000..f6b7db2d6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Avl301ProtocolDecoder.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class Avl301ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Avl301ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private String readImei(ByteBuf buf) {
+ int b = buf.readUnsignedByte();
+ StringBuilder imei = new StringBuilder();
+ imei.append(b & 0x0F);
+ for (int i = 0; i < 7; i++) {
+ b = buf.readUnsignedByte();
+ imei.append((b & 0xF0) >> 4);
+ imei.append(b & 0x0F);
+ }
+ return imei.toString();
+ }
+
+ public static final int MSG_LOGIN = 'L';
+ public static final int MSG_STATUS = 'H';
+ public static final int MSG_GPS_LBS_STATUS = '$';
+
+ private void sendResponse(Channel channel, int type) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer(5);
+ response.writeByte('$');
+ response.writeByte(type);
+ response.writeByte('#');
+ response.writeByte('\r'); response.writeByte('\n');
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(1); // header
+ int type = buf.readUnsignedByte();
+ buf.readUnsignedByte(); // length
+
+ if (type == MSG_LOGIN) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, readImei(buf));
+ if (deviceSession == null) {
+ sendResponse(channel, type);
+ }
+
+ } else if (type == MSG_STATUS) {
+
+ sendResponse(channel, type);
+
+ } else if (type == MSG_GPS_LBS_STATUS) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ int gpsLength = buf.readUnsignedByte(); // gps len and sat
+ position.set(Position.KEY_SATELLITES, gpsLength & 0xf);
+
+ position.set(Position.KEY_SATELLITES_VISIBLE, buf.readUnsignedByte()); // satellites
+
+ double latitude = buf.readUnsignedInt() / 600000.0;
+ double longitude = buf.readUnsignedInt() / 600000.0;
+ position.setSpeed(buf.readUnsignedByte());
+
+ int union = buf.readUnsignedShort(); // course and flags
+ position.setCourse(union & 0x03FF);
+ position.setValid((union & 0x1000) != 0);
+ if ((union & 0x0400) != 0) {
+ latitude = -latitude;
+ }
+ if ((union & 0x0800) != 0) {
+ longitude = -longitude;
+ }
+
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+
+ if ((union & 0x4000) != 0) {
+ position.set("acc", (union & 0x8000) != 0);
+ }
+
+ position.setNetwork(new Network(
+ CellTower.fromLacCid(buf.readUnsignedShort(), buf.readUnsignedMedium())));
+
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ int flags = buf.readUnsignedByte();
+ position.set("acc", (flags & 0x2) != 0);
+
+ // parse other flags
+
+ position.set(Position.KEY_POWER, buf.readUnsignedByte());
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/BceFrameDecoder.java b/src/main/java/org/traccar/protocol/BceFrameDecoder.java
new file mode 100644
index 000000000..381a97696
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/BceFrameDecoder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class BceFrameDecoder extends BaseFrameDecoder {
+
+ private static final int HANDSHAKE_LENGTH = 7; // "#BCE#\r\n"
+
+ private boolean header = true;
+
+ private static byte checksum(ByteBuf buf, int end) {
+ byte result = 0;
+ for (int i = 0; i < end; i++) {
+ result += buf.getByte(buf.readerIndex() + i);
+ }
+ return result;
+ }
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (header && buf.readableBytes() >= HANDSHAKE_LENGTH) {
+ buf.skipBytes(HANDSHAKE_LENGTH);
+ header = false;
+ }
+
+ int end = 8; // IMEI
+
+ while (buf.readableBytes() >= end + 2 + 1 + 1 + 1) {
+ end += buf.getUnsignedShortLE(buf.readerIndex() + end) + 2;
+
+ if (buf.readableBytes() > end && checksum(buf, end) == buf.getByte(buf.readerIndex() + end)) {
+ return buf.readRetainedSlice(end + 1);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/BceProtocol.java b/src/main/java/org/traccar/protocol/BceProtocol.java
new file mode 100644
index 000000000..6453a05a9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/BceProtocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class BceProtocol extends BaseProtocol {
+
+ public BceProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_OUTPUT_CONTROL);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new BceFrameDecoder());
+ pipeline.addLast(new BceProtocolEncoder());
+ pipeline.addLast(new BceProtocolDecoder(BceProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/BceProtocolDecoder.java b/src/main/java/org/traccar/protocol/BceProtocolDecoder.java
new file mode 100644
index 000000000..ed810bebb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/BceProtocolDecoder.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class BceProtocolDecoder extends BaseProtocolDecoder {
+
+ public BceProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final int DATA_TYPE = 7;
+
+ public static final int MSG_ASYNC_STACK = 0xA5;
+ public static final int MSG_STACK_COFIRM = 0x19;
+ public static final int MSG_TIME_TRIGGERED = 0xA0;
+ public static final int MSG_OUTPUT_CONTROL = 0x41;
+ public static final int MSG_OUTPUT_CONTROL_ACK = 0xC1;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ String imei = String.format("%015d", buf.readLongLE());
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ while (buf.readableBytes() > 1) {
+
+ int dataEnd = buf.readUnsignedShortLE() + buf.readerIndex();
+ int type = buf.readUnsignedByte();
+
+ if (type != MSG_ASYNC_STACK && type != MSG_TIME_TRIGGERED) {
+ return null;
+ }
+
+ int confirmKey = buf.readUnsignedByte() & 0x7F;
+
+ while (buf.readerIndex() < dataEnd) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int structEnd = buf.readUnsignedByte() + buf.readerIndex();
+
+ long time = buf.readUnsignedIntLE();
+ if ((time & 0x0f) == DATA_TYPE) {
+
+ time = time >> 4 << 1;
+ time += 0x47798280; // 01/01/2008
+ position.setTime(new Date(time * 1000));
+
+ // Read masks
+ int mask;
+ List<Integer> masks = new LinkedList<>();
+ do {
+ mask = buf.readUnsignedShortLE();
+ masks.add(mask);
+ } while (BitUtil.check(mask, 15));
+
+ mask = masks.get(0);
+
+ if (BitUtil.check(mask, 0)) {
+ position.setValid(true);
+ position.setLongitude(buf.readFloatLE());
+ position.setLatitude(buf.readFloatLE());
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+
+ int status = buf.readUnsignedByte();
+ position.set(Position.KEY_SATELLITES, BitUtil.to(status, 4));
+ position.set(Position.KEY_HDOP, BitUtil.from(status, 4));
+
+ position.setCourse(buf.readUnsignedByte() * 2);
+ position.setAltitude(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ }
+
+ if (BitUtil.check(mask, 1)) {
+ position.set(Position.KEY_INPUT, buf.readUnsignedShortLE());
+ }
+
+ for (int i = 1; i <= 8; i++) {
+ if (BitUtil.check(mask, i + 1)) {
+ position.set(Position.PREFIX_ADC + i, buf.readUnsignedShortLE());
+ }
+ }
+
+ if (BitUtil.check(mask, 10)) {
+ buf.skipBytes(4);
+ }
+ if (BitUtil.check(mask, 11)) {
+ buf.skipBytes(4);
+ }
+ if (BitUtil.check(mask, 12)) {
+ buf.skipBytes(2);
+ }
+ if (BitUtil.check(mask, 13)) {
+ buf.skipBytes(2);
+ }
+
+ if (BitUtil.check(mask, 14)) {
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShortLE(), buf.readUnsignedByte(),
+ buf.readUnsignedShortLE(), buf.readUnsignedShortLE(),
+ buf.readUnsignedByte())));
+ buf.readUnsignedByte();
+ }
+
+ if (BitUtil.check(mask, 0)) {
+ positions.add(position);
+ }
+ }
+
+ buf.readerIndex(structEnd);
+ }
+
+ // Send response
+ if (type == MSG_ASYNC_STACK && channel != null) {
+ ByteBuf response = Unpooled.buffer(8 + 2 + 2 + 1);
+ response.writeLongLE(Long.parseLong(imei));
+ response.writeShortLE(2);
+ response.writeByte(MSG_STACK_COFIRM);
+ response.writeByte(confirmKey);
+
+ int checksum = 0;
+ for (int i = 0; i < response.writerIndex(); i++) {
+ checksum += response.getUnsignedByte(i);
+ }
+ response.writeByte(checksum);
+
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/BceProtocolEncoder.java b/src/main/java/org/traccar/protocol/BceProtocolEncoder.java
new file mode 100644
index 000000000..1bbf3db12
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/BceProtocolEncoder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+public class BceProtocolEncoder extends BaseProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ if (command.getType().equals(Command.TYPE_OUTPUT_CONTROL)) {
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeLongLE(Long.parseLong(getUniqueId(command.getDeviceId())));
+ buf.writeShortLE(1 + 1 + 3 + 1); // length
+ buf.writeByte(BceProtocolDecoder.MSG_OUTPUT_CONTROL);
+ buf.writeByte(command.getInteger(Command.KEY_INDEX) == 1 ? 0x0A : 0x0B);
+ buf.writeByte(0xFF); // index
+ buf.writeByte(0x00); // form id
+ buf.writeShortLE(Integer.parseInt(command.getString(Command.KEY_DATA)) > 0 ? 0x0055 : 0x0000);
+ buf.writeByte(Checksum.sum(buf.nioBuffer()));
+
+ return buf;
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/BlackKiteProtocol.java b/src/main/java/org/traccar/protocol/BlackKiteProtocol.java
new file mode 100644
index 000000000..617a24d7a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/BlackKiteProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 Vijay Kumar (vijaykumar@zilogic.com)
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class BlackKiteProtocol extends BaseProtocol {
+
+ public BlackKiteProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new GalileoFrameDecoder());
+ pipeline.addLast(new BlackKiteProtocolDecoder(BlackKiteProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/BlackKiteProtocolDecoder.java b/src/main/java/org/traccar/protocol/BlackKiteProtocolDecoder.java
new file mode 100644
index 000000000..dca4b908a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/BlackKiteProtocolDecoder.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2015 Vijay Kumar (vijaykumar@zilogic.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+public class BlackKiteProtocolDecoder extends BaseProtocolDecoder {
+
+ public BlackKiteProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final int TAG_IMEI = 0x03;
+ private static final int TAG_DATE = 0x20;
+ private static final int TAG_COORDINATES = 0x30;
+ private static final int TAG_SPEED_COURSE = 0x33;
+ private static final int TAG_ALTITUDE = 0x34;
+ private static final int TAG_STATUS = 0x40;
+ private static final int TAG_DIGITAL_OUTPUTS = 0x45;
+ private static final int TAG_DIGITAL_INPUTS = 0x46;
+ private static final int TAG_INPUT_VOLTAGE1 = 0x50;
+ private static final int TAG_INPUT_VOLTAGE2 = 0x51;
+ private static final int TAG_INPUT_VOLTAGE3 = 0x52;
+ private static final int TAG_INPUT_VOLTAGE4 = 0x53;
+ private static final int TAG_XT1 = 0x60;
+ private static final int TAG_XT2 = 0x61;
+ private static final int TAG_XT3 = 0x62;
+
+ private void sendReply(Channel channel, int checksum) {
+ if (channel != null) {
+ ByteBuf reply = Unpooled.buffer(3);
+ reply.writeByte(0x02);
+ reply.writeShortLE((short) checksum);
+ channel.writeAndFlush(new NetworkMessage(reply, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedByte(); // header
+ int length = (buf.readUnsignedShortLE() & 0x7fff) + 3;
+
+ List<Position> positions = new LinkedList<>();
+ Set<Integer> tags = new HashSet<>();
+ boolean hasLocation = false;
+ Position position = new Position(getProtocolName());
+
+ while (buf.readerIndex() < length) {
+
+ // Check if new message started
+ int tag = buf.readUnsignedByte();
+ if (tags.contains(tag)) {
+ if (hasLocation && position.getFixTime() != null) {
+ positions.add(position);
+ }
+ tags.clear();
+ hasLocation = false;
+ position = new Position(getProtocolName());
+ }
+ tags.add(tag);
+
+ switch (tag) {
+
+ case TAG_IMEI:
+ getDeviceSession(channel, remoteAddress, buf.readSlice(15).toString(StandardCharsets.US_ASCII));
+ break;
+
+ case TAG_DATE:
+ position.setTime(new Date(buf.readUnsignedIntLE() * 1000));
+ break;
+
+ case TAG_COORDINATES:
+ hasLocation = true;
+ position.setValid((buf.readUnsignedByte() & 0xf0) == 0x00);
+ position.setLatitude(buf.readIntLE() / 1000000.0);
+ position.setLongitude(buf.readIntLE() / 1000000.0);
+ break;
+
+ case TAG_SPEED_COURSE:
+ position.setSpeed(buf.readUnsignedShortLE() * 0.0539957);
+ position.setCourse(buf.readUnsignedShortLE() * 0.1);
+ break;
+
+ case TAG_ALTITUDE:
+ position.setAltitude(buf.readShortLE());
+ break;
+
+ case TAG_STATUS:
+ int status = buf.readUnsignedShortLE();
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 9));
+ if (BitUtil.check(status, 15)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ position.set(Position.KEY_CHARGE, BitUtil.check(status, 2));
+ break;
+
+ case TAG_DIGITAL_INPUTS:
+ int input = buf.readUnsignedShortLE();
+ for (int i = 0; i < 16; i++) {
+ position.set(Position.PREFIX_IO + (i + 1), BitUtil.check(input, i));
+ }
+ break;
+
+ case TAG_DIGITAL_OUTPUTS:
+ int output = buf.readUnsignedShortLE();
+ for (int i = 0; i < 16; i++) {
+ position.set(Position.PREFIX_IO + (i + 17), BitUtil.check(output, i));
+ }
+ break;
+
+ case TAG_INPUT_VOLTAGE1:
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShortLE() / 1000.0);
+ break;
+
+ case TAG_INPUT_VOLTAGE2:
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShortLE() / 1000.0);
+ break;
+
+ case TAG_INPUT_VOLTAGE3:
+ position.set(Position.PREFIX_ADC + 3, buf.readUnsignedShortLE() / 1000.0);
+ break;
+
+ case TAG_INPUT_VOLTAGE4:
+ position.set(Position.PREFIX_ADC + 4, buf.readUnsignedShortLE() / 1000.0);
+ break;
+
+ case TAG_XT1:
+ case TAG_XT2:
+ case TAG_XT3:
+ buf.skipBytes(16);
+ break;
+
+ default:
+ break;
+
+ }
+ }
+
+ if (hasLocation && position.getFixTime() != null) {
+ positions.add(position);
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ sendReply(channel, buf.readUnsignedShortLE());
+
+ for (Position p : positions) {
+ p.setDeviceId(deviceSession.getDeviceId());
+ }
+
+ if (positions.isEmpty()) {
+ return null;
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/BoxProtocol.java b/src/main/java/org/traccar/protocol/BoxProtocol.java
new file mode 100644
index 000000000..dfea15938
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/BoxProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class BoxProtocol extends BaseProtocol {
+
+ public BoxProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '\r'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new BoxProtocolDecoder(BoxProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/BoxProtocolDecoder.java b/src/main/java/org/traccar/protocol/BoxProtocolDecoder.java
new file mode 100644
index 000000000..3635c29e5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/BoxProtocolDecoder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2014 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class BoxProtocolDecoder extends BaseProtocolDecoder {
+
+ public BoxProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("L,")
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .text("G,")
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(d+.?d*),") // distance
+ .number("(d+),") // event
+ .number("(d+)") // status
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("H,")) {
+
+ int index = sentence.indexOf(',', 2) + 1;
+ String id = sentence.substring(index, sentence.indexOf(',', index));
+ getDeviceSession(channel, remoteAddress, id);
+
+ } else if (sentence.startsWith("E,")) {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("A," + sentence.substring(2) + "\r", remoteAddress));
+ }
+
+ } else if (sentence.startsWith("L,")) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setCourse(parser.nextDouble());
+
+ position.set(Position.KEY_ODOMETER_TRIP, parser.nextDouble() * 1000);
+ position.set(Position.KEY_EVENT, parser.next());
+
+ int status = parser.nextInt();
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 0));
+ position.set(Position.KEY_MOTION, BitUtil.check(status, 1));
+ position.setValid(!BitUtil.check(status, 2));
+ position.set(Position.KEY_STATUS, status);
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/C2stekProtocol.java b/src/main/java/org/traccar/protocol/C2stekProtocol.java
new file mode 100644
index 000000000..804621fd3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/C2stekProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class C2stekProtocol extends BaseProtocol {
+
+ public C2stekProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, false, "$AP"));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new C2stekProtocolDecoder(C2stekProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/C2stekProtocolDecoder.java b/src/main/java/org/traccar/protocol/C2stekProtocolDecoder.java
new file mode 100644
index 000000000..6a31cb2f4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/C2stekProtocolDecoder.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class C2stekProtocolDecoder extends BaseProtocolDecoder {
+
+ public C2stekProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("PA$")
+ .number("(d+)") // imei
+ .text("$")
+ .expression(".#") // data type
+ .number("(dd)(dd)(dd)#") // date (yymmdd)
+ .number("(dd)(dd)(dd)#") // time (hhmmss)
+ .number("([01])#") // valid
+ .number("([+-]?d+.d+)#") // latitude
+ .number("([+-]?d+.d+)#") // longitude
+ .number("(d+.d+)#") // speed
+ .number("(d+.d+)#") // course
+ .number("(-?d+.d+)#") // altitude
+ .number("(d+)#") // battery
+ .number("d+#") // geo area alarm
+ .number("(x+)#") // alarm
+ .number("([01])") // armed
+ .number("([01])") // door
+ .number("([01])#") // ignition
+ .any()
+ .text("$AP")
+ .compile();
+
+ private String decodeAlarm(int alarm) {
+ switch (alarm) {
+ case 0x2:
+ return Position.ALARM_SHOCK;
+ case 0x3:
+ return Position.ALARM_POWER_CUT;
+ case 0x4:
+ return Position.ALARM_OVERSPEED;
+ case 0x5:
+ return Position.ALARM_SOS;
+ case 0x6:
+ return Position.ALARM_DOOR;
+ case 0xA:
+ return Position.ALARM_LOW_BATTERY;
+ case 0xB:
+ return Position.ALARM_FAULT;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ if (sentence.contains("$20$") && channel != null) {
+ channel.writeAndFlush(new NetworkMessage(sentence, remoteAddress));
+ }
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+ position.setValid(parser.nextInt() > 0);
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setCourse(parser.nextDouble());
+ position.setAltitude(parser.nextDouble());
+
+ position.set(Position.KEY_BATTERY, parser.nextInt() * 0.001);
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.nextHexInt()));
+
+ position.set(Position.KEY_ARMED, parser.nextInt() > 0);
+ position.set(Position.KEY_DOOR, parser.nextInt() > 0);
+ position.set(Position.KEY_IGNITION, parser.nextInt() > 0);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CalAmpProtocol.java b/src/main/java/org/traccar/protocol/CalAmpProtocol.java
new file mode 100644
index 000000000..232e72a8c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CalAmpProtocol.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class CalAmpProtocol extends BaseProtocol {
+
+ public CalAmpProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CalAmpProtocolDecoder(CalAmpProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CalAmpProtocolDecoder.java b/src/main/java/org/traccar/protocol/CalAmpProtocolDecoder.java
new file mode 100644
index 000000000..31416d7f1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CalAmpProtocolDecoder.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+
+public class CalAmpProtocolDecoder extends BaseProtocolDecoder {
+
+ public CalAmpProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_NULL = 0;
+ public static final int MSG_ACK = 1;
+ public static final int MSG_EVENT_REPORT = 2;
+ public static final int MSG_ID_REPORT = 3;
+ public static final int MSG_USER_DATA = 4;
+ public static final int MSG_APP_DATA = 5;
+ public static final int MSG_CONFIG = 6;
+ public static final int MSG_UNIT_REQUEST = 7;
+ public static final int MSG_LOCATE_REPORT = 8;
+ public static final int MSG_USER_DATA_ACC = 9;
+ public static final int MSG_MINI_EVENT_REPORT = 10;
+ public static final int MSG_MINI_USER_DATA = 11;
+
+ public static final int SERVICE_UNACKNOWLEDGED = 0;
+ public static final int SERVICE_ACKNOWLEDGED = 1;
+ public static final int SERVICE_RESPONSE = 2;
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress, int type, int index, int result) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer(10);
+ response.writeByte(SERVICE_RESPONSE);
+ response.writeByte(MSG_ACK);
+ response.writeShort(index);
+ response.writeByte(type);
+ response.writeByte(result);
+ response.writeByte(0);
+ response.writeMedium(0);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ private Position decodePosition(DeviceSession deviceSession, int type, ByteBuf buf) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+ if (type != MSG_MINI_EVENT_REPORT) {
+ buf.readUnsignedInt(); // fix time
+ }
+ position.setLatitude(buf.readInt() * 0.0000001);
+ position.setLongitude(buf.readInt() * 0.0000001);
+ if (type != MSG_MINI_EVENT_REPORT) {
+ position.setAltitude(buf.readInt() * 0.01);
+ position.setSpeed(UnitsConverter.knotsFromCps(buf.readUnsignedInt()));
+ }
+ position.setCourse(buf.readShort());
+ if (type == MSG_MINI_EVENT_REPORT) {
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ }
+
+ if (type == MSG_MINI_EVENT_REPORT) {
+ position.set(Position.KEY_SATELLITES, buf.getUnsignedByte(buf.readerIndex()) & 0xf);
+ position.setValid((buf.readUnsignedByte() & 0x20) == 0);
+ } else {
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.setValid((buf.readUnsignedByte() & 0x08) == 0);
+ }
+
+ if (type != MSG_MINI_EVENT_REPORT) {
+ position.set("carrier", buf.readUnsignedShort());
+ position.set(Position.KEY_RSSI, buf.readShort());
+ }
+
+ position.set("modem", buf.readUnsignedByte());
+
+ if (type != MSG_MINI_EVENT_REPORT) {
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte());
+ }
+
+ int input = buf.readUnsignedByte();
+ position.set(Position.KEY_INPUT, input);
+ position.set(Position.KEY_IGNITION, BitUtil.check(input, 0));
+
+ if (type != MSG_MINI_EVENT_REPORT) {
+ position.set(Position.KEY_STATUS, buf.readUnsignedByte());
+ }
+
+ if (type == MSG_EVENT_REPORT || type == MSG_MINI_EVENT_REPORT) {
+ if (type != MSG_MINI_EVENT_REPORT) {
+ buf.readUnsignedByte(); // event index
+ }
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+ }
+
+ int accType = BitUtil.from(buf.getUnsignedByte(buf.readerIndex()), 6);
+ int accCount = BitUtil.to(buf.readUnsignedByte(), 6);
+
+ if (type != MSG_MINI_EVENT_REPORT) {
+ position.set("append", buf.readUnsignedByte());
+ }
+
+ if (accType == 1) {
+ buf.readUnsignedInt(); // threshold
+ buf.readUnsignedInt(); // mask
+ }
+
+ for (int i = 0; i < accCount; i++) {
+ if (buf.readableBytes() >= 4) {
+ position.set("acc" + i, buf.readUnsignedInt());
+ }
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (BitUtil.check(buf.getByte(buf.readerIndex()), 7)) {
+
+ int content = buf.readUnsignedByte();
+
+ if (BitUtil.check(content, 0)) {
+ String id = ByteBufUtil.hexDump(buf.readSlice(buf.readUnsignedByte()));
+ getDeviceSession(channel, remoteAddress, id);
+ }
+
+ if (BitUtil.check(content, 1)) {
+ buf.skipBytes(buf.readUnsignedByte()); // identifier type
+ }
+
+ if (BitUtil.check(content, 2)) {
+ buf.skipBytes(buf.readUnsignedByte()); // authentication
+ }
+
+ if (BitUtil.check(content, 3)) {
+ buf.skipBytes(buf.readUnsignedByte()); // routing
+ }
+
+ if (BitUtil.check(content, 4)) {
+ buf.skipBytes(buf.readUnsignedByte()); // forwarding
+ }
+
+ if (BitUtil.check(content, 5)) {
+ buf.skipBytes(buf.readUnsignedByte()); // response redirection
+ }
+
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int service = buf.readUnsignedByte();
+ int type = buf.readUnsignedByte();
+ int index = buf.readUnsignedShort();
+
+ if (service == SERVICE_ACKNOWLEDGED) {
+ sendResponse(channel, remoteAddress, type, index, 0);
+ }
+
+ if (type == MSG_EVENT_REPORT || type == MSG_LOCATE_REPORT || type == MSG_MINI_EVENT_REPORT) {
+ return decodePosition(deviceSession, type, buf);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CarTrackProtocol.java b/src/main/java/org/traccar/protocol/CarTrackProtocol.java
new file mode 100644
index 000000000..e340fba25
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CarTrackProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class CarTrackProtocol extends BaseProtocol {
+
+ public CarTrackProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "##"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new CarTrackProtocolDecoder(CarTrackProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CarTrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/CarTrackProtocolDecoder.java
new file mode 100644
index 000000000..ce3345826
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CarTrackProtocolDecoder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2014 Rohit
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class CarTrackProtocolDecoder extends BaseProtocolDecoder {
+
+ public CarTrackProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$$") // header
+ .number("(d+)") // device id
+ .text("?").expression("*")
+ .text("&A")
+ .number("(dddd)") // command
+ .text("&B")
+ .number("(dd)(dd)(dd).(ddd),") // time (hhmmss.sss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.dddd),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.dddd),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d*)?,") // speed
+ .number("(d+.d*)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .expression("&C([^&]*)") // io
+ .expression("&D([^&]*)") // odometer
+ .expression("&E([^&]*)") // alarm
+ .expression("&Y([^&]*)").optional() // adc
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_COMMAND, parser.next());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.PREFIX_IO + 1, parser.next());
+
+ String odometer = parser.next();
+ odometer = odometer.replace(":", "A");
+ odometer = odometer.replace(";", "B");
+ odometer = odometer.replace("<", "C");
+ odometer = odometer.replace("=", "D");
+ odometer = odometer.replace(">", "E");
+ odometer = odometer.replace("?", "F");
+ position.set(Position.KEY_ODOMETER, Integer.parseInt(odometer, 16));
+
+ parser.next(); // there is no meaningful alarms
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CarcellProtocol.java b/src/main/java/org/traccar/protocol/CarcellProtocol.java
new file mode 100644
index 000000000..0c305efcb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CarcellProtocol.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class CarcellProtocol extends BaseProtocol {
+
+ public CarcellProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '\r'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new CarcellProtocolEncoder());
+ pipeline.addLast(new CarcellProtocolDecoder(CarcellProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CarcellProtocolDecoder.java b/src/main/java/org/traccar/protocol/CarcellProtocolDecoder.java
new file mode 100644
index 000000000..344b2f1ea
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CarcellProtocolDecoder.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.Parser.CoordinateFormat;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+public class CarcellProtocolDecoder extends BaseProtocolDecoder {
+
+ public CarcellProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("([$%])") // memory flag
+ .number("(d+),") // imei
+ .groupBegin()
+ .number("([NS])(dd)(dd).(dddd),") // latitude
+ .number("([EW])(ddd)(dd).(dddd),") // longitude
+ .or()
+ .text("CEL,")
+ .number("([NS])(d+.d+),") // latitude
+ .number("([EW])(d+.d+),") // longitude
+ .groupEnd()
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .groupBegin()
+ .number("([-+]ddd)([-+]ddd)([-+]ddd),") // x,y,z
+ .or()
+ .number("(d+),") // accel
+ .groupEnd()
+ .number("(d+),") // battery
+ .number("(d+),") // csq
+ .number("(d),") // jamming
+ .number("(d+),") // hdop
+ .expression("([CG]),?") // clock type
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d),") // block
+ .number("(d),") // ignition
+ .groupBegin()
+ .number("(d),") // cloned
+ .expression("([AF])") // panic
+ .number("(d),") // painel
+ .number("(d+),") // battery voltage
+ .or()
+ .number("(dd),") // time until delivery
+ .expression("([AF])") // panic
+ .number("(d),") // aux
+ .number("(d{2,4}),") // battery voltage
+ .number("(d{20}),") // ccid
+ .groupEnd()
+ .number("(xx)") // crc
+ .any() // full format
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.set(Position.KEY_ARCHIVE, parser.next().equals("%"));
+ position.setValid(true);
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.hasNext(8)) {
+ position.setLatitude(parser.nextCoordinate(CoordinateFormat.HEM_DEG_MIN_MIN));
+ position.setLongitude(parser.nextCoordinate(CoordinateFormat.HEM_DEG_MIN_MIN));
+ }
+
+ if (parser.hasNext(4)) {
+ position.setLatitude(parser.nextCoordinate(CoordinateFormat.HEM_DEG));
+ position.setLongitude(parser.nextCoordinate(CoordinateFormat.HEM_DEG));
+ }
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt(0)));
+ position.setCourse(parser.nextInt(0));
+
+ if (parser.hasNext(3)) {
+ position.set("x", parser.nextInt(0));
+ position.set("y", parser.nextInt(0));
+ position.set("z", parser.nextInt(0));
+ }
+
+ if (parser.hasNext(1)) {
+ position.set(Position.KEY_ACCELERATION, parser.nextInt(0));
+ }
+
+ Double internalBattery = (parser.nextDouble(0) + 100d) * 0.0294d;
+ position.set(Position.KEY_BATTERY, internalBattery);
+ position.set(Position.KEY_RSSI, parser.nextInt(0));
+ position.set("jamming", parser.next().equals("1"));
+ position.set(Position.KEY_GPS, parser.nextInt(0));
+
+ position.set("clockType", parser.next());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.set("blocked", parser.next().equals("1"));
+ position.set(Position.KEY_IGNITION, parser.next().equals("1"));
+
+ if (parser.hasNext(4)) {
+ position.set("cloned", parser.next().equals("1"));
+
+ parser.next(); // panic button status
+
+ String painelStatus = parser.next();
+ if (painelStatus.equals("1")) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ position.set("painel", painelStatus.equals("2"));
+
+ Double mainVoltage = parser.nextDouble(0) / 100d;
+ position.set(Position.KEY_POWER, mainVoltage);
+ }
+
+ if (parser.hasNext(5)) {
+ position.set("timeUntilDelivery", parser.nextInt(0));
+ parser.next(); // panic button status
+ position.set(Position.KEY_INPUT, parser.next());
+
+ Double mainVoltage = parser.nextDouble(0) / 100d;
+ position.set(Position.KEY_POWER, mainVoltage);
+
+ position.set("iccid", parser.next());
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CarcellProtocolEncoder.java b/src/main/java/org/traccar/protocol/CarcellProtocolEncoder.java
new file mode 100644
index 000000000..e8f0081a0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CarcellProtocolEncoder.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class CarcellProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, "$SRVCMD,{%s},BA#\r\n", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, "$SRVCMD,{%s},BD#\r\n", Command.KEY_UNIQUE_ID);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CarscopProtocol.java b/src/main/java/org/traccar/protocol/CarscopProtocol.java
new file mode 100644
index 000000000..2c754a97f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CarscopProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class CarscopProtocol extends BaseProtocol {
+
+ public CarscopProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '^'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new CarscopProtocolDecoder(CarscopProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CarscopProtocolDecoder.java b/src/main/java/org/traccar/protocol/CarscopProtocolDecoder.java
new file mode 100644
index 000000000..161666adc
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CarscopProtocolDecoder.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class CarscopProtocolDecoder extends BaseProtocolDecoder {
+
+ public CarscopProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("*")
+ .any()
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .expression("([AV])") // validity
+ .number("(dd)(dd.dddd)") // latitude
+ .expression("([NS])")
+ .number("(ddd)(dd.dddd)") // longitude
+ .expression("([EW])")
+ .number("(ddd.d)") // speed
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(ddd.dd)") // course
+ .groupBegin()
+ .number("(d{8})") // state
+ .number("L(d{6})") // odometer
+ .groupEnd("?")
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ DeviceSession deviceSession;
+ int index = sentence.indexOf("UB05");
+ if (index != -1) {
+ String imei = sentence.substring(index + 4, index + 4 + 15);
+ deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ } else {
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ }
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+
+ dateBuilder.setDate(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.setCourse(parser.nextDouble(0));
+
+ if (parser.hasNext(2)) {
+ position.set(Position.KEY_STATUS, parser.next());
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CastelProtocol.java b/src/main/java/org/traccar/protocol/CastelProtocol.java
new file mode 100644
index 000000000..9b854afc3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CastelProtocol.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+import java.nio.ByteOrder;
+public class CastelProtocol extends BaseProtocol {
+
+ public CastelProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 2, 2, -4, 0, true));
+ pipeline.addLast(new CastelProtocolEncoder());
+ pipeline.addLast(new CastelProtocolDecoder(CastelProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CastelProtocolEncoder());
+ pipeline.addLast(new CastelProtocolDecoder(CastelProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CastelProtocolDecoder.java b/src/main/java/org/traccar/protocol/CastelProtocolDecoder.java
new file mode 100644
index 000000000..0541adf6f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CastelProtocolDecoder.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.ObdDecoder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class CastelProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final Map<Integer, Integer> PID_LENGTH_MAP = new HashMap<>();
+
+ static {
+ int[] l1 = {
+ 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0b, 0x0d,
+ 0x0e, 0x0f, 0x11, 0x12, 0x13, 0x1c, 0x1d, 0x1e, 0x2c,
+ 0x2d, 0x2e, 0x2f, 0x30, 0x33, 0x43, 0x45, 0x46,
+ 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x51, 0x52,
+ 0x5a
+ };
+ int[] l2 = {
+ 0x02, 0x03, 0x0a, 0x0c, 0x10, 0x14, 0x15, 0x16,
+ 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1f, 0x21, 0x22,
+ 0x23, 0x31, 0x32, 0x3c, 0x3d, 0x3e, 0x3f, 0x42,
+ 0x44, 0x4d, 0x4e, 0x50, 0x53, 0x54, 0x55, 0x56,
+ 0x57, 0x58, 0x59
+ };
+ int[] l4 = {
+ 0x00, 0x01, 0x20, 0x24, 0x25, 0x26, 0x27, 0x28,
+ 0x29, 0x2a, 0x2b, 0x34, 0x35, 0x36, 0x37, 0x38,
+ 0x39, 0x3a, 0x3b, 0x40, 0x41, 0x4f
+ };
+ for (int i : l1) {
+ PID_LENGTH_MAP.put(i, 1);
+ }
+ for (int i : l2) {
+ PID_LENGTH_MAP.put(i, 2);
+ }
+ for (int i : l4) {
+ PID_LENGTH_MAP.put(i, 4);
+ }
+ }
+
+ public CastelProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final short MSG_SC_LOGIN = 0x1001;
+ public static final short MSG_SC_LOGIN_RESPONSE = (short) 0x9001;
+ public static final short MSG_SC_LOGOUT = 0x1002;
+ public static final short MSG_SC_HEARTBEAT = 0x1003;
+ public static final short MSG_SC_HEARTBEAT_RESPONSE = (short) 0x9003;
+ public static final short MSG_SC_GPS = 0x4001;
+ public static final short MSG_SC_PID_DATA = 0x4002;
+ public static final short MSG_SC_SUPPORTED_PID = 0x4004;
+ public static final short MSG_SC_OBD_DATA = 0x4005;
+ public static final short MSG_SC_DTCS_PASSENGER = 0x4006;
+ public static final short MSG_SC_DTCS_COMMERCIAL = 0x400B;
+ public static final short MSG_SC_ALARM = 0x4007;
+ public static final short MSG_SC_CELL = 0x4008;
+ public static final short MSG_SC_GPS_SLEEP = 0x4009;
+ public static final short MSG_SC_FUEL = 0x400E;
+ public static final short MSG_SC_AGPS_REQUEST = 0x5101;
+ public static final short MSG_SC_QUERY_RESPONSE = (short) 0xA002;
+ public static final short MSG_SC_CURRENT_LOCATION = (short) 0xB001;
+
+ public static final short MSG_CC_LOGIN = 0x4001;
+ public static final short MSG_CC_LOGIN_RESPONSE = (short) 0x8001;
+ public static final short MSG_CC_HEARTBEAT = 0x4206;
+ public static final short MSG_CC_PETROL_CONTROL = 0x4583;
+ public static final short MSG_CC_HEARTBEAT_RESPONSE = (short) 0x8206;
+
+ private Position readPosition(DeviceSession deviceSession, ByteBuf buf) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDateReverse(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ double lat = buf.readUnsignedIntLE() / 3600000.0;
+ double lon = buf.readUnsignedIntLE() / 3600000.0;
+ position.setSpeed(UnitsConverter.knotsFromCps(buf.readUnsignedShortLE()));
+ position.setCourse(buf.readUnsignedShortLE() * 0.1);
+
+ int flags = buf.readUnsignedByte();
+ if ((flags & 0x02) == 0) {
+ lat = -lat;
+ }
+ if ((flags & 0x01) == 0) {
+ lon = -lon;
+ }
+ position.setLatitude(lat);
+ position.setLongitude(lon);
+ position.setValid((flags & 0x0C) > 0);
+ position.set(Position.KEY_SATELLITES, flags >> 4);
+
+ return position;
+ }
+
+ private Position createPosition(DeviceSession deviceSession) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ return position;
+ }
+
+ private void decodeObd(Position position, ByteBuf buf, boolean groups) {
+
+ int count = buf.readUnsignedByte();
+
+ int[] pids = new int[count];
+ for (int i = 0; i < count; i++) {
+ pids[i] = buf.readUnsignedShortLE() & 0xff;
+ }
+
+ if (groups) {
+ buf.readUnsignedByte(); // group count
+ buf.readUnsignedByte(); // group size
+ }
+
+ for (int i = 0; i < count; i++) {
+ int value;
+ switch (PID_LENGTH_MAP.get(pids[i])) {
+ case 1:
+ value = buf.readUnsignedByte();
+ break;
+ case 2:
+ value = buf.readUnsignedShortLE();
+ break;
+ case 4:
+ value = buf.readIntLE();
+ break;
+ default:
+ value = 0;
+ break;
+ }
+ position.add(ObdDecoder.decodeData(pids[i], value, false));
+ }
+ }
+
+ private void decodeStat(Position position, ByteBuf buf) {
+
+ buf.readUnsignedIntLE(); // ACC ON time
+ buf.readUnsignedIntLE(); // UTC time
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedIntLE());
+ position.set(Position.KEY_FUEL_CONSUMPTION, buf.readUnsignedIntLE());
+ buf.readUnsignedShortLE(); // current fuel consumption
+ position.set(Position.KEY_STATUS, buf.readUnsignedIntLE());
+ buf.skipBytes(8);
+ }
+
+ private void sendResponse(
+ Channel channel, SocketAddress remoteAddress,
+ int version, ByteBuf id, short type, ByteBuf content) {
+
+ if (channel != null) {
+ int length = 2 + 2 + 1 + id.readableBytes() + 2 + 2 + 2;
+ if (content != null) {
+ length += content.readableBytes();
+ }
+
+ ByteBuf response = Unpooled.buffer(length);
+ response.writeByte('@'); response.writeByte('@');
+ response.writeShortLE(length);
+ response.writeByte(version);
+ response.writeBytes(id);
+ response.writeShort(type);
+ if (content != null) {
+ response.writeBytes(content);
+ content.release();
+ }
+ response.writeShortLE(
+ Checksum.crc16(Checksum.CRC16_X25, response.nioBuffer(0, response.writerIndex())));
+ response.writeByte(0x0D); response.writeByte(0x0A);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ private void sendResponse(
+ Channel channel, SocketAddress remoteAddress, ByteBuf id, short type) {
+
+ if (channel != null) {
+ int length = 2 + 2 + id.readableBytes() + 2 + 4 + 8 + 2 + 2;
+
+ ByteBuf response = Unpooled.buffer(length);
+ response.writeByte('@'); response.writeByte('@');
+ response.writeShortLE(length);
+ response.writeBytes(id);
+ response.writeShort(type);
+ response.writeIntLE(0);
+ for (int i = 0; i < 8; i++) {
+ response.writeByte(0xff);
+ }
+ response.writeShortLE(
+ Checksum.crc16(Checksum.CRC16_X25, response.nioBuffer(0, response.writerIndex())));
+ response.writeByte(0x0D); response.writeByte(0x0A);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ private void decodeAlarm(Position position, int alarm) {
+ switch (alarm) {
+ case 0x01:
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ break;
+ case 0x02:
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_POWER);
+ break;
+ case 0x03:
+ position.set(Position.KEY_ALARM, Position.ALARM_TEMPERATURE);
+ break;
+ case 0x04:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ break;
+ case 0x05:
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ break;
+ case 0x06:
+ position.set(Position.KEY_ALARM, Position.ALARM_IDLE);
+ break;
+ case 0x07:
+ position.set(Position.KEY_ALARM, Position.ALARM_TOW);
+ break;
+ case 0x08:
+ position.set(Position.KEY_ALARM, Position.ALARM_HIGH_RPM);
+ break;
+ case 0x09:
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_ON);
+ break;
+ case 0x0B:
+ position.set(Position.KEY_ALARM, Position.ALARM_LANE_CHANGE);
+ break;
+ case 0x0C:
+ position.set(Position.KEY_ALARM, Position.ALARM_CORNERING);
+ break;
+ case 0x0E:
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_OFF);
+ break;
+ case 0x16:
+ position.set(Position.KEY_IGNITION, true);
+ break;
+ case 0x17:
+ position.set(Position.KEY_IGNITION, false);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private Object decodeSc(
+ Channel channel, SocketAddress remoteAddress, ByteBuf buf,
+ int version, ByteBuf id, short type, DeviceSession deviceSession) {
+
+ if (type == MSG_SC_HEARTBEAT) {
+
+ sendResponse(channel, remoteAddress, version, id, MSG_SC_HEARTBEAT_RESPONSE, null);
+
+ } else if (type == MSG_SC_LOGIN || type == MSG_SC_LOGOUT || type == MSG_SC_GPS
+ || type == MSG_SC_ALARM || type == MSG_SC_CURRENT_LOCATION || type == MSG_SC_FUEL) {
+
+ if (type == MSG_SC_LOGIN) {
+ ByteBuf response = Unpooled.buffer(10);
+ response.writeIntLE(0xFFFFFFFF);
+ response.writeShortLE(0);
+ response.writeIntLE((int) (System.currentTimeMillis() / 1000));
+ sendResponse(channel, remoteAddress, version, id, MSG_SC_LOGIN_RESPONSE, response);
+ }
+
+ if (type == MSG_SC_GPS) {
+ buf.readUnsignedByte(); // historical
+ } else if (type == MSG_SC_ALARM) {
+ buf.readUnsignedIntLE(); // alarm
+ } else if (type == MSG_SC_CURRENT_LOCATION) {
+ buf.readUnsignedShortLE();
+ }
+
+ buf.readUnsignedIntLE(); // ACC ON time
+ buf.readUnsignedIntLE(); // UTC time
+ long odometer = buf.readUnsignedIntLE();
+ long tripOdometer = buf.readUnsignedIntLE();
+ long fuelConsumption = buf.readUnsignedIntLE();
+ buf.readUnsignedShortLE(); // current fuel consumption
+ long status = buf.readUnsignedIntLE();
+ buf.skipBytes(8);
+
+ int count = buf.readUnsignedByte();
+
+ List<Position> positions = new LinkedList<>();
+
+ for (int i = 0; i < count; i++) {
+ Position position = readPosition(deviceSession, buf);
+ position.set(Position.KEY_ODOMETER, odometer);
+ position.set(Position.KEY_ODOMETER_TRIP, tripOdometer);
+ position.set(Position.KEY_FUEL_CONSUMPTION, fuelConsumption);
+ position.set(Position.KEY_STATUS, status);
+ positions.add(position);
+ }
+
+ if (type == MSG_SC_ALARM) {
+ int alarmCount = buf.readUnsignedByte();
+ for (int i = 0; i < alarmCount; i++) {
+ if (buf.readUnsignedByte() != 0) {
+ int alarm = buf.readUnsignedByte();
+ for (Position position : positions) {
+ decodeAlarm(position, alarm);
+ }
+ buf.readUnsignedShortLE(); // description
+ buf.readUnsignedShortLE(); // threshold
+ }
+ }
+ } else if (type == MSG_SC_FUEL) {
+ for (Position position : positions) {
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShortLE());
+ }
+ }
+
+ if (!positions.isEmpty()) {
+ return positions;
+ }
+
+ } else if (type == MSG_SC_GPS_SLEEP) {
+
+ buf.readUnsignedIntLE(); // device time
+
+ return readPosition(deviceSession, buf);
+
+ } else if (type == MSG_SC_AGPS_REQUEST) {
+
+ return readPosition(deviceSession, buf);
+
+ } else if (type == MSG_SC_PID_DATA) {
+
+ Position position = createPosition(deviceSession);
+
+ decodeStat(position, buf);
+
+ buf.readUnsignedShortLE(); // sample rate
+ decodeObd(position, buf, true);
+
+ return position;
+
+ } else if (type == MSG_SC_DTCS_PASSENGER) {
+
+ Position position = createPosition(deviceSession);
+
+ decodeStat(position, buf);
+
+ buf.readUnsignedByte(); // flag
+ position.add(ObdDecoder.decodeCodes(ByteBufUtil.hexDump(buf.readSlice(buf.readUnsignedByte()))));
+
+ return position;
+
+ } else if (type == MSG_SC_OBD_DATA) {
+
+ Position position = createPosition(deviceSession);
+
+ decodeStat(position, buf);
+
+ buf.readUnsignedByte(); // flag
+ decodeObd(position, buf, false);
+
+ return position;
+
+ } else if (type == MSG_SC_CELL) {
+
+ Position position = createPosition(deviceSession);
+
+ decodeStat(position, buf);
+
+ position.setNetwork(new Network(
+ CellTower.fromLacCid(buf.readUnsignedShortLE(), buf.readUnsignedShortLE())));
+
+ return position;
+
+ } else if (type == MSG_SC_QUERY_RESPONSE) {
+
+ Position position = createPosition(deviceSession);
+
+ buf.readUnsignedShortLE(); // index
+ buf.readUnsignedByte(); // response count
+ buf.readUnsignedByte(); // response index
+
+ int failureCount = buf.readUnsignedByte();
+ for (int i = 0; i < failureCount; i++) {
+ buf.readUnsignedShortLE(); // tag
+ }
+
+ int successCount = buf.readUnsignedByte();
+ for (int i = 0; i < successCount; i++) {
+ buf.readUnsignedShortLE(); // tag
+ position.set(Position.KEY_RESULT,
+ buf.readSlice(buf.readUnsignedShortLE()).toString(StandardCharsets.US_ASCII));
+ }
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+ private Object decodeCc(
+ Channel channel, SocketAddress remoteAddress, ByteBuf buf,
+ int version, ByteBuf id, short type, DeviceSession deviceSession) {
+
+ if (type == MSG_CC_HEARTBEAT) {
+
+ sendResponse(channel, remoteAddress, version, id, MSG_CC_HEARTBEAT_RESPONSE, null);
+
+ buf.readUnsignedByte(); // 0x01 for history
+ int count = buf.readUnsignedByte();
+
+ List<Position> positions = new LinkedList<>();
+
+ for (int i = 0; i < count; i++) {
+ Position position = readPosition(deviceSession, buf);
+
+ position.set(Position.KEY_STATUS, buf.readUnsignedIntLE());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte());
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+
+ buf.readUnsignedByte(); // geo-fencing id
+ buf.readUnsignedByte(); // geo-fencing flags
+ buf.readUnsignedByte(); // additional flags
+
+ position.setNetwork(new Network(
+ CellTower.fromLacCid(buf.readUnsignedShortLE(), buf.readUnsignedShortLE())));
+
+ positions.add(position);
+ }
+
+ return positions;
+
+ } else if (type == MSG_CC_LOGIN) {
+
+ sendResponse(channel, remoteAddress, version, id, MSG_CC_LOGIN_RESPONSE, null);
+
+ Position position = readPosition(deviceSession, buf);
+
+ position.set(Position.KEY_STATUS, buf.readUnsignedIntLE());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte());
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+
+ buf.readUnsignedByte(); // geo-fencing id
+ buf.readUnsignedByte(); // geo-fencing flags
+ buf.readUnsignedByte(); // additional flags
+
+ // GSM_CELL_CODE
+ // STR_Z - firmware version
+ // STR_Z - hardware version
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+ private Object decodeMpip(
+ Channel channel, SocketAddress remoteAddress, ByteBuf buf,
+ int version, ByteBuf id, short type, DeviceSession deviceSession) {
+
+ if (type == 0x4001) {
+
+ sendResponse(channel, remoteAddress, version, id, (short) type, null);
+
+ return readPosition(deviceSession, buf);
+
+ } else if (type == 0x2001) {
+
+ sendResponse(channel, remoteAddress, id, (short) 0x1001);
+
+ buf.readUnsignedIntLE(); // index
+ buf.readUnsignedIntLE(); // unix time
+ buf.readUnsignedByte();
+
+ return readPosition(deviceSession, buf);
+
+ } else if (type == 0x4201 || type == 0x4202 || type == 0x4206) {
+
+ return readPosition(deviceSession, buf);
+
+ } else if (type == 0x4204) {
+
+ List<Position> positions = new LinkedList<>();
+
+ for (int i = 0; i < 8; i++) {
+ Position position = readPosition(deviceSession, buf);
+ buf.skipBytes(31);
+ positions.add(position);
+ }
+
+ return positions;
+
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int header = buf.readUnsignedShortLE();
+ buf.readUnsignedShortLE(); // length
+
+ int version = -1;
+ if (header == 0x4040) {
+ version = buf.readUnsignedByte();
+ }
+
+ ByteBuf id = buf.readSlice(20);
+ short type = buf.readShort();
+
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, id.toString(StandardCharsets.US_ASCII).trim());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ switch (version) {
+ case -1:
+ return decodeMpip(channel, remoteAddress, buf, version, id, type, deviceSession);
+ case 3:
+ case 4:
+ return decodeSc(channel, remoteAddress, buf, version, id, type, deviceSession);
+ default:
+ return decodeCc(channel, remoteAddress, buf, version, id, type, deviceSession);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CastelProtocolEncoder.java b/src/main/java/org/traccar/protocol/CastelProtocolEncoder.java
new file mode 100644
index 000000000..e1f78e7c1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CastelProtocolEncoder.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.Context;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class CastelProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeContent(long deviceId, short type, ByteBuf content) {
+
+ ByteBuf buf = Unpooled.buffer(0);
+ String uniqueId = Context.getIdentityManager().getById(deviceId).getUniqueId();
+
+ buf.writeByte('@');
+ buf.writeByte('@');
+
+ buf.writeShortLE(2 + 2 + 1 + 20 + 2 + content.readableBytes() + 2 + 2); // length
+
+ buf.writeByte(1); // protocol version
+
+ buf.writeBytes(uniqueId.getBytes(StandardCharsets.US_ASCII));
+ buf.writeZero(20 - uniqueId.length());
+
+ buf.writeShort(type);
+ buf.writeBytes(content);
+
+ buf.writeShortLE(Checksum.crc16(Checksum.CRC16_X25, buf.nioBuffer()));
+
+ buf.writeByte('\r');
+ buf.writeByte('\n');
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+ ByteBuf content = Unpooled.buffer(0);
+ switch (command.getType()) {
+ case Command.TYPE_ENGINE_STOP:
+ content.writeByte(1);
+ return encodeContent(command.getDeviceId(), CastelProtocolDecoder.MSG_CC_PETROL_CONTROL, content);
+ case Command.TYPE_ENGINE_RESUME:
+ content.writeByte(0);
+ return encodeContent(command.getDeviceId(), CastelProtocolDecoder.MSG_CC_PETROL_CONTROL, content);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CautelaProtocol.java b/src/main/java/org/traccar/protocol/CautelaProtocol.java
new file mode 100644
index 000000000..452bdf8d4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CautelaProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class CautelaProtocol extends BaseProtocol {
+
+ public CautelaProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new CautelaProtocolDecoder(CautelaProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CautelaProtocolDecoder.java b/src/main/java/org/traccar/protocol/CautelaProtocolDecoder.java
new file mode 100644
index 000000000..bddf19b41
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CautelaProtocolDecoder.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class CautelaProtocolDecoder extends BaseProtocolDecoder {
+
+ public CautelaProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(d+),") // type
+ .number("(d+),") // imei
+ .number("(dd),(dd),(dd),") // date (ddmmyy)
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(dd)(dd),") // time (hhmm)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ parser.next(); // type
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder();
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setValid(true);
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+
+ dateBuilder.setHour(parser.nextInt()).setMinute(parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CellocatorFrameDecoder.java b/src/main/java/org/traccar/protocol/CellocatorFrameDecoder.java
new file mode 100644
index 000000000..7d5499d92
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CellocatorFrameDecoder.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class CellocatorFrameDecoder extends BaseFrameDecoder {
+
+ private static final int MESSAGE_MINIMUM_LENGTH = 15;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < MESSAGE_MINIMUM_LENGTH) {
+ return null;
+ }
+
+ int length = 0;
+ int type = buf.getUnsignedByte(4);
+ switch (type) {
+ case CellocatorProtocolDecoder.MSG_CLIENT_STATUS:
+ length = 70;
+ break;
+ case CellocatorProtocolDecoder.MSG_CLIENT_PROGRAMMING:
+ length = 31;
+ break;
+ case CellocatorProtocolDecoder.MSG_CLIENT_SERIAL_LOG:
+ length = 70;
+ break;
+ case CellocatorProtocolDecoder.MSG_CLIENT_SERIAL:
+ if (buf.readableBytes() >= 19) {
+ length = 19 + buf.getUnsignedShortLE(16);
+ }
+ break;
+ case CellocatorProtocolDecoder.MSG_CLIENT_MODULAR:
+ length = 15 + buf.getUnsignedByte(13);
+ break;
+ default:
+ break;
+ }
+
+ if (length > 0 && buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CellocatorProtocol.java b/src/main/java/org/traccar/protocol/CellocatorProtocol.java
new file mode 100644
index 000000000..a52170dc9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CellocatorProtocol.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class CellocatorProtocol extends BaseProtocol {
+
+ public CellocatorProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_OUTPUT_CONTROL);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CellocatorFrameDecoder());
+ pipeline.addLast(new CellocatorProtocolEncoder());
+ pipeline.addLast(new CellocatorProtocolDecoder(CellocatorProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CellocatorProtocolEncoder());
+ pipeline.addLast(new CellocatorProtocolDecoder(CellocatorProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CellocatorProtocolDecoder.java b/src/main/java/org/traccar/protocol/CellocatorProtocolDecoder.java
new file mode 100644
index 000000000..d23f76a93
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CellocatorProtocolDecoder.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2013 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class CellocatorProtocolDecoder extends BaseProtocolDecoder {
+
+ public CellocatorProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ static final int MSG_CLIENT_STATUS = 0;
+ static final int MSG_CLIENT_PROGRAMMING = 3;
+ static final int MSG_CLIENT_SERIAL_LOG = 7;
+ static final int MSG_CLIENT_SERIAL = 8;
+ static final int MSG_CLIENT_MODULAR = 9;
+
+ public static final int MSG_SERVER_ACKNOWLEDGE = 4;
+
+ private byte commandCount;
+
+ private void sendReply(Channel channel, SocketAddress remoteAddress, long deviceId, byte packetNumber) {
+ if (channel != null) {
+ ByteBuf reply = Unpooled.buffer(28);
+ reply.writeByte('M');
+ reply.writeByte('C');
+ reply.writeByte('G');
+ reply.writeByte('P');
+ reply.writeByte(MSG_SERVER_ACKNOWLEDGE);
+ reply.writeIntLE((int) deviceId);
+ reply.writeByte(commandCount++);
+ reply.writeIntLE(0); // authentication code
+ reply.writeByte(0);
+ reply.writeByte(packetNumber);
+ reply.writeZero(11);
+
+ byte checksum = 0;
+ for (int i = 4; i < 27; i++) {
+ checksum += reply.getByte(i);
+ }
+ reply.writeByte(checksum);
+
+ channel.writeAndFlush(new NetworkMessage(reply, remoteAddress));
+ }
+ }
+
+ private String decodeAlarm(short reason) {
+ switch (reason) {
+ case 70:
+ return Position.ALARM_SOS;
+ case 80:
+ return Position.ALARM_POWER_CUT;
+ case 81:
+ return Position.ALARM_LOW_POWER;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ boolean alternative = buf.getByte(buf.readerIndex() + 3) != 'P';
+
+ buf.skipBytes(4); // system code
+ int type = buf.readUnsignedByte();
+ long deviceUniqueId = buf.readUnsignedIntLE();
+
+ if (type != MSG_CLIENT_SERIAL) {
+ buf.readUnsignedShortLE(); // communication control
+ }
+ byte packetNumber = buf.readByte();
+
+ sendReply(channel, remoteAddress, deviceUniqueId, packetNumber);
+
+ if (type == MSG_CLIENT_STATUS) {
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(deviceUniqueId));
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_VERSION_HW, buf.readUnsignedByte());
+ position.set(Position.KEY_VERSION_FW, buf.readUnsignedByte());
+ buf.readUnsignedByte(); // protocol version
+
+ position.set(Position.KEY_STATUS, buf.readUnsignedByte() & 0x0f);
+
+ buf.readUnsignedByte(); // operator / configuration flags
+ buf.readUnsignedByte(); // reason data
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte()));
+
+ position.set("mode", buf.readUnsignedByte());
+ position.set(Position.KEY_INPUT, buf.readUnsignedIntLE());
+
+ if (alternative) {
+ buf.readUnsignedByte(); // input
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShortLE());
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShortLE());
+ } else {
+ buf.readUnsignedByte(); // operator
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedIntLE());
+ }
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedMediumLE());
+
+ buf.skipBytes(6); // multi-purpose data
+ buf.readUnsignedShortLE(); // fix time
+ buf.readUnsignedByte(); // location status
+ buf.readUnsignedByte(); // mode 1
+ buf.readUnsignedByte(); // mode 2
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ position.setValid(true);
+
+ if (alternative) {
+ position.setLongitude(buf.readIntLE() / 10000000.0);
+ position.setLatitude(buf.readIntLE() / 10000000.0);
+ } else {
+ position.setLongitude(buf.readIntLE() / Math.PI * 180 / 100000000);
+ position.setLatitude(buf.readIntLE() / Math.PI * 180 / 100000000);
+ }
+
+ position.setAltitude(buf.readIntLE() * 0.01);
+
+ if (alternative) {
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedIntLE()));
+ position.setCourse(buf.readUnsignedShortLE() / 1000.0);
+ } else {
+ position.setSpeed(UnitsConverter.knotsFromMps(buf.readUnsignedIntLE() * 0.01));
+ position.setCourse(buf.readUnsignedShortLE() / Math.PI * 180.0 / 1000.0);
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTimeReverse(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setDateReverse(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedShortLE());
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CellocatorProtocolEncoder.java b/src/main/java/org/traccar/protocol/CellocatorProtocolEncoder.java
new file mode 100644
index 000000000..0382dbbc7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CellocatorProtocolEncoder.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.model.Command;
+
+public class CellocatorProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeContent(long deviceId, int command, int data1, int data2) {
+
+ ByteBuf buf = Unpooled.buffer(0);
+ buf.writeByte('M');
+ buf.writeByte('C');
+ buf.writeByte('G');
+ buf.writeByte('P');
+ buf.writeByte(0);
+ buf.writeIntLE(Integer.parseInt(getUniqueId(deviceId)));
+ buf.writeByte(0); // command numerator
+ buf.writeIntLE(0); // authentication code
+ buf.writeByte(command);
+ buf.writeByte(command);
+ buf.writeByte(data1);
+ buf.writeByte(data1);
+ buf.writeByte(data2);
+ buf.writeByte(data2);
+ buf.writeIntLE(0); // command specific data
+
+ byte checksum = 0;
+ for (int i = 4; i < buf.writerIndex(); i++) {
+ checksum += buf.getByte(i);
+ }
+ buf.writeByte(checksum);
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_OUTPUT_CONTROL:
+ int data = Integer.parseInt(command.getString(Command.KEY_DATA)) << 4
+ + command.getInteger(Command.KEY_INDEX);
+ return encodeContent(command.getDeviceId(), 0x03, data, 0);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CguardProtocol.java b/src/main/java/org/traccar/protocol/CguardProtocol.java
new file mode 100644
index 000000000..9157ca35c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CguardProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class CguardProtocol extends BaseProtocol {
+
+ public CguardProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new CguardProtocolDecoder(CguardProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CguardProtocolDecoder.java b/src/main/java/org/traccar/protocol/CguardProtocolDecoder.java
new file mode 100644
index 000000000..d934921f1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CguardProtocolDecoder.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class CguardProtocolDecoder extends BaseProtocolDecoder {
+
+ public CguardProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_NV = new PatternBuilder()
+ .text("NV:")
+ .number("(dd)(dd)(dd) ") // date (yymmdd)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number(":(-?d+.d+)") // longitude
+ .number(":(-?d+.d+)") // latitude
+ .number(":(d+.?d*)") // speed
+ .number(":(?:NAN|(d+.?d*))") // accuracy
+ .number(":(?:NAN|(d+.?d*))") // course
+ .number(":(?:NAN|(d+.?d*))").optional() // altitude
+ .compile();
+
+ private static final Pattern PATTERN_BC = new PatternBuilder()
+ .text("BC:")
+ .number("(dd)(dd)(dd) ") // date (yymmdd)
+ .number("(dd)(dd)(dd):") // time (hhmmss)
+ .expression("(.+)") // data
+ .compile();
+
+ private Position decodePosition(DeviceSession deviceSession, String sentence) {
+
+ Parser parser = new Parser(PATTERN_NV, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(true);
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+
+ position.setAccuracy(parser.nextDouble(0));
+
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ return position;
+ }
+
+ private Position decodeStatus(DeviceSession deviceSession, String sentence) {
+
+ Parser parser = new Parser(PATTERN_BC, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, parser.nextDateTime());
+
+ String[] data = parser.next().split(":");
+ for (int i = 0; i < data.length / 2; i++) {
+ String key = data[i * 2];
+ String value = data[i * 2 + 1];
+ switch (key) {
+ case "CSQ1":
+ position.set(Position.KEY_RSSI, Integer.parseInt(value));
+ break;
+ case "NSQ1":
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(value));
+ break;
+ case "BAT1":
+ if (value.contains(".")) {
+ position.set(Position.KEY_BATTERY, Double.parseDouble(value));
+ } else {
+ position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(value));
+ }
+ break;
+ case "PWR1":
+ position.set(Position.KEY_POWER, Double.parseDouble(value));
+ break;
+ default:
+ position.set(key.toLowerCase(), value);
+ break;
+ }
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("ID:") || sentence.startsWith("IDRO:")) {
+ getDeviceSession(channel, remoteAddress, sentence.substring(sentence.indexOf(':') + 1));
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (sentence.startsWith("NV:")) {
+ return decodePosition(deviceSession, sentence);
+ } else if (sentence.startsWith("BC:")) {
+ return decodeStatus(deviceSession, sentence);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CityeasyProtocol.java b/src/main/java/org/traccar/protocol/CityeasyProtocol.java
new file mode 100644
index 000000000..f4b49c9ff
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CityeasyProtocol.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class CityeasyProtocol extends BaseProtocol {
+
+ public CityeasyProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_POSITION_PERIODIC,
+ Command.TYPE_POSITION_STOP,
+ Command.TYPE_SET_TIMEZONE);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 2, 2, -4, 0));
+ pipeline.addLast(new CityeasyProtocolEncoder());
+ pipeline.addLast(new CityeasyProtocolDecoder(CityeasyProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CityeasyProtocolDecoder.java b/src/main/java/org/traccar/protocol/CityeasyProtocolDecoder.java
new file mode 100644
index 000000000..9c4c7e11d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CityeasyProtocolDecoder.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+public class CityeasyProtocolDecoder extends BaseProtocolDecoder {
+
+ public CityeasyProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .groupBegin()
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("([AV]),") // validity
+ .number("(d+),") // satellites
+ .number("([NS]),(d+.d+),") // latitude
+ .number("([EW]),(d+.d+),") // longitude
+ .number("(d+.d),") // speed
+ .number("(d+.d),") // hdop
+ .number("(d+.d)") // altitude
+ .groupEnd("?").text(";")
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(d+),") // lac
+ .number("(d+)") // cell
+ .any()
+ .compile();
+
+ public static final int MSG_ADDRESS_REQUEST = 0x0001;
+ public static final int MSG_STATUS = 0x0002;
+ public static final int MSG_LOCATION_REPORT = 0x0003;
+ public static final int MSG_LOCATION_REQUEST = 0x0004;
+ public static final int MSG_LOCATION_INTERVAL = 0x0005;
+ public static final int MSG_PHONE_NUMBER = 0x0006;
+ public static final int MSG_MONITORING = 0x0007;
+ public static final int MSG_TIMEZONE = 0x0008;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ buf.readUnsignedShort(); // length
+
+ String imei = ByteBufUtil.hexDump(buf.readSlice(7));
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, imei, imei + Checksum.luhn(Long.parseLong(imei)));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type = buf.readUnsignedShort();
+
+ if (type == MSG_LOCATION_REPORT || type == MSG_LOCATION_REQUEST) {
+
+ String sentence = buf.toString(buf.readerIndex(), buf.readableBytes() - 8, StandardCharsets.US_ASCII);
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.hasNext(15)) {
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(parser.next().equals("A"));
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+
+ position.setSpeed(parser.nextDouble(0));
+ position.set(Position.KEY_HDOP, parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ } else {
+
+ getLastLocation(position, null);
+
+ }
+
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(0), parser.nextInt(0), parser.nextInt(0), parser.nextInt(0))));
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CityeasyProtocolEncoder.java b/src/main/java/org/traccar/protocol/CityeasyProtocolEncoder.java
new file mode 100644
index 000000000..350fdf0ab
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CityeasyProtocolEncoder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import java.util.TimeZone;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+public class CityeasyProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeContent(int type, ByteBuf content) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeByte('S');
+ buf.writeByte('S');
+ buf.writeShort(2 + 2 + 2 + content.readableBytes() + 4 + 2 + 2);
+ buf.writeShort(type);
+ buf.writeBytes(content);
+ buf.writeInt(0x0B);
+ buf.writeShort(Checksum.crc16(Checksum.CRC16_KERMIT, buf.nioBuffer()));
+ buf.writeByte('\r');
+ buf.writeByte('\n');
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ ByteBuf content = Unpooled.buffer();
+
+ switch (command.getType()) {
+ case Command.TYPE_POSITION_SINGLE:
+ return encodeContent(CityeasyProtocolDecoder.MSG_LOCATION_REQUEST, content);
+ case Command.TYPE_POSITION_PERIODIC:
+ content.writeShort(command.getInteger(Command.KEY_FREQUENCY));
+ return encodeContent(CityeasyProtocolDecoder.MSG_LOCATION_INTERVAL, content);
+ case Command.TYPE_POSITION_STOP:
+ content.writeShort(0);
+ return encodeContent(CityeasyProtocolDecoder.MSG_LOCATION_INTERVAL, content);
+ case Command.TYPE_SET_TIMEZONE:
+ int timezone = TimeZone.getTimeZone(command.getString(Command.KEY_TIMEZONE)).getRawOffset() / 60000;
+ if (timezone < 0) {
+ content.writeByte(1);
+ } else {
+ content.writeByte(0);
+ }
+ content.writeShort(Math.abs(timezone));
+ return encodeContent(CityeasyProtocolDecoder.MSG_TIMEZONE, content);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ContinentalProtocol.java b/src/main/java/org/traccar/protocol/ContinentalProtocol.java
new file mode 100644
index 000000000..bc7928fba
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ContinentalProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ContinentalProtocol extends BaseProtocol {
+
+ public ContinentalProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 2, 2, -4, 0));
+ pipeline.addLast(new ContinentalProtocolDecoder(ContinentalProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ContinentalProtocolDecoder.java b/src/main/java/org/traccar/protocol/ContinentalProtocolDecoder.java
new file mode 100644
index 000000000..471afa0d6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ContinentalProtocolDecoder.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+
+public class ContinentalProtocolDecoder extends BaseProtocolDecoder {
+
+ public ContinentalProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_KEEPALIVE = 0x00;
+ public static final int MSG_STATUS = 0x02;
+ public static final int MSG_ACK = 0x06;
+ public static final int MSG_NACK = 0x15;
+
+ private double readCoordinate(ByteBuf buf, boolean extended) {
+ long value = buf.readUnsignedInt();
+ if (extended ? (value & 0x08000000) != 0 : (value & 0x00800000) != 0) {
+ value |= extended ? 0xF0000000 : 0xFF000000;
+ }
+ return (int) value / (extended ? 360000.0 : 3600.0);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ buf.readUnsignedShort(); // length
+ buf.readUnsignedByte(); // software version
+
+ long serialNumber = buf.readUnsignedInt();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(serialNumber));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ buf.readUnsignedByte(); // product
+
+ int type = buf.readUnsignedByte();
+
+ if (type == MSG_STATUS) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setFixTime(new Date(buf.readUnsignedInt() * 1000L));
+
+ boolean extended = buf.getUnsignedByte(buf.readerIndex()) != 0;
+ position.setLatitude(readCoordinate(buf, extended));
+ position.setLongitude(readCoordinate(buf, extended));
+
+ position.setCourse(buf.readUnsignedShort());
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+
+ position.setValid(buf.readUnsignedByte() > 0);
+
+ position.setDeviceTime(new Date(buf.readUnsignedInt() * 1000L));
+
+ position.set(Position.KEY_EVENT, buf.readUnsignedShort());
+
+ int input = buf.readUnsignedShort();
+ position.set(Position.KEY_IGNITION, BitUtil.check(input, 0));
+ position.set(Position.KEY_INPUT, input);
+
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedShort());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte());
+ position.set(Position.KEY_DEVICE_TEMP, buf.readByte());
+
+ buf.readUnsignedShort(); // reserved
+
+ if (buf.readableBytes() > 4) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+ }
+
+ if (buf.readableBytes() > 4) {
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromHours(buf.readUnsignedInt()));
+ }
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CradlepointProtocol.java b/src/main/java/org/traccar/protocol/CradlepointProtocol.java
new file mode 100644
index 000000000..4a09e0311
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CradlepointProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class CradlepointProtocol extends BaseProtocol {
+
+ public CradlepointProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new CradlepointProtocolDecoder(CradlepointProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/CradlepointProtocolDecoder.java b/src/main/java/org/traccar/protocol/CradlepointProtocolDecoder.java
new file mode 100644
index 000000000..a282131ce
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/CradlepointProtocolDecoder.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class CradlepointProtocolDecoder extends BaseProtocolDecoder {
+
+ public CradlepointProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("([^,]+),") // id
+ .number("(d{1,6}),") // time (hhmmss)
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+)?,") // speed
+ .number("(d+.d+)?,") // course
+ .expression("([^,]+)?,") // carrier
+ .expression("([^,]+)?,") // serdis
+ .number("(-?d+)?,") // rsrp
+ .number("(-?d+)?,") // rssi
+ .number("(-?d+)?,") // rsrq
+ .expression("([^,]+)?,") // ecio
+ .expression("([^,]+)?") // wan ip
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int time = parser.nextInt();
+ DateBuilder dateBuilder = new DateBuilder(new Date());
+ dateBuilder.setHour(time / 100 / 100);
+ dateBuilder.setMinute(time / 100 % 100);
+ dateBuilder.setSecond(time % 100);
+ position.setTime(dateBuilder.getDate());
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set("carrid", parser.next());
+ position.set("serdis", parser.next());
+ position.set("rsrp", parser.nextInt());
+ position.set(Position.KEY_RSSI, parser.nextInt());
+ position.set("rsrq", parser.nextInt());
+ position.set("ecio", parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/DishaProtocol.java b/src/main/java/org/traccar/protocol/DishaProtocol.java
new file mode 100644
index 000000000..38f49cc05
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/DishaProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class DishaProtocol extends BaseProtocol {
+
+ public DishaProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new DishaProtocolDecoder(DishaProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/DishaProtocolDecoder.java b/src/main/java/org/traccar/protocol/DishaProtocolDecoder.java
new file mode 100644
index 000000000..3223988ab
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/DishaProtocolDecoder.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class DishaProtocolDecoder extends BaseProtocolDecoder {
+
+ public DishaProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$A#A#")
+ .number("(d+)#") // imei
+ .expression("([AVMX])#") // validity
+ .number("(dd)(dd)(dd)#") // time (hhmmss)
+ .number("(dd)(dd)(dd)#") // date (ddmmyy)
+ .number("(dd)(dd.d+)#") // latitude
+ .expression("([NS])#")
+ .number("(ddd)(dd.d+)#") // longitude
+ .expression("([EW])#")
+ .number("(d+.d+)#") // speed
+ .number("(d+.d+)#") // course
+ .number("(d+)#") // satellites
+ .number("(d+.d+)#") // hdop
+ .number("(d+)#") // gsm
+ .expression("([012])#") // power mode
+ .number("(d+)#") // battery
+ .number("(d+)#") // adc 1
+ .number("(d+)#") // adc 2
+ .number("d+.d+#") // day distance
+ .number("(d+.d+)#") // odometer
+ .expression("([01]+)") // digital inputs
+ .text("*")
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.next().equals("A"));
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.set(Position.KEY_RSSI, parser.nextDouble());
+ position.set(Position.KEY_CHARGE, parser.nextInt(0) == 2);
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt(0));
+
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt(0));
+ position.set(Position.PREFIX_ADC + 2, parser.nextInt(0));
+
+ position.set(Position.KEY_ODOMETER, parser.nextDouble(0) * 1000);
+ position.set(Position.KEY_INPUT, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/DmtHttpProtocol.java b/src/main/java/org/traccar/protocol/DmtHttpProtocol.java
new file mode 100644
index 000000000..34568128f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/DmtHttpProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class DmtHttpProtocol extends BaseProtocol {
+
+ public DmtHttpProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(65535));
+ pipeline.addLast(new DmtHttpProtocolDecoder(DmtHttpProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/DmtHttpProtocolDecoder.java b/src/main/java/org/traccar/protocol/DmtHttpProtocolDecoder.java
new file mode 100644
index 000000000..987361baf
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/DmtHttpProtocolDecoder.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.traccar.BaseHttpProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import java.io.StringReader;
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TimeZone;
+
+public class DmtHttpProtocolDecoder extends BaseHttpProtocolDecoder {
+
+ public DmtHttpProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ JsonObject root = Json.createReader(
+ new StringReader(request.content().toString(StandardCharsets.US_ASCII))).readObject();
+
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, root.getString("IMEI"));
+ if (deviceSession == null) {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ JsonArray records = root.getJsonArray("Records");
+
+ for (int i = 0; i < records.size(); i++) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ JsonObject record = records.getJsonObject(i);
+
+ position.set(Position.KEY_INDEX, record.getInt("SeqNo"));
+ position.set(Position.KEY_EVENT, record.getInt("Reason"));
+
+ position.setDeviceTime(dateFormat.parse(record.getString("DateUTC")));
+
+ JsonArray fields = record.getJsonArray("Fields");
+
+ for (int j = 0; j < fields.size(); j++) {
+ JsonObject field = fields.getJsonObject(j);
+ switch (field.getInt("FType")) {
+ case 0:
+ position.setFixTime(dateFormat.parse(field.getString("GpsUTC")));
+ position.setLatitude(field.getJsonNumber("Lat").doubleValue());
+ position.setLongitude(field.getJsonNumber("Long").doubleValue());
+ position.setAltitude(field.getInt("Alt"));
+ position.setSpeed(UnitsConverter.knotsFromCps(field.getInt("Spd")));
+ position.setCourse(field.getInt("Head"));
+ position.setAccuracy(field.getInt("PosAcc"));
+ position.setValid(field.getInt("GpsStat") > 0);
+ break;
+ case 2:
+ int input = field.getInt("DIn");
+ int output = field.getInt("DOut");
+
+ position.set(Position.KEY_IGNITION, BitUtil.check(input, 0));
+
+ position.set(Position.KEY_INPUT, input);
+ position.set(Position.KEY_OUTPUT, output);
+ position.set(Position.KEY_STATUS, field.getInt("DevStat"));
+ break;
+ case 6:
+ JsonObject adc = field.getJsonObject("AnalogueData");
+ if (adc.containsKey("1")) {
+ position.set(Position.KEY_BATTERY, adc.getInt("1") * 0.001);
+ }
+ if (adc.containsKey("2")) {
+ position.set(Position.KEY_POWER, adc.getInt("2") * 0.01);
+ }
+ if (adc.containsKey("3")) {
+ position.set(Position.KEY_DEVICE_TEMP, adc.getInt("3") * 0.01);
+ }
+ if (adc.containsKey("4")) {
+ position.set(Position.KEY_RSSI, adc.getInt("4"));
+ }
+ if (adc.containsKey("5")) {
+ position.set("solarPower", adc.getInt("5") * 0.001);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ positions.add(position);
+ }
+
+ sendResponse(channel, HttpResponseStatus.OK);
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/DmtProtocol.java b/src/main/java/org/traccar/protocol/DmtProtocol.java
new file mode 100644
index 000000000..78a5243c0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/DmtProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import java.nio.ByteOrder;
+public class DmtProtocol extends BaseProtocol {
+
+ public DmtProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 3, 2, 0, 0, true));
+ pipeline.addLast(new DmtProtocolDecoder(DmtProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/DmtProtocolDecoder.java b/src/main/java/org/traccar/protocol/DmtProtocolDecoder.java
new file mode 100644
index 000000000..c04e90f1d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/DmtProtocolDecoder.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class DmtProtocolDecoder extends BaseProtocolDecoder {
+
+ public DmtProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_HELLO = 0x00;
+ public static final int MSG_HELLO_RESPONSE = 0x01;
+ public static final int MSG_DATA_RECORD = 0x04;
+ public static final int MSG_COMMIT = 0x05;
+ public static final int MSG_COMMIT_RESPONSE = 0x06;
+ public static final int MSG_DATA_RECORD_64 = 0x10;
+
+ public static final int MSG_CANNED_REQUEST_1 = 0x14;
+ public static final int MSG_CANNED_RESPONSE_1 = 0x15;
+ public static final int MSG_CANNED_REQUEST_2 = 0x22;
+ public static final int MSG_CANNED_RESPONSE_2 = 0x23;
+
+ private void sendResponse(Channel channel, int type, ByteBuf content) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(0x02); response.writeByte(0x55); // header
+ response.writeByte(type);
+ response.writeShortLE(content != null ? content.readableBytes() : 0);
+ if (content != null) {
+ response.writeBytes(content);
+ content.release();
+ }
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ private List<Position> decodeFixed64(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ while (buf.readableBytes() >= 64) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readByte(); // type
+
+ position.set(Position.KEY_INDEX, buf.readUnsignedIntLE());
+
+ long time = buf.readUnsignedIntLE();
+ position.setTime(new DateBuilder()
+ .setYear((int) (2000 + (time & 0x3F)))
+ .setMonth((int) (time >> 6) & 0xF)
+ .setDay((int) (time >> 10) & 0x1F)
+ .setHour((int) (time >> 15) & 0x1F)
+ .setMinute((int) (time >> 20) & 0x3F)
+ .setSecond((int) (time >> 26) & 0x3F)
+ .getDate());
+
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setSpeed(UnitsConverter.knotsFromCps(buf.readUnsignedShortLE()));
+ position.setCourse(buf.readUnsignedByte() * 2);
+ position.setAltitude(buf.readShortLE());
+
+ buf.readUnsignedShortLE(); // position accuracy
+ buf.readUnsignedByte(); // speed accuracy
+
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+
+ position.setValid(BitUtil.check(buf.readByte(), 0));
+
+ position.set(Position.KEY_INPUT, buf.readUnsignedIntLE());
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedShortLE());
+
+ for (int i = 1; i <= 5; i++) {
+ position.set(Position.PREFIX_ADC + i, buf.readShortLE());
+ }
+
+ position.set(Position.KEY_DEVICE_TEMP, buf.readByte());
+
+ buf.readShortLE(); // accelerometer x
+ buf.readShortLE(); // accelerometer y
+ buf.readShortLE(); // accelerometer z
+
+ buf.skipBytes(8); // device id
+
+ position.set(Position.KEY_PDOP, buf.readUnsignedShortLE() * 0.01);
+
+ buf.skipBytes(2); // reserved
+
+ buf.readUnsignedShortLE(); // checksum
+
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+ private List<Position> decodeStandard(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ while (buf.isReadable()) {
+ int recordEnd = buf.readerIndex() + buf.readUnsignedShortLE();
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_INDEX, buf.readUnsignedIntLE());
+
+ position.setDeviceTime(new Date(1356998400000L + buf.readUnsignedIntLE() * 1000)); // since 1 Jan 2013
+
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+
+ while (buf.readerIndex() < recordEnd) {
+
+ int fieldId = buf.readUnsignedByte();
+ int fieldLength = buf.readUnsignedByte();
+ int fieldEnd = buf.readerIndex() + (fieldLength == 255 ? buf.readUnsignedShortLE() : fieldLength);
+
+ if (fieldId == 0) {
+
+ position.setFixTime(new Date(1356998400000L + buf.readUnsignedIntLE() * 1000));
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+ position.setAltitude(buf.readShortLE());
+ position.setSpeed(UnitsConverter.knotsFromCps(buf.readUnsignedShortLE()));
+
+ buf.readUnsignedByte(); // speed accuracy
+
+ position.setCourse(buf.readUnsignedByte() * 2);
+
+ position.set(Position.KEY_PDOP, buf.readUnsignedByte() * 0.1);
+
+ position.setAccuracy(buf.readUnsignedByte());
+ position.setValid(buf.readUnsignedByte() != 0);
+
+ } else if (fieldId == 2) {
+
+ int input = buf.readIntLE();
+ int output = buf.readUnsignedShortLE();
+ int status = buf.readUnsignedShortLE();
+
+ position.set(Position.KEY_IGNITION, BitUtil.check(input, 0));
+
+ position.set(Position.KEY_INPUT, input);
+ position.set(Position.KEY_OUTPUT, output);
+ position.set(Position.KEY_STATUS, status);
+
+ } else if (fieldId == 6) {
+
+ while (buf.readerIndex() < fieldEnd) {
+ switch (buf.readUnsignedByte()) {
+ case 1:
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.001);
+ break;
+ case 2:
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() * 0.01);
+ break;
+ case 3:
+ position.set(Position.KEY_DEVICE_TEMP, buf.readShortLE() * 0.01);
+ break;
+ case 4:
+ position.set(Position.KEY_RSSI, buf.readUnsignedShortLE());
+ break;
+ case 5:
+ position.set("solarPower", buf.readUnsignedShortLE() * 0.001);
+ break;
+ default:
+ break;
+ }
+ }
+
+ }
+
+ buf.readerIndex(fieldEnd);
+
+ }
+
+ if (position.getFixTime() == null) {
+ getLastLocation(position, position.getDeviceTime());
+ }
+
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+
+ int type = buf.readUnsignedByte();
+ int length = buf.readUnsignedShortLE();
+
+ if (type == MSG_HELLO) {
+
+ buf.readUnsignedIntLE(); // device serial number
+
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, buf.readSlice(15).toString(StandardCharsets.US_ASCII));
+
+ ByteBuf response = Unpooled.buffer();
+ if (length == 51) {
+ response.writeByte(0); // reserved
+ response.writeIntLE(0); // reserved
+ } else {
+ response.writeIntLE((int) ((System.currentTimeMillis() - 1356998400000L) / 1000));
+ response.writeIntLE(deviceSession != null ? 0 : 1); // flags
+ }
+
+ sendResponse(channel, MSG_HELLO_RESPONSE, response);
+
+ } else if (type == MSG_COMMIT) {
+
+ ByteBuf response = Unpooled.buffer(0);
+ response.writeByte(1); // flags (success)
+ sendResponse(channel, MSG_COMMIT_RESPONSE, response);
+
+ } else if (type == MSG_CANNED_REQUEST_1) {
+
+ ByteBuf response = Unpooled.buffer(0);
+ response.writeBytes(new byte[12]);
+ sendResponse(channel, MSG_CANNED_RESPONSE_1, response);
+
+ } else if (type == MSG_CANNED_REQUEST_2) {
+
+ sendResponse(channel, MSG_CANNED_RESPONSE_2, null);
+
+ } else if (type == MSG_DATA_RECORD_64) {
+
+ return decodeFixed64(channel, remoteAddress, buf);
+
+ } else if (type == MSG_DATA_RECORD) {
+
+ return decodeStandard(channel, remoteAddress, buf);
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/DwayProtocol.java b/src/main/java/org/traccar/protocol/DwayProtocol.java
new file mode 100644
index 000000000..05fd8b6e7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/DwayProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class DwayProtocol extends BaseProtocol {
+
+ public DwayProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new DwayProtocolDecoder(DwayProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/DwayProtocolDecoder.java b/src/main/java/org/traccar/protocol/DwayProtocolDecoder.java
new file mode 100644
index 000000000..9b02c898e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/DwayProtocolDecoder.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class DwayProtocolDecoder extends BaseProtocolDecoder {
+
+ public DwayProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("AA55,")
+ .number("d+,") // index
+ .number("(d+),") // imei
+ .number("d+,") // type
+ .number("(dd)(dd)(dd),") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+),") // altitude
+ .number(" ?(d+.d+),") // speed
+ .number("(d+),") // course
+ .number("([01]{4}),") // input
+ .number("([01]{4}),") // output
+ .number("([01]+),") // flags
+ .number("(d+),") // battery
+ .number("(d+),") // adc1
+ .number("(d+),") // adc2
+ .number("(d+)") // driver
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ if (sentence.equals("AA55,HB")) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("55AA,HB,OK\r\n", remoteAddress));
+ }
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(true);
+ position.setTime(parser.nextDateTime());
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setAltitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_INPUT, parser.nextBinInt());
+ position.set(Position.KEY_OUTPUT, parser.nextBinInt());
+
+ position.set(Position.KEY_BATTERY, parser.nextInt() * 0.001);
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt() * 0.001);
+ position.set(Position.PREFIX_ADC + 2, parser.nextInt() * 0.001);
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EasyTrackProtocol.java b/src/main/java/org/traccar/protocol/EasyTrackProtocol.java
new file mode 100644
index 000000000..74c636d06
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EasyTrackProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class EasyTrackProtocol extends BaseProtocol {
+
+ public EasyTrackProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "#", "\r\n"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new EasyTrackProtocolDecoder(EasyTrackProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EasyTrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/EasyTrackProtocolDecoder.java
new file mode 100644
index 000000000..2ddb24f5c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EasyTrackProtocolDecoder.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class EasyTrackProtocolDecoder extends BaseProtocolDecoder {
+
+ public EasyTrackProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("*").expression("..,") // manufacturer
+ .number("(d+),") // imei
+ .expression("([^,]{2}),") // command
+ .expression("([AV]),") // validity
+ .number("(xx)(xx)(xx),") // date (yymmdd)
+ .number("(xx)(xx)(xx),") // time (hhmmss)
+ .number("(x)(x{7}),") // latitude
+ .number("(x)(x{7}),") // longitude
+ .number("(x{4}),") // speed
+ .number("(x{4}),") // course
+ .number("(x{8}),") // status
+ .number("(x+),") // signal
+ .number("(d+),") // power
+ .number("(x{4}),") // oil
+ .number("(x+),?") // odometer
+ .number("(d+)?") // altitude
+ .any()
+ .compile();
+
+ private String decodeAlarm(long status) {
+ if ((status & 0x02000000) != 0) {
+ return Position.ALARM_GEOFENCE_ENTER;
+ }
+ if ((status & 0x04000000) != 0) {
+ return Position.ALARM_GEOFENCE_EXIT;
+ }
+ if ((status & 0x08000000) != 0) {
+ return Position.ALARM_LOW_BATTERY;
+ }
+ if ((status & 0x20000000) != 0) {
+ return Position.ALARM_VIBRATION;
+ }
+ if ((status & 0x80000000) != 0) {
+ return Position.ALARM_OVERSPEED;
+ }
+ if ((status & 0x00010000) != 0) {
+ return Position.ALARM_SOS;
+ }
+ if ((status & 0x00040000) != 0) {
+ return Position.ALARM_POWER_CUT;
+ }
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_COMMAND, parser.next());
+
+ position.setValid(parser.next().equals("A"));
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(parser.nextHexInt(0), parser.nextHexInt(0), parser.nextHexInt(0))
+ .setTime(parser.nextHexInt(0), parser.nextHexInt(0), parser.nextHexInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ if (BitUtil.check(parser.nextHexInt(0), 3)) {
+ position.setLatitude(-parser.nextHexInt(0) / 600000.0);
+ } else {
+ position.setLatitude(parser.nextHexInt(0) / 600000.0);
+ }
+
+ if (BitUtil.check(parser.nextHexInt(0), 3)) {
+ position.setLongitude(-parser.nextHexInt(0) / 600000.0);
+ } else {
+ position.setLongitude(parser.nextHexInt(0) / 600000.0);
+ }
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextHexInt(0) / 100.0));
+ position.setCourse(parser.nextHexInt(0) / 100.0);
+
+ long status = parser.nextHexLong();
+ position.set(Position.KEY_STATUS, status);
+ position.set(Position.KEY_ALARM, decodeAlarm(status));
+
+ position.set("signal", parser.next());
+ position.set(Position.KEY_POWER, parser.nextDouble(0));
+ position.set("oil", parser.nextHexInt(0));
+ position.set(Position.KEY_ODOMETER, parser.nextHexInt(0) * 100);
+
+ position.setAltitude(parser.nextDouble(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EelinkProtocol.java b/src/main/java/org/traccar/protocol/EelinkProtocol.java
new file mode 100644
index 000000000..de4ea971b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EelinkProtocol.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class EelinkProtocol extends BaseProtocol {
+
+ public EelinkProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_REBOOT_DEVICE);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 3, 2));
+ pipeline.addLast(new EelinkProtocolEncoder(false));
+ pipeline.addLast(new EelinkProtocolDecoder(EelinkProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new EelinkProtocolEncoder(true));
+ pipeline.addLast(new EelinkProtocolDecoder(EelinkProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EelinkProtocolDecoder.java b/src/main/java/org/traccar/protocol/EelinkProtocolDecoder.java
new file mode 100644
index 000000000..2a1db2e32
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EelinkProtocolDecoder.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.socket.DatagramChannel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class EelinkProtocolDecoder extends BaseProtocolDecoder {
+
+ public EelinkProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_LOGIN = 0x01;
+ public static final int MSG_GPS = 0x02;
+ public static final int MSG_HEARTBEAT = 0x03;
+ public static final int MSG_ALARM = 0x04;
+ public static final int MSG_STATE = 0x05;
+ public static final int MSG_SMS = 0x06;
+ public static final int MSG_OBD = 0x07;
+ public static final int MSG_DOWNLINK = 0x80;
+ public static final int MSG_DATA = 0x81;
+
+ public static final int MSG_NORMAL = 0x12;
+ public static final int MSG_WARNING = 0x14;
+ public static final int MSG_REPORT = 0x15;
+ public static final int MSG_COMMAND = 0x16;
+ public static final int MSG_OBD_DATA = 0x17;
+ public static final int MSG_OBD_BODY = 0x18;
+ public static final int MSG_OBD_CODE = 0x19;
+ public static final int MSG_CAMERA_INFO = 0x1E;
+ public static final int MSG_CAMERA_DATA = 0x1F;
+
+ private String decodeAlarm(Short value) {
+ switch (value) {
+ case 0x01:
+ return Position.ALARM_POWER_OFF;
+ case 0x02:
+ return Position.ALARM_SOS;
+ case 0x03:
+ return Position.ALARM_LOW_BATTERY;
+ case 0x04:
+ return Position.ALARM_VIBRATION;
+ case 0x08:
+ case 0x09:
+ return Position.ALARM_GPS_ANTENNA_CUT;
+ case 0x81:
+ return Position.ALARM_LOW_SPEED;
+ case 0x82:
+ return Position.ALARM_OVERSPEED;
+ case 0x83:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 0x84:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 0x85:
+ return Position.ALARM_ACCIDENT;
+ case 0x86:
+ return Position.ALARM_FALL_DOWN;
+ default:
+ return null;
+ }
+ }
+
+ private void decodeStatus(Position position, int status) {
+ if (BitUtil.check(status, 1)) {
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 2));
+ }
+ if (BitUtil.check(status, 3)) {
+ position.set(Position.KEY_ARMED, BitUtil.check(status, 4));
+ }
+ if (BitUtil.check(status, 5)) {
+ position.set(Position.KEY_BLOCKED, !BitUtil.check(status, 6));
+ }
+ if (BitUtil.check(status, 7)) {
+ position.set(Position.KEY_CHARGE, BitUtil.check(status, 8));
+ }
+ position.set(Position.KEY_STATUS, status);
+ }
+
+ private Position decodeOld(DeviceSession deviceSession, ByteBuf buf, int type, int index) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_INDEX, index);
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+ position.setLatitude(buf.readInt() / 1800000.0);
+ position.setLongitude(buf.readInt() / 1800000.0);
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.setCourse(buf.readUnsignedShort());
+
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedShort(), buf.readUnsignedShort(), buf.readUnsignedMedium())));
+
+ position.setValid((buf.readUnsignedByte() & 0x01) != 0);
+
+ if (type == MSG_GPS) {
+
+ if (buf.readableBytes() >= 2) {
+ decodeStatus(position, buf.readUnsignedShort());
+ }
+
+ if (buf.readableBytes() >= 2 * 4) {
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.001);
+
+ position.set(Position.KEY_RSSI, buf.readUnsignedShort());
+
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShort());
+
+ }
+
+ } else if (type == MSG_ALARM) {
+
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte()));
+
+ } else if (type == MSG_STATE) {
+
+ int statusType = buf.readUnsignedByte();
+
+ position.set(Position.KEY_EVENT, statusType);
+
+ if (statusType == 0x01 || statusType == 0x02 || statusType == 0x03) {
+ buf.readUnsignedInt(); // device time
+ if (buf.readableBytes() >= 2) {
+ decodeStatus(position, buf.readUnsignedShort());
+ }
+ }
+
+ }
+
+ return position;
+ }
+
+ private Position decodeNew(DeviceSession deviceSession, ByteBuf buf, int type, int index) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_INDEX, index);
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+
+ int flags = buf.readUnsignedByte();
+
+ if (BitUtil.check(flags, 0)) {
+ position.setLatitude(buf.readInt() / 1800000.0);
+ position.setLongitude(buf.readInt() / 1800000.0);
+ position.setAltitude(buf.readShort());
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+ position.setCourse(buf.readUnsignedShort());
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ } else {
+ getLastLocation(position, position.getDeviceTime());
+ }
+
+ if (BitUtil.check(flags, 1)) {
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedShort(),
+ buf.readUnsignedShort(), buf.readUnsignedInt(), buf.readUnsignedByte())));
+ }
+
+ if (BitUtil.check(flags, 2)) {
+ buf.skipBytes(7); // bsid1
+ }
+
+ if (BitUtil.check(flags, 3)) {
+ buf.skipBytes(7); // bsid2
+ }
+
+ if (BitUtil.check(flags, 4)) {
+ buf.skipBytes(7); // bss0
+ }
+
+ if (BitUtil.check(flags, 5)) {
+ buf.skipBytes(7); // bss1
+ }
+
+ if (BitUtil.check(flags, 6)) {
+ buf.skipBytes(7); // bss2
+ }
+
+ if (type == MSG_WARNING) {
+
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte()));
+
+ } else if (type == MSG_REPORT) {
+
+ buf.readUnsignedByte(); // report type
+
+ }
+
+ if (type == MSG_NORMAL || type == MSG_WARNING || type == MSG_REPORT) {
+
+ int status = buf.readUnsignedShort();
+ position.setValid(BitUtil.check(status, 0));
+ if (BitUtil.check(status, 1)) {
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 2));
+ }
+ position.set(Position.KEY_STATUS, status);
+
+ }
+
+ if (type == MSG_NORMAL) {
+
+ if (buf.readableBytes() >= 2) {
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.001);
+ }
+
+ if (buf.readableBytes() >= 4) {
+ position.set(Position.PREFIX_ADC + 0, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort());
+ }
+
+ if (buf.readableBytes() >= 4) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+ }
+
+ if (buf.readableBytes() >= 4) {
+ buf.readUnsignedShort(); // gsm counter
+ buf.readUnsignedShort(); // gps counter
+ }
+
+ if (buf.readableBytes() >= 4) {
+ position.set(Position.KEY_STEPS, buf.readUnsignedShort());
+ buf.readUnsignedShort(); // walking time
+ }
+
+ if (buf.readableBytes() >= 12) {
+ position.set(Position.PREFIX_TEMP + 1, buf.readUnsignedShort() / 256.0);
+ position.set("humidity", buf.readUnsignedShort() * 0.1);
+ position.set("illuminance", buf.readUnsignedInt() / 256.0);
+ position.set("co2", buf.readUnsignedInt());
+ }
+
+ if (buf.readableBytes() >= 2) {
+ position.set(Position.PREFIX_TEMP + 2, buf.readShort() / 16.0);
+ }
+
+ }
+
+ return position;
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("Lat:")
+ .number("([NS])(d+.d+)") // latitude
+ .any()
+ .text("Lon:")
+ .number("([EW])(d+.d+)") // longitude
+ .any()
+ .text("Course:")
+ .number("(d+.d+)") // course
+ .any()
+ .text("Speed:")
+ .number("(d+.d+)") // speed
+ .any()
+ .expression("Date ?Time:")
+ .number("(dddd)-(dd)-(dd) ") // date
+ .number("(dd):(dd):(dd)") // time
+ .compile();
+
+ private Position decodeResult(DeviceSession deviceSession, ByteBuf buf, int index) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_INDEX, index);
+
+ buf.readUnsignedByte(); // type
+ buf.readUnsignedInt(); // uid
+
+ String sentence = buf.toString(StandardCharsets.UTF_8);
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (parser.matches()) {
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setCourse(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setTime(parser.nextDateTime());
+
+ } else {
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_RESULT, sentence);
+
+ }
+
+ return position;
+ }
+
+ private Position decodeObd(DeviceSession deviceSession, ByteBuf buf, int index) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, new Date(buf.readUnsignedInt() * 1000));
+
+ while (buf.readableBytes() > 0) {
+ int pid = buf.readUnsignedByte();
+ int value = buf.readInt();
+ switch (pid) {
+ case 0x89:
+ position.set(Position.KEY_FUEL_CONSUMPTION, value);
+ break;
+ case 0x8a:
+ position.set(Position.KEY_ODOMETER, value * 1000L);
+ break;
+ case 0x8b:
+ position.set(Position.KEY_FUEL_LEVEL, value / 10);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ String uniqueId = null;
+ DeviceSession deviceSession;
+
+ if (buf.getByte(0) == 'E' && buf.getByte(1) == 'L') {
+ buf.skipBytes(2 + 2 + 2); // udp header
+ uniqueId = ByteBufUtil.hexDump(buf.readSlice(8)).substring(1);
+ deviceSession = getDeviceSession(channel, remoteAddress, uniqueId);
+ } else {
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ }
+
+ buf.skipBytes(2); // header
+ int type = buf.readUnsignedByte();
+ buf.readShort(); // length
+ int index = buf.readUnsignedShort();
+
+ if (type != MSG_GPS && type != MSG_DATA) {
+ ByteBuf content = Unpooled.buffer();
+ if (type == MSG_LOGIN) {
+ content.writeInt((int) (System.currentTimeMillis() / 1000));
+ content.writeByte(1); // protocol version
+ content.writeByte(0); // action mask
+ }
+ ByteBuf response = EelinkProtocolEncoder.encodeContent(
+ channel instanceof DatagramChannel, uniqueId, type, index, content);
+ content.release();
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ if (type == MSG_LOGIN) {
+
+ if (deviceSession == null) {
+ getDeviceSession(channel, remoteAddress, ByteBufUtil.hexDump(buf.readSlice(8)).substring(1));
+ }
+
+ } else {
+
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (type == MSG_GPS || type == MSG_ALARM || type == MSG_STATE || type == MSG_SMS) {
+
+ return decodeOld(deviceSession, buf, type, index);
+
+ } else if (type >= MSG_NORMAL && type <= MSG_OBD_CODE) {
+
+ return decodeNew(deviceSession, buf, type, index);
+
+ } else if (type == MSG_HEARTBEAT && buf.readableBytes() >= 2) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ decodeStatus(position, buf.readUnsignedShort());
+
+ return position;
+
+ } else if (type == MSG_OBD) {
+
+ return decodeObd(deviceSession, buf, index);
+
+ } else if (type == MSG_DOWNLINK) {
+
+ return decodeResult(deviceSession, buf, index);
+
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EelinkProtocolEncoder.java b/src/main/java/org/traccar/protocol/EelinkProtocolEncoder.java
new file mode 100644
index 000000000..8f33441fb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EelinkProtocolEncoder.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.DataConverter;
+import org.traccar.model.Command;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+public class EelinkProtocolEncoder extends BaseProtocolEncoder {
+
+ private boolean connectionless;
+
+ public EelinkProtocolEncoder(boolean connectionless) {
+ this.connectionless = connectionless;
+ }
+
+ public static int checksum(ByteBuffer buf) {
+ int sum = 0;
+ while (buf.hasRemaining()) {
+ sum = (((sum << 1) | (sum >> 15)) + (buf.get() & 0xFF)) & 0xFFFF;
+ }
+ return sum;
+ }
+
+ public static ByteBuf encodeContent(
+ boolean connectionless, String uniqueId, int type, int index, ByteBuf content) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ if (connectionless) {
+ buf.writeBytes(DataConverter.parseHex('0' + uniqueId));
+ }
+
+ buf.writeByte(0x67);
+ buf.writeByte(0x67);
+ buf.writeByte(type);
+ buf.writeShort(2 + (content != null ? content.readableBytes() : 0)); // length
+ buf.writeShort(index);
+
+ if (content != null) {
+ buf.writeBytes(content);
+ }
+
+ ByteBuf result = Unpooled.buffer();
+
+ if (connectionless) {
+ result.writeByte('E');
+ result.writeByte('L');
+ result.writeShort(2 + buf.readableBytes()); // length
+ result.writeShort(checksum(buf.nioBuffer()));
+ }
+
+ result.writeBytes(buf);
+ buf.release();
+
+ return result;
+ }
+
+ private ByteBuf encodeContent(long deviceId, String content) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeByte(0x01); // command
+ buf.writeInt(0); // server id
+ buf.writeBytes(content.getBytes(StandardCharsets.UTF_8));
+
+ return encodeContent(connectionless, getUniqueId(deviceId), EelinkProtocolDecoder.MSG_DOWNLINK, 0, buf);
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return encodeContent(command.getDeviceId(), command.getString(Command.KEY_DATA));
+ case Command.TYPE_POSITION_SINGLE:
+ return encodeContent(command.getDeviceId(), "WHERE#");
+ case Command.TYPE_ENGINE_STOP:
+ return encodeContent(command.getDeviceId(), "RELAY,1#");
+ case Command.TYPE_ENGINE_RESUME:
+ return encodeContent(command.getDeviceId(), "RELAY,0#");
+ case Command.TYPE_REBOOT_DEVICE:
+ return encodeContent(command.getDeviceId(), "RESET#");
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EgtsFrameDecoder.java b/src/main/java/org/traccar/protocol/EgtsFrameDecoder.java
new file mode 100644
index 000000000..84f1f11a7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EgtsFrameDecoder.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class EgtsFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ int headerLength = buf.getUnsignedByte(buf.readerIndex() + 3);
+ int frameLength = buf.getUnsignedShortLE(buf.readerIndex() + 5);
+
+ int length = headerLength + frameLength + (frameLength > 0 ? 2 : 0);
+
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EgtsProtocol.java b/src/main/java/org/traccar/protocol/EgtsProtocol.java
new file mode 100644
index 000000000..5d4638f37
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EgtsProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class EgtsProtocol extends BaseProtocol {
+
+ public EgtsProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new EgtsFrameDecoder());
+ pipeline.addLast(new EgtsProtocolDecoder(EgtsProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EgtsProtocolDecoder.java b/src/main/java/org/traccar/protocol/EgtsProtocolDecoder.java
new file mode 100644
index 000000000..b9fcb2f44
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EgtsProtocolDecoder.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class EgtsProtocolDecoder extends BaseProtocolDecoder {
+
+ public EgtsProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int PT_RESPONSE = 0;
+ public static final int PT_APPDATA = 1;
+ public static final int PT_SIGNED_APPDATA = 2;
+
+ public static final int SERVICE_AUTH = 1;
+ public static final int SERVICE_TELEDATA = 2;
+ public static final int SERVICE_COMMANDS = 4;
+ public static final int SERVICE_FIRMWARE = 9;
+ public static final int SERVICE_ECALL = 10;
+
+ public static final int MSG_RECORD_RESPONSE = 0;
+ public static final int MSG_TERM_IDENTITY = 1;
+ public static final int MSG_MODULE_DATA = 2;
+ public static final int MSG_VEHICLE_DATA = 3;
+ public static final int MSG_AUTH_PARAMS = 4;
+ public static final int MSG_AUTH_INFO = 5;
+ public static final int MSG_SERVICE_INFO = 6;
+ public static final int MSG_RESULT_CODE = 7;
+ public static final int MSG_POS_DATA = 16;
+ public static final int MSG_EXT_POS_DATA = 17;
+ public static final int MSG_AD_SENSORS_DATA = 18;
+ public static final int MSG_COUNTERS_DATA = 19;
+ public static final int MSG_STATE_DATA = 20;
+ public static final int MSG_LOOPIN_DATA = 22;
+ public static final int MSG_ABS_DIG_SENS_DATA = 23;
+ public static final int MSG_ABS_AN_SENS_DATA = 24;
+ public static final int MSG_ABS_CNTR_DATA = 25;
+ public static final int MSG_ABS_LOOPIN_DATA = 26;
+ public static final int MSG_LIQUID_LEVEL_SENSOR = 27;
+ public static final int MSG_PASSENGERS_COUNTERS = 28;
+
+ private int packetId;
+
+ private void sendResponse(
+ Channel channel, int packetType, int index, int serviceType, int type, ByteBuf content) {
+ if (channel != null) {
+
+ ByteBuf data = Unpooled.buffer();
+ data.writeByte(type);
+ data.writeShortLE(content.readableBytes());
+ data.writeBytes(content);
+ content.release();
+
+ ByteBuf record = Unpooled.buffer();
+ if (packetType == PT_RESPONSE) {
+ record.writeShortLE(index);
+ record.writeByte(0); // success
+ }
+ record.writeShortLE(data.readableBytes());
+ record.writeShortLE(0);
+ record.writeByte(0); // flags (possibly 1 << 6)
+ record.writeByte(serviceType);
+ record.writeByte(serviceType);
+ record.writeBytes(data);
+ data.release();
+ int recordChecksum = Checksum.crc16(Checksum.CRC16_CCITT_FALSE, record.nioBuffer());
+
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(1); // protocol version
+ response.writeByte(0); // security key id
+ response.writeByte(0); // flags
+ response.writeByte(5 + 2 + 2 + 2); // header length
+ response.writeByte(0); // encoding
+ response.writeShortLE(record.readableBytes());
+ response.writeShortLE(packetId++);
+ response.writeByte(packetType);
+ response.writeByte(Checksum.crc8(Checksum.CRC8_EGTS, response.nioBuffer()));
+ response.writeBytes(record);
+ record.release();
+ response.writeShortLE(recordChecksum);
+
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int index = buf.getUnsignedShort(buf.readerIndex() + 5 + 2);
+ buf.skipBytes(buf.getUnsignedByte(buf.readerIndex() + 3));
+
+ List<Position> positions = new LinkedList<>();
+
+ while (buf.readableBytes() > 2) {
+
+ int length = buf.readUnsignedShortLE();
+ int recordIndex = buf.readUnsignedShortLE();
+ int recordFlags = buf.readUnsignedByte();
+
+ if (BitUtil.check(recordFlags, 0)) {
+ buf.readUnsignedIntLE(); // object id
+ }
+
+ if (BitUtil.check(recordFlags, 1)) {
+ buf.readUnsignedIntLE(); // event id
+ }
+ if (BitUtil.check(recordFlags, 2)) {
+ buf.readUnsignedIntLE(); // time
+ }
+
+ int serviceType = buf.readUnsignedByte();
+ buf.readUnsignedByte(); // recipient service type
+
+ int recordEnd = buf.readerIndex() + length;
+
+ Position position = new Position(getProtocolName());
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession != null) {
+ position.setDeviceId(deviceSession.getDeviceId());
+ }
+
+ ByteBuf response = Unpooled.buffer();
+ response.writeShortLE(recordIndex);
+ response.writeByte(0); // success
+ sendResponse(channel, PT_RESPONSE, index, serviceType, MSG_RECORD_RESPONSE, response);
+
+ while (buf.readerIndex() < recordEnd) {
+ int type = buf.readUnsignedByte();
+ int end = buf.readUnsignedShortLE() + buf.readerIndex();
+
+ if (type == MSG_TERM_IDENTITY) {
+
+ buf.readUnsignedIntLE(); // object id
+ int flags = buf.readUnsignedByte();
+
+ if (BitUtil.check(flags, 0)) {
+ buf.readUnsignedShortLE(); // home dispatcher identifier
+ }
+ if (BitUtil.check(flags, 1)) {
+ getDeviceSession(
+ channel, remoteAddress, buf.readSlice(15).toString(StandardCharsets.US_ASCII).trim());
+ }
+ if (BitUtil.check(flags, 2)) {
+ getDeviceSession(
+ channel, remoteAddress, buf.readSlice(16).toString(StandardCharsets.US_ASCII).trim());
+ }
+ if (BitUtil.check(flags, 3)) {
+ buf.skipBytes(3); // language identifier
+ }
+ if (BitUtil.check(flags, 5)) {
+ buf.skipBytes(3); // network identifier
+ }
+ if (BitUtil.check(flags, 6)) {
+ buf.readUnsignedShortLE(); // buffer size
+ }
+ if (BitUtil.check(flags, 7)) {
+ getDeviceSession(
+ channel, remoteAddress, buf.readSlice(15).toString(StandardCharsets.US_ASCII).trim());
+ }
+
+ response = Unpooled.buffer();
+ response.writeByte(0); // success
+ sendResponse(channel, PT_APPDATA, 0, serviceType, MSG_RESULT_CODE, response);
+
+ } else if (type == MSG_POS_DATA) {
+
+ position.setTime(new Date((buf.readUnsignedIntLE() + 1262304000) * 1000)); // since 2010-01-01
+ position.setLatitude(buf.readUnsignedIntLE() * 90.0 / 0xFFFFFFFFL);
+ position.setLongitude(buf.readUnsignedIntLE() * 180.0 / 0xFFFFFFFFL);
+
+ int flags = buf.readUnsignedByte();
+ position.setValid(BitUtil.check(flags, 0));
+ if (BitUtil.check(flags, 5)) {
+ position.setLatitude(-position.getLatitude());
+ }
+ if (BitUtil.check(flags, 6)) {
+ position.setLongitude(-position.getLongitude());
+ }
+
+ int speed = buf.readUnsignedShortLE();
+ position.setSpeed(UnitsConverter.knotsFromKph(BitUtil.to(speed, 14) * 0.1));
+ position.setCourse(buf.readUnsignedByte() + (BitUtil.check(speed, 15) ? 0x100 : 0));
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedMediumLE() * 100);
+ position.set(Position.KEY_INPUT, buf.readUnsignedByte());
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+
+ if (BitUtil.check(flags, 7)) {
+ position.setAltitude(buf.readMediumLE());
+ }
+
+ } else if (type == MSG_EXT_POS_DATA) {
+
+ int flags = buf.readUnsignedByte();
+
+ if (BitUtil.check(flags, 0)) {
+ position.set(Position.KEY_VDOP, buf.readUnsignedShortLE());
+ }
+ if (BitUtil.check(flags, 1)) {
+ position.set(Position.KEY_HDOP, buf.readUnsignedShortLE());
+ }
+ if (BitUtil.check(flags, 2)) {
+ position.set(Position.KEY_PDOP, buf.readUnsignedShortLE());
+ }
+ if (BitUtil.check(flags, 3)) {
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ }
+
+ } else if (type == MSG_AD_SENSORS_DATA) {
+
+ buf.readUnsignedByte(); // inputs flags
+
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedByte());
+
+ buf.readUnsignedByte(); // adc flags
+
+ }
+
+ buf.readerIndex(end);
+ }
+
+ if (serviceType == SERVICE_TELEDATA && deviceSession != null) {
+ positions.add(position);
+ }
+ }
+
+ return positions.isEmpty() ? null : positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EnforaProtocol.java b/src/main/java/org/traccar/protocol/EnforaProtocol.java
new file mode 100644
index 000000000..f78e4b377
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EnforaProtocol.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class EnforaProtocol extends BaseProtocol {
+
+ public EnforaProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, -2, 2));
+ pipeline.addLast(new EnforaProtocolEncoder());
+ pipeline.addLast(new EnforaProtocolDecoder(EnforaProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new EnforaProtocolEncoder());
+ pipeline.addLast(new EnforaProtocolDecoder(EnforaProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EnforaProtocolDecoder.java b/src/main/java/org/traccar/protocol/EnforaProtocolDecoder.java
new file mode 100644
index 000000000..bfa7a116b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EnforaProtocolDecoder.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BufferUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+public class EnforaProtocolDecoder extends BaseProtocolDecoder {
+
+ public EnforaProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("GPRMC,")
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+)?,") // speed
+ .number("(d+.d+)?,") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .any()
+ .compile();
+
+ public static final int IMEI_LENGTH = 15;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ // Find IMEI number
+ int index = -1;
+ for (int i = buf.readerIndex(); i < buf.writerIndex() - IMEI_LENGTH; i++) {
+ index = i;
+ for (int j = i; j < i + IMEI_LENGTH; j++) {
+ if (!Character.isDigit((char) buf.getByte(j))) {
+ index = -1;
+ break;
+ }
+ }
+ if (index > 0) {
+ break;
+ }
+ }
+ if (index == -1) {
+ return null;
+ }
+
+ String imei = buf.toString(index, IMEI_LENGTH, StandardCharsets.US_ASCII);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ // Find NMEA sentence
+ int start = BufferUtil.indexOf("GPRMC", buf);
+ if (start == -1) {
+ return null;
+ }
+
+ String sentence = buf.toString(start, buf.readableBytes() - start, StandardCharsets.US_ASCII);
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EnforaProtocolEncoder.java b/src/main/java/org/traccar/protocol/EnforaProtocolEncoder.java
new file mode 100644
index 000000000..a46e6367d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EnforaProtocolEncoder.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Jose Castellanos
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class EnforaProtocolEncoder extends StringProtocolEncoder {
+
+ private ByteBuf encodeContent(String content) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeShort(content.length() + 6);
+ buf.writeShort(0); // index
+ buf.writeByte(0x04); // command type
+ buf.writeByte(0); // optional header
+ buf.writeBytes(content.getBytes(StandardCharsets.US_ASCII));
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return encodeContent(command.getString(Command.KEY_DATA));
+ case Command.TYPE_ENGINE_STOP:
+ return encodeContent("AT$IOGP3=1");
+ case Command.TYPE_ENGINE_RESUME:
+ return encodeContent("AT$IOGP3=0");
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EsealProtocol.java b/src/main/java/org/traccar/protocol/EsealProtocol.java
new file mode 100644
index 000000000..7a27c617d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EsealProtocol.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class EsealProtocol extends BaseProtocol {
+
+ public EsealProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_ALARM_ARM,
+ Command.TYPE_ALARM_DISARM);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new EsealProtocolEncoder());
+ pipeline.addLast(new EsealProtocolDecoder(EsealProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EsealProtocolDecoder.java b/src/main/java/org/traccar/protocol/EsealProtocolDecoder.java
new file mode 100644
index 000000000..7a1fd7022
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EsealProtocolDecoder.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class EsealProtocolDecoder extends BaseProtocolDecoder {
+
+ private String config;
+
+ public EsealProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ config = Context.getConfig().getString(getProtocolName() + ".config");
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("##S,")
+ .expression("[^,]+,") // device type
+ .number("(d+),") // device id
+ .number("d+,") // customer id
+ .expression("[^,]+,") // firmware version
+ .expression("([^,]+),") // type
+ .number("(d+),") // index
+ .number("(dddd)-(dd)-(dd),") // date
+ .number("(dd):(dd):(dd),") // time
+ .number("d+,") // interval
+ .expression("([AV]),") // validity
+ .number("(d+.d+)([NS]) ") // latitude
+ .number("(d+.d+)([EW]),") // longitude
+ .number("(d+),") // course
+ .number("(d+),") // speed
+ .expression("([^,]+),") // door
+ .number("(d+.d+),") // acceleration
+ .expression("([^,]+),") // nfc
+ .number("(d+.d+),") // battery
+ .number("(-?d+),") // rssi
+ .text("E##")
+ .compile();
+
+ private void sendResponse(Channel channel, String prefix, String type, String payload) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(
+ prefix + type + "," + payload + ",E##\r\n", channel.remoteAddress()));
+ }
+ }
+
+ private String decodeAlarm(String type) {
+ switch (type) {
+ case "Event-Door":
+ return Position.ALARM_DOOR;
+ case "Event-Shock":
+ return Position.ALARM_SHOCK;
+ case "Event-Drop":
+ return Position.ALARM_FALL_DOWN;
+ case "Event-Lock":
+ return Position.ALARM_LOCK;
+ case "Event-RC-Unlock":
+ return Position.ALARM_UNLOCK;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ String type = parser.next();
+ String prefix = sentence.substring(0, sentence.indexOf(type));
+ int index = parser.nextInt();
+
+ position.set(Position.KEY_INDEX, index);
+ position.set(Position.KEY_ALARM, decodeAlarm(type));
+
+ switch (type) {
+ case "Startup":
+ sendResponse(channel, prefix, type + " ACK", index + "," + config);
+ break;
+ case "Normal":
+ case "Button-Normal":
+ case "Termination":
+ case "Event-Door":
+ case "Event-Shock":
+ case "Event-Drop":
+ case "Event-Lock":
+ case "Event-RC-Unlock":
+ sendResponse(channel, prefix, type + " ACK", String.valueOf(index));
+ break;
+ default:
+ break;
+ }
+
+ position.setTime(parser.nextDateTime());
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setCourse(parser.nextInt());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt()));
+
+ switch (parser.next()) {
+ case "Open":
+ position.set(Position.KEY_DOOR, true);
+ break;
+ case "Close":
+ position.set(Position.KEY_DOOR, false);
+ break;
+ default:
+ break;
+ }
+
+ position.set(Position.KEY_ACCELERATION, parser.nextDouble());
+ position.set("nfc", parser.next());
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.KEY_RSSI, parser.nextInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EsealProtocolEncoder.java b/src/main/java/org/traccar/protocol/EsealProtocolEncoder.java
new file mode 100644
index 000000000..b9bcc5b0a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EsealProtocolEncoder.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class EsealProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatCommand(
+ command, "##S,eSeal,{%s},256,3.0.8,{%s},E##", Command.KEY_UNIQUE_ID, Command.KEY_DATA);
+ case Command.TYPE_ALARM_ARM:
+ return formatCommand(
+ command, "##S,eSeal,{%s},256,3.0.8,RC-Power Control,Power OFF,E##", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ALARM_DISARM:
+ return formatCommand(
+ command, "##S,eSeal,{%s},256,3.0.8,RC-Unlock,E##", Command.KEY_UNIQUE_ID);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EskyFrameDecoder.java b/src/main/java/org/traccar/protocol/EskyFrameDecoder.java
new file mode 100644
index 000000000..da24c1273
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EskyFrameDecoder.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class EskyFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ buf.readerIndex(buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 'E'));
+
+ int endIndex = buf.indexOf(buf.readerIndex() + 1, buf.writerIndex(), (byte) 'E');
+ if (endIndex > 0) {
+ return buf.readRetainedSlice(endIndex - buf.readerIndex());
+ } else {
+ return buf.readRetainedSlice(buf.readableBytes()); // assume full frame
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EskyProtocol.java b/src/main/java/org/traccar/protocol/EskyProtocol.java
new file mode 100644
index 000000000..aaa92da58
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EskyProtocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class EskyProtocol extends BaseProtocol {
+
+ public EskyProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new EskyFrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new EskyProtocolDecoder(EskyProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/EskyProtocolDecoder.java b/src/main/java/org/traccar/protocol/EskyProtocolDecoder.java
new file mode 100644
index 000000000..641b2e28f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/EskyProtocolDecoder.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class EskyProtocolDecoder extends BaseProtocolDecoder {
+
+ public EskyProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("..;") // header
+ .number("d+;") // index
+ .number("(d+);") // imei
+ .text("R;") // data type
+ .number("(d+)[+;]") // satellites
+ .number("(dd)(dd)(dd)") // date
+ .number("(dd)(dd)(dd)[+;]") // time
+ .number("(-?d+.d+)[+;]") // latitude
+ .number("(-?d+.d+)[+;]") // longitude
+ .number("(d+.d+)[+;]") // speed
+ .number("(d+)[+;]") // course
+ .groupBegin()
+ .text("0x").number("(d+)[+;]") // input
+ .number("(d+)[+;]") // message type
+ .number("(d+)[+;]") // odometer
+ .groupEnd("?")
+ .number("(d+)") // voltage
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+
+ position.setValid(true);
+ position.setTime(parser.nextDateTime());
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromMps(parser.nextDouble()));
+ position.setCourse(parser.nextDouble());
+
+ if (parser.hasNext(3)) {
+ position.set(Position.KEY_INPUT, parser.nextHexInt());
+ position.set(Position.KEY_EVENT, parser.nextInt());
+ position.set(Position.KEY_ODOMETER, parser.nextInt());
+ }
+
+ position.set(Position.KEY_BATTERY, parser.nextInt() * 0.001);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ExtremTracProtocol.java b/src/main/java/org/traccar/protocol/ExtremTracProtocol.java
new file mode 100644
index 000000000..692fd4e99
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ExtremTracProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ExtremTracProtocol extends BaseProtocol {
+
+ public ExtremTracProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new ExtremTracProtocolDecoder(ExtremTracProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ExtremTracProtocolDecoder.java b/src/main/java/org/traccar/protocol/ExtremTracProtocolDecoder.java
new file mode 100644
index 000000000..9fde6f0a0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ExtremTracProtocolDecoder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class ExtremTracProtocolDecoder extends BaseProtocolDecoder {
+
+ public ExtremTracProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$GPRMC,")
+ .number("(d+),") // device id
+ .number("(dd)(dd)(dd).(ddd),") // time (hhmmss.sss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FifotrackProtocol.java b/src/main/java/org/traccar/protocol/FifotrackProtocol.java
new file mode 100644
index 000000000..371e01e55
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FifotrackProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class FifotrackProtocol extends BaseProtocol {
+
+ public FifotrackProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new FifotrackProtocolDecoder(FifotrackProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FifotrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/FifotrackProtocolDecoder.java
new file mode 100644
index 000000000..beaa34125
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FifotrackProtocolDecoder.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DataConverter;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class FifotrackProtocolDecoder extends BaseProtocolDecoder {
+
+ private ByteBuf photo;
+
+ public FifotrackProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$$")
+ .number("d+,") // length
+ .number("(d+),") // imei
+ .number("x+,") // index
+ .expression("[^,]+,") // type
+ .number("(d+)?,") // alarm
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("([AV]),") // validity
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(-?d+),") // altitude
+ .number("(d+),") // odometer
+ .number("d+,") // runtime
+ .number("(xxxx),") // status
+ .number("(x+)?,") // input
+ .number("(x+)?,") // output
+ .number("(d+)|") // mcc
+ .number("(d+)|") // mnc
+ .number("(x+)|") // lac
+ .number("(x+),") // cid
+ .number("([x|]+)") // adc
+ .expression(",([^,]+)") // rfid
+ .expression(",([^*]+)").optional(2) // sensors
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_PHOTO = new PatternBuilder()
+ .text("$$")
+ .number("d+,") // length
+ .number("(d+),") // imei
+ .any()
+ .number(",(d+),") // length
+ .expression("([^*]+)") // photo id
+ .text("*")
+ .number("xx")
+ .compile();
+
+ private static final Pattern PATTERN_PHOTO_DATA = new PatternBuilder()
+ .text("$$")
+ .number("d+,") // length
+ .number("(d+),") // imei
+ .expression("([^*]+),") // photo id
+ .number("(d+),") // offset
+ .number("(d+),") // size
+ .number("(x+)") // data
+ .text("*")
+ .number("xx")
+ .compile();
+
+ private void requestPhoto(Channel channel, SocketAddress socketAddress, String imei, String file) {
+ if (channel != null) {
+ String content = "D06," + file + "," + photo.writerIndex() + "," + Math.min(1024, photo.writableBytes());
+ int length = 1 + imei.length() + 1 + content.length() + 5;
+ String response = String.format("@@%02d,%s,%s*", length, imei, content);
+ response += Checksum.sum(response) + "\r\n";
+ channel.writeAndFlush(new NetworkMessage(response, socketAddress));
+ }
+ }
+
+ private Object decodeLocation(
+ Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_ALARM, parser.next());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt(0)));
+ position.setCourse(parser.nextInt(0));
+ position.setAltitude(parser.nextInt(0));
+
+ position.set(Position.KEY_ODOMETER, parser.nextLong(0));
+ position.set(Position.KEY_STATUS, parser.nextHexInt(0));
+ if (parser.hasNext()) {
+ position.set(Position.KEY_INPUT, parser.nextHexInt(0));
+ }
+ if (parser.hasNext()) {
+ position.set(Position.KEY_OUTPUT, parser.nextHexInt(0));
+ }
+
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(0), parser.nextInt(0), parser.nextHexInt(0), parser.nextHexInt(0))));
+
+ String[] adc = parser.next().split("\\|");
+ for (int i = 0; i < adc.length; i++) {
+ position.set(Position.PREFIX_ADC + (i + 1), Integer.parseInt(adc[i], 16));
+ }
+
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ if (parser.hasNext()) {
+ String[] sensors = parser.next().split("\\|");
+ for (int i = 0; i < sensors.length; i++) {
+ position.set(Position.PREFIX_IO + (i + 1), sensors[i]);
+ }
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ int typeIndex = sentence.indexOf(',', sentence.indexOf(',', sentence.indexOf(',') + 1) + 1) + 1;
+ String type = sentence.substring(typeIndex, typeIndex + 3);
+
+ if (type.equals("D05")) {
+ Parser parser = new Parser(PATTERN_PHOTO, sentence);
+ if (parser.matches()) {
+ String imei = parser.next();
+ int length = parser.nextInt();
+ String photoId = parser.next();
+ photo = Unpooled.buffer(length);
+ requestPhoto(channel, remoteAddress, imei, photoId);
+ }
+ } else if (type.equals("D06")) {
+ Parser parser = new Parser(PATTERN_PHOTO_DATA, sentence);
+ if (parser.matches()) {
+ String imei = parser.next();
+ String photoId = parser.next();
+ parser.nextInt(); // offset
+ parser.nextInt(); // size
+ photo.writeBytes(DataConverter.parseHex(parser.next()));
+ requestPhoto(channel, remoteAddress, imei, photoId);
+ }
+ } else {
+ return decodeLocation(channel, remoteAddress, sentence);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FlespiProtocol.java b/src/main/java/org/traccar/protocol/FlespiProtocol.java
new file mode 100644
index 000000000..2c0729b76
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FlespiProtocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class FlespiProtocol extends BaseProtocol {
+
+ public FlespiProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
+ pipeline.addLast(new FlespiProtocolDecoder(FlespiProtocol.this));
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/traccar/protocol/FlespiProtocolDecoder.java b/src/main/java/org/traccar/protocol/FlespiProtocolDecoder.java
new file mode 100644
index 000000000..86da3943e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FlespiProtocolDecoder.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.traccar.BaseHttpProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.model.Position;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonNumber;
+import javax.json.JsonObject;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+import java.io.StringReader;
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class FlespiProtocolDecoder extends BaseHttpProtocolDecoder {
+
+ public FlespiProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ JsonArray result = Json.createReader(new StringReader(request.content().toString(StandardCharsets.UTF_8)))
+ .readArray();
+ List<Position> positions = new LinkedList<>();
+ for (int i = 0; i < result.size(); i++) {
+ JsonObject message = result.getJsonObject(i);
+ JsonString ident = message.getJsonString("ident");
+ if (ident == null) {
+ continue;
+ }
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, ident.getString());
+ if (deviceSession == null) {
+ continue;
+ }
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ decodePosition(message, position);
+ positions.add(position);
+ }
+
+ sendResponse(channel, HttpResponseStatus.OK);
+ return positions;
+ }
+
+ private void decodePosition(JsonObject object, Position position) {
+ for (Map.Entry<String, JsonValue> param : object.entrySet()) {
+ String paramName = param.getKey();
+ JsonValue paramValue = param.getValue();
+ int index = -1;
+ if (paramName.contains("#")) {
+ String[] parts = paramName.split("#");
+ paramName = parts[0];
+ index = Integer.parseInt(parts[1]);
+ }
+ if (!decodeParam(paramName, index, paramValue, position)) {
+ decodeUnknownParam(param.getKey(), param.getValue(), position);
+ }
+ }
+ if (position.getLatitude() == 0 && position.getLongitude() == 0) {
+ getLastLocation(position, position.getDeviceTime());
+ }
+ }
+
+ private boolean decodeParam(String name, int index, JsonValue value, Position position) {
+ switch (name) {
+ case "timestamp":
+ position.setTime(new Date(((JsonNumber) value).longValue() * 1000));
+ return true;
+ case "position.latitude":
+ position.setLatitude(((JsonNumber) value).doubleValue());
+ return true;
+ case "position.longitude":
+ position.setLongitude(((JsonNumber) value).doubleValue());
+ return true;
+ case "position.speed":
+ position.setSpeed(((JsonNumber) value).doubleValue());
+ return true;
+ case "position.direction":
+ position.setCourse(((JsonNumber) value).doubleValue());
+ return true;
+ case "position.altitude":
+ position.setAltitude(((JsonNumber) value).doubleValue());
+ return true;
+ case "position.satellites":
+ position.set(Position.KEY_SATELLITES, ((JsonNumber) value).intValue());
+ return true;
+ case "position.valid":
+ position.setValid(value == JsonValue.TRUE);
+ return true;
+ case "position.hdop":
+ position.set(Position.KEY_HDOP, ((JsonNumber) value).doubleValue());
+ return true;
+ case "position.pdop":
+ position.set(Position.KEY_PDOP, ((JsonNumber) value).doubleValue());
+ return true;
+ case "din":
+ case "dout":
+ position.set(name.equals("din") ? Position.KEY_INPUT : Position.KEY_OUTPUT,
+ ((JsonNumber) value).intValue());
+ return true;
+ case "gps.vehicle.mileage":
+ position.set(Position.KEY_ODOMETER, ((JsonNumber) value).doubleValue());
+ return true;
+ case "external.powersource.voltage":
+ position.set(Position.KEY_POWER, ((JsonNumber) value).doubleValue());
+ return true;
+ case "battery.voltage":
+ position.set(Position.KEY_BATTERY, ((JsonNumber) value).doubleValue());
+ return true;
+ case "fuel.level":
+ case "can.fuel.level":
+ position.set(Position.KEY_FUEL_LEVEL, ((JsonNumber) value).doubleValue());
+ return true;
+ case "engine.rpm":
+ case "can.engine.rpm":
+ position.set(Position.KEY_RPM, ((JsonNumber) value).doubleValue());
+ return true;
+ case "can.engine.temperature":
+ position.set(Position.PREFIX_TEMP + (index > 0 ? index : 0), ((JsonNumber) value).doubleValue());
+ return true;
+ case "engine.ignition.status":
+ position.set(Position.KEY_IGNITION, value == JsonValue.TRUE);
+ return true;
+ case "movement.status":
+ position.set(Position.KEY_MOTION, value == JsonValue.TRUE);
+ return true;
+ case "device.temperature":
+ position.set(Position.KEY_DEVICE_TEMP, ((JsonNumber) value).doubleValue());
+ return true;
+ case "ibutton.code":
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, ((JsonString) value).getString());
+ return true;
+ case "vehicle.vin":
+ position.set(Position.KEY_VIN, ((JsonString) value).getString());
+ return true;
+ case "alarm.event.trigger":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ return true;
+ case "towing.event.trigger":
+ case "towing.alarm.status":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_TOW);
+ }
+ return true;
+ case "geofence.event.enter":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE_ENTER);
+ }
+ return true;
+ case "geofence.event.exit":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE_EXIT);
+ }
+ return true;
+ case "shock.event.trigger":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SHOCK);
+ }
+ return true;
+ case "overspeeding.event.trigger":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ }
+ return true;
+ case "harsh.acceleration.event.trigger":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ }
+ return true;
+ case "harsh.braking.event.trigger":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ }
+ return true;
+ case "harsh.cornering.event.trigger":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_CORNERING);
+ }
+ return true;
+ case "gnss.antenna.cut.status":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GPS_ANTENNA_CUT);
+ }
+ return true;
+ case "gsm.jamming.event.trigger":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_JAMMING);
+ }
+ return true;
+ case "hood.open.status":
+ if (value == JsonValue.TRUE) {
+ position.set(Position.KEY_ALARM, Position.ALARM_BONNET);
+ }
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void decodeUnknownParam(String name, JsonValue value, Position position) {
+ if (value instanceof JsonNumber) {
+ if (((JsonNumber) value).isIntegral()) {
+ position.set(name, ((JsonNumber) value).longValue());
+ } else {
+ position.set(name, ((JsonNumber) value).doubleValue());
+ }
+ position.set(name, ((JsonNumber) value).doubleValue());
+ } else if (value instanceof JsonString) {
+ position.set(name, ((JsonString) value).getString());
+ } else if (value == JsonValue.TRUE || value == JsonValue.FALSE) {
+ position.set(name, value == JsonValue.TRUE);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FlexCommProtocol.java b/src/main/java/org/traccar/protocol/FlexCommProtocol.java
new file mode 100644
index 000000000..9343ebeb8
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FlexCommProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.FixedLengthFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class FlexCommProtocol extends BaseProtocol {
+
+ public FlexCommProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new FixedLengthFrameDecoder(2 + 2 + 101 + 5));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new FlexCommProtocolDecoder(FlexCommProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FlexCommProtocolDecoder.java b/src/main/java/org/traccar/protocol/FlexCommProtocolDecoder.java
new file mode 100644
index 000000000..068c0a05c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FlexCommProtocolDecoder.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class FlexCommProtocolDecoder extends BaseProtocolDecoder {
+
+ public FlexCommProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("7E")
+ .number("(dd)") // status
+ .number("(d{15})") // imei
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .expression("([01])") // valid
+ .number("(d{9})") // latitude
+ .number("(d{10})") // longitude
+ .number("(d{4})") // altitude
+ .number("(ddd)") // speed
+ .number("(ddd)") // course
+ .number("(dd)") // satellites view
+ .number("(dd)") // satellites used
+ .number("(dd)") // rssi
+ .number("(ddd)") // mcc
+ .number("(ddd)") // mnc
+ .number("(x{6})") // lac
+ .number("(x{6})") // cid
+ .expression("([01])([01])([01])") // input
+ .expression("([01])([01])") // output
+ .number("(ddd)") // fuel
+ .number("(d{4})") // temperature
+ .number("(ddd)") // battery
+ .number("(ddd)") // power
+ .any()
+ .compile();
+
+ private static double parseSignedValue(Parser parser, int decimalPoints) {
+ String stringValue = parser.next();
+ boolean negative = stringValue.charAt(0) == '1';
+ double value = Integer.parseInt(stringValue.substring(1)) * Math.pow(10, -decimalPoints);
+ return negative ? -value : value;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ position.set(Position.KEY_STATUS, parser.nextInt());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+ position.setValid(parser.next().equals("1"));
+ position.setLatitude(parseSignedValue(parser, 6));
+ position.setLongitude(parseSignedValue(parser, 6));
+ position.setAltitude(parseSignedValue(parser, 0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt()));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES_VISIBLE, parser.nextInt());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_RSSI, parser.nextInt());
+
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(), parser.nextInt(), parser.nextHexInt(), parser.nextHexInt())));
+
+ for (int i = 1; i <= 3; i++) {
+ position.set(Position.PREFIX_IN + i, parser.nextInt());
+ }
+
+ for (int i = 1; i <= 2; i++) {
+ position.set(Position.PREFIX_OUT + i, parser.nextInt());
+ }
+
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt());
+ position.set(Position.PREFIX_TEMP + 1, parseSignedValue(parser, 0));
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+ position.set(Position.KEY_POWER, parser.nextInt() * 0.1);
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("{01}", remoteAddress));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FlextrackProtocol.java b/src/main/java/org/traccar/protocol/FlextrackProtocol.java
new file mode 100644
index 000000000..ddd1d58f0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FlextrackProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class FlextrackProtocol extends BaseProtocol {
+
+ public FlextrackProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "\r"));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new FlextrackProtocolDecoder(FlextrackProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FlextrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/FlextrackProtocolDecoder.java
new file mode 100644
index 000000000..9dce22ede
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FlextrackProtocolDecoder.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class FlextrackProtocolDecoder extends BaseProtocolDecoder {
+
+ public FlextrackProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_LOGON = new PatternBuilder()
+ .number("(-?d+),") // index
+ .text("LOGON,")
+ .number("(d+),") // node id
+ .number("(d+)") // iccid
+ .compile();
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(-?d+),") // index
+ .text("UNITSTAT,")
+ .number("(dddd)(dd)(dd),") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("d+,") // node id
+ .number("([NS])(d+).(d+.d+),") // latitude
+ .number("([EW])(d+).(d+.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // satellites
+ .number("(d+),") // battery
+ .number("(-?d+),") // gsm
+ .number("(x+),") // state
+ .number("(ddd)") // mcc
+ .number("(dd),") // mnc
+ .number("(-?d+),") // altitude
+ .number("(d+),") // hdop
+ .number("(x+),") // cell
+ .number("d+,") // gps fix time
+ .number("(x+),") // lac
+ .number("(d+)") // odometer
+ .compile();
+
+ private void sendAcknowledgement(Channel channel, SocketAddress remoteAddress, String index) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(index + ",ACK\r", remoteAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.contains("LOGON")) {
+
+ Parser parser = new Parser(PATTERN_LOGON, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ sendAcknowledgement(channel, remoteAddress, parser.next());
+
+ String id = parser.next();
+ String iccid = parser.next();
+
+ getDeviceSession(channel, remoteAddress, iccid, id);
+
+ } else if (sentence.contains("UNITSTAT")) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ sendAcknowledgement(channel, remoteAddress, parser.next());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt(0)));
+ position.setCourse(parser.nextInt(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY, parser.nextInt(0));
+ int rssi = parser.nextInt(0);
+ position.set(Position.KEY_STATUS, parser.nextHexInt(0));
+
+ int mcc = parser.nextInt(0);
+ int mnc = parser.nextInt(0);
+
+ position.setAltitude(parser.nextInt(0));
+
+ position.set(Position.KEY_HDOP, parser.nextInt(0) * 0.1);
+
+ position.setNetwork(new Network(CellTower.from(
+ mcc, mnc, parser.nextHexInt(0), parser.nextHexInt(0), rssi)));
+
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FoxProtocol.java b/src/main/java/org/traccar/protocol/FoxProtocol.java
new file mode 100644
index 000000000..9bac773b5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FoxProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class FoxProtocol extends BaseProtocol {
+
+ public FoxProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "</fox>"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new FoxProtocolDecoder(FoxProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FoxProtocolDecoder.java b/src/main/java/org/traccar/protocol/FoxProtocolDecoder.java
new file mode 100644
index 000000000..449f00022
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FoxProtocolDecoder.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class FoxProtocolDecoder extends BaseProtocolDecoder {
+
+ public FoxProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(d+),") // status id
+ .expression("([AV]),") // validity
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .expression("[^,]*,") // cell info
+ .number("([01]+) ") // input
+ .number("(d+) ") // power
+ .number("(d+) ") // temperature
+ .number("(d+) ") // rpm
+ .number("(d+) ") // fuel
+ .number("(d+) ") // adc 1
+ .number("(d+) ") // adc 2
+ .number("([01]+) ") // output
+ .number("(d+),") // odometer
+ .expression("(.+)") // status info
+ .compile();
+
+ private String getAttribute(String xml, String key) {
+ int start = xml.indexOf(key + "=\"");
+ if (start != -1) {
+ start += key.length() + 2;
+ int end = xml.indexOf("\"", start);
+ if (end != -1) {
+ return xml.substring(start, end);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String xml = (String) msg;
+ String id = getAttribute(xml, "id");
+ String data = getAttribute(xml, "data");
+
+ if (id != null && data != null) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, data);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_STATUS, parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_INPUT, parser.nextBinInt(0));
+ position.set(Position.KEY_POWER, parser.nextDouble(0) * 0.1);
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt(0));
+ position.set(Position.KEY_RPM, parser.nextInt(0));
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt(0));
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt(0));
+ position.set(Position.PREFIX_ADC + 2, parser.nextInt(0));
+ position.set(Position.KEY_OUTPUT, parser.nextBinInt(0));
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+
+ position.set("statusData", parser.next());
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FreedomProtocol.java b/src/main/java/org/traccar/protocol/FreedomProtocol.java
new file mode 100644
index 000000000..bc6b92d5f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FreedomProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class FreedomProtocol extends BaseProtocol {
+
+ public FreedomProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new FreedomProtocolDecoder(FreedomProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FreedomProtocolDecoder.java b/src/main/java/org/traccar/protocol/FreedomProtocolDecoder.java
new file mode 100644
index 000000000..1d2dd3133
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FreedomProtocolDecoder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class FreedomProtocolDecoder extends BaseProtocolDecoder {
+
+ public FreedomProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("IMEI,")
+ .number("(d+),") // imei
+ .number("(dddd)/(dd)/(dd), ") // date (yyyy/dd/mm)
+ .number("(dd):(dd):(dd), ") // time (hh:mm:ss)
+ .expression("([NS]), ")
+ .number("Lat:(dd)(d+.d+), ") // latitude
+ .expression("([EW]), ")
+ .number("Lon:(ddd)(d+.d+), ") // longitude
+ .text("Spd:").number("(d+.d+)") // speed
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(true);
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+
+ position.setSpeed(parser.nextDouble(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FreematicsProtocol.java b/src/main/java/org/traccar/protocol/FreematicsProtocol.java
new file mode 100644
index 000000000..999b075a1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FreematicsProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class FreematicsProtocol extends BaseProtocol {
+
+ public FreematicsProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new FreematicsProtocolDecoder(FreematicsProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/FreematicsProtocolDecoder.java b/src/main/java/org/traccar/protocol/FreematicsProtocolDecoder.java
new file mode 100644
index 000000000..ba47699c3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/FreematicsProtocolDecoder.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class FreematicsProtocolDecoder extends BaseProtocolDecoder {
+
+ public FreematicsProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private Object decodeEvent(
+ Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ DeviceSession deviceSession = null;
+ String event = null;
+ String time = null;
+
+ for (String pair : sentence.split(",")) {
+ String[] data = pair.split("=");
+ String key = data[0];
+ String value = data[1];
+ switch (key) {
+ case "ID":
+ case "VIN":
+ if (deviceSession == null) {
+ deviceSession = getDeviceSession(channel, remoteAddress, value);
+ }
+ break;
+ case "EV":
+ event = value;
+ break;
+ case "TS":
+ time = value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (channel != null && deviceSession != null && event != null && time != null) {
+ String message = String.format("1#EV=%s,RX=1,TS=%s", event, time);
+ message += '*' + Checksum.sum(message);
+ channel.writeAndFlush(new NetworkMessage(message, remoteAddress));
+ }
+
+ return null;
+ }
+
+ private Object decodePosition(
+ Channel channel, SocketAddress remoteAddress, String sentence) throws Exception {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+ Position position = null;
+ DateBuilder dateBuilder = null;
+
+ for (String pair : sentence.split(",")) {
+ String[] data = pair.split("[=:]");
+ int key = Integer.parseInt(data[0], 16);
+ String value = data[1];
+ switch (key) {
+ case 0x0:
+ if (position != null) {
+ position.setTime(dateBuilder.getDate());
+ positions.add(position);
+ }
+ position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setValid(true);
+ dateBuilder = new DateBuilder(new Date());
+ break;
+ case 0x11:
+ value = ("000000" + value).substring(value.length());
+ dateBuilder.setDateReverse(
+ Integer.parseInt(value.substring(0, 2)),
+ Integer.parseInt(value.substring(2, 4)),
+ Integer.parseInt(value.substring(4)));
+ break;
+ case 0x10:
+ value = ("00000000" + value).substring(value.length());
+ dateBuilder.setTime(
+ Integer.parseInt(value.substring(0, 2)),
+ Integer.parseInt(value.substring(2, 4)),
+ Integer.parseInt(value.substring(4, 6)),
+ Integer.parseInt(value.substring(6)) * 10);
+ break;
+ case 0xA:
+ position.setLatitude(Double.parseDouble(value));
+ break;
+ case 0xB:
+ position.setLongitude(Double.parseDouble(value));
+ break;
+ case 0xC:
+ position.setAltitude(Double.parseDouble(value));
+ break;
+ case 0xD:
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(value)));
+ break;
+ case 0xE:
+ position.setCourse(Integer.parseInt(value));
+ break;
+ case 0xF:
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(value));
+ break;
+ case 0x20:
+ position.set(Position.KEY_ACCELERATION, value);
+ break;
+ case 0x24:
+ position.set(Position.KEY_BATTERY, Integer.parseInt(value) * 0.01);
+ break;
+ case 0x81:
+ position.set(Position.KEY_RSSI, Integer.parseInt(value));
+ break;
+ case 0x82:
+ position.set(Position.KEY_DEVICE_TEMP, Integer.parseInt(value) * 0.1);
+ break;
+ default:
+ position.set(data[0], value);
+ break;
+ }
+ }
+
+ if (position != null) {
+ position.setTime(dateBuilder.getDate());
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ int startIndex = sentence.indexOf('#');
+ int endIndex = sentence.indexOf('*');
+
+ if (startIndex > 0 && endIndex > 0) {
+ sentence = sentence.substring(startIndex + 1, endIndex);
+
+ if (sentence.startsWith("EV")) {
+ return decodeEvent(channel, remoteAddress, sentence);
+ } else {
+ return decodePosition(channel, remoteAddress, sentence);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GalileoFrameDecoder.java b/src/main/java/org/traccar/protocol/GalileoFrameDecoder.java
new file mode 100644
index 000000000..c23d26c83
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GalileoFrameDecoder.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class GalileoFrameDecoder extends BaseFrameDecoder {
+
+ private static final int MESSAGE_MINIMUM_LENGTH = 5;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < MESSAGE_MINIMUM_LENGTH) {
+ return null;
+ }
+
+ int length = buf.getUnsignedShortLE(buf.readerIndex() + 1) & 0x7fff;
+ if (buf.readableBytes() >= (length + MESSAGE_MINIMUM_LENGTH)) {
+ return buf.readRetainedSlice(length + MESSAGE_MINIMUM_LENGTH);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GalileoProtocol.java b/src/main/java/org/traccar/protocol/GalileoProtocol.java
new file mode 100644
index 000000000..9b7fe1a4b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GalileoProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class GalileoProtocol extends BaseProtocol {
+
+ public GalileoProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_OUTPUT_CONTROL);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new GalileoFrameDecoder());
+ pipeline.addLast(new GalileoProtocolEncoder());
+ pipeline.addLast(new GalileoProtocolDecoder(GalileoProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GalileoProtocolDecoder.java b/src/main/java/org/traccar/protocol/GalileoProtocolDecoder.java
new file mode 100644
index 000000000..01c55a9ae
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GalileoProtocolDecoder.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class GalileoProtocolDecoder extends BaseProtocolDecoder {
+
+ public GalileoProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private ByteBuf photo;
+
+ private static final Map<Integer, Integer> TAG_LENGTH_MAP = new HashMap<>();
+
+ static {
+ int[] l1 = {
+ 0x01, 0x02, 0x35, 0x43, 0xc4, 0xc5, 0xc6, 0xc7,
+ 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
+ 0xd0, 0xd1, 0xd2, 0xd5, 0x88, 0x8a, 0x8b, 0x8c,
+ 0xa0, 0xaf, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
+ 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae
+ };
+ int[] l2 = {
+ 0x04, 0x10, 0x34, 0x40, 0x41, 0x42, 0x45, 0x46,
+ 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x60, 0x61,
+ 0x62, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
+ 0x77, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
+ 0xb7, 0xb8, 0xb9, 0xd6, 0xd7, 0xd8, 0xd9, 0xda
+ };
+ int[] l3 = {
+ 0x63, 0x64, 0x6f, 0x5d, 0x65, 0x66, 0x67, 0x68,
+ 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e
+ };
+ int[] l4 = {
+ 0x20, 0x33, 0x44, 0x90, 0xc0, 0xc2, 0xc3, 0xd3,
+ 0xd4, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xf0, 0xf9,
+ 0x5a, 0x47, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
+ 0xf7, 0xf8, 0xe2, 0xe9
+ };
+ for (int i : l1) {
+ TAG_LENGTH_MAP.put(i, 1);
+ }
+ for (int i : l2) {
+ TAG_LENGTH_MAP.put(i, 2);
+ }
+ for (int i : l3) {
+ TAG_LENGTH_MAP.put(i, 3);
+ }
+ for (int i : l4) {
+ TAG_LENGTH_MAP.put(i, 4);
+ }
+ TAG_LENGTH_MAP.put(0x5b, 7); // variable length
+ TAG_LENGTH_MAP.put(0x5c, 68);
+ }
+
+ private static int getTagLength(int tag) {
+ Integer length = TAG_LENGTH_MAP.get(tag);
+ if (length == null) {
+ throw new IllegalArgumentException("Unknown tag: " + tag);
+ }
+ return length;
+ }
+
+ private void sendReply(Channel channel, int header, int checksum) {
+ if (channel != null) {
+ ByteBuf reply = Unpooled.buffer(3);
+ reply.writeByte(header);
+ reply.writeShortLE((short) checksum);
+ channel.writeAndFlush(new NetworkMessage(reply, channel.remoteAddress()));
+ }
+ }
+
+ private void decodeTag(Position position, ByteBuf buf, int tag) {
+ if (tag >= 0x50 && tag <= 0x57) {
+ position.set(Position.PREFIX_ADC + (tag - 0x50), buf.readUnsignedShortLE());
+ } else if (tag >= 0x60 && tag <= 0x62) {
+ position.set("fuel" + (tag - 0x60), buf.readUnsignedShortLE());
+ } else if (tag >= 0xa0 && tag <= 0xaf) {
+ position.set("can8BitR" + (tag - 0xa0 + 15), buf.readUnsignedByte());
+ } else if (tag >= 0xb0 && tag <= 0xb9) {
+ position.set("can16BitR" + (tag - 0xb0 + 5), buf.readUnsignedShortLE());
+ } else if (tag >= 0xc4 && tag <= 0xd2) {
+ position.set("can8BitR" + (tag - 0xc4), buf.readUnsignedByte());
+ } else if (tag >= 0xd6 && tag <= 0xda) {
+ position.set("can16BitR" + (tag - 0xd6), buf.readUnsignedShortLE());
+ } else if (tag >= 0xdb && tag <= 0xdf) {
+ position.set("can32BitR" + (tag - 0xdb), buf.readUnsignedIntLE());
+ } else if (tag >= 0xe2 && tag <= 0xe9) {
+ position.set("userData" + (tag - 0xe2), buf.readUnsignedIntLE());
+ } else if (tag >= 0xf0 && tag <= 0xf9) {
+ position.set("can32BitR" + (tag - 0xf0 + 5), buf.readUnsignedIntLE());
+ } else {
+ decodeTagOther(position, buf, tag);
+ }
+ }
+
+ private void decodeTagOther(Position position, ByteBuf buf, int tag) {
+ switch (tag) {
+ case 0x01:
+ position.set(Position.KEY_VERSION_HW, buf.readUnsignedByte());
+ break;
+ case 0x02:
+ position.set(Position.KEY_VERSION_FW, buf.readUnsignedByte());
+ break;
+ case 0x04:
+ position.set("deviceId", buf.readUnsignedShortLE());
+ break;
+ case 0x10:
+ position.set(Position.KEY_INDEX, buf.readUnsignedShortLE());
+ break;
+ case 0x20:
+ position.setTime(new Date(buf.readUnsignedIntLE() * 1000));
+ break;
+ case 0x33:
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShortLE() * 0.1));
+ position.setCourse(buf.readUnsignedShortLE() * 0.1);
+ break;
+ case 0x34:
+ position.setAltitude(buf.readShortLE());
+ break;
+ case 0x35:
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte() * 0.1);
+ break;
+ case 0x40:
+ position.set(Position.KEY_STATUS, buf.readUnsignedShortLE());
+ break;
+ case 0x41:
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() / 1000.0);
+ break;
+ case 0x42:
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() / 1000.0);
+ break;
+ case 0x43:
+ position.set(Position.KEY_DEVICE_TEMP, buf.readByte());
+ break;
+ case 0x44:
+ position.set(Position.KEY_ACCELERATION, buf.readUnsignedIntLE());
+ break;
+ case 0x45:
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedShortLE());
+ break;
+ case 0x46:
+ position.set(Position.KEY_INPUT, buf.readUnsignedShortLE());
+ break;
+ case 0x48:
+ position.set("statusExtended", buf.readUnsignedShortLE());
+ break;
+ case 0x58:
+ position.set("rs2320", buf.readUnsignedShortLE());
+ break;
+ case 0x59:
+ position.set("rs2321", buf.readUnsignedShortLE());
+ break;
+ case 0x90:
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, String.valueOf(buf.readUnsignedIntLE()));
+ break;
+ case 0xc0:
+ position.set("fuelTotal", buf.readUnsignedIntLE() * 0.5);
+ break;
+ case 0xc1:
+ position.set(Position.KEY_FUEL_LEVEL, buf.readUnsignedByte() * 0.4);
+ position.set(Position.PREFIX_TEMP + 1, buf.readUnsignedByte() - 40);
+ position.set(Position.KEY_RPM, buf.readUnsignedShortLE() * 0.125);
+ break;
+ case 0xc2:
+ position.set("canB0", buf.readUnsignedIntLE());
+ break;
+ case 0xc3:
+ position.set("canB1", buf.readUnsignedIntLE());
+ break;
+ case 0xd4:
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ break;
+ case 0xe0:
+ position.set(Position.KEY_INDEX, buf.readUnsignedIntLE());
+ break;
+ case 0xe1:
+ position.set(Position.KEY_RESULT,
+ buf.readSlice(buf.readUnsignedByte()).toString(StandardCharsets.US_ASCII));
+ break;
+ case 0xea:
+ position.set("userDataArray", ByteBufUtil.hexDump(buf.readSlice(buf.readUnsignedByte())));
+ position.set("userDataArray", ByteBufUtil.hexDump(buf.readSlice(buf.readUnsignedByte())));
+ break;
+ default:
+ buf.skipBytes(getTagLength(tag));
+ break;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int header = buf.readUnsignedByte();
+ if (header == 0x01) {
+ return decodePositions(channel, remoteAddress, buf);
+ } else if (header == 0x07) {
+ return decodePhoto(channel, remoteAddress, buf);
+ }
+
+ return null;
+ }
+
+ private Object decodePositions(
+ Channel channel, SocketAddress remoteAddress, ByteBuf buf) throws Exception {
+
+ int length = (buf.readUnsignedShortLE() & 0x7fff) + 3;
+
+ List<Position> positions = new LinkedList<>();
+ Set<Integer> tags = new HashSet<>();
+ boolean hasLocation = false;
+
+ DeviceSession deviceSession = null;
+ Position position = new Position(getProtocolName());
+
+ while (buf.readerIndex() < length) {
+
+ int tag = buf.readUnsignedByte();
+ if (tags.contains(tag)) {
+ if (hasLocation && position.getFixTime() != null) {
+ positions.add(position);
+ }
+ tags.clear();
+ hasLocation = false;
+ position = new Position(getProtocolName()); // new position starts
+ }
+ tags.add(tag);
+
+ if (tag == 0x03) {
+ deviceSession = getDeviceSession(
+ channel, remoteAddress, buf.readSlice(15).toString(StandardCharsets.US_ASCII));
+ } else if (tag == 0x30) {
+ hasLocation = true;
+ position.setValid((buf.readUnsignedByte() & 0xf0) == 0x00);
+ position.setLatitude(buf.readIntLE() / 1000000.0);
+ position.setLongitude(buf.readIntLE() / 1000000.0);
+ } else {
+ decodeTag(position, buf, tag);
+ }
+
+ }
+
+ if (deviceSession == null) {
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+ }
+
+ if (hasLocation && position.getFixTime() != null) {
+ positions.add(position);
+ } else if (position.getAttributes().containsKey(Position.KEY_RESULT)) {
+ position.setDeviceId(deviceSession.getDeviceId());
+ getLastLocation(position, null);
+ positions.add(position);
+ }
+
+ sendReply(channel, 0x02, buf.readUnsignedShortLE());
+
+ for (Position p : positions) {
+ p.setDeviceId(deviceSession.getDeviceId());
+ }
+
+ return positions.isEmpty() ? null : positions;
+ }
+
+ private Object decodePhoto(
+ Channel channel, SocketAddress remoteAddress, ByteBuf buf) throws Exception {
+
+ int length = buf.readUnsignedShortLE();
+
+ Position position = null;
+
+ if (length > 1) {
+
+ if (photo == null) {
+ photo = Unpooled.buffer();
+ }
+
+ buf.readUnsignedByte(); // part number
+ photo.writeBytes(buf, length - 1);
+
+ } else {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ String uniqueId = Context.getIdentityManager().getById(deviceSession.getDeviceId()).getUniqueId();
+
+ position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_IMAGE, Context.getMediaManager().writeFile(uniqueId, photo, "jpg"));
+ photo.release();
+ photo = null;
+
+ }
+
+ sendReply(channel, 0x07, buf.readUnsignedShortLE());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GalileoProtocolEncoder.java b/src/main/java/org/traccar/protocol/GalileoProtocolEncoder.java
new file mode 100644
index 000000000..3b2145e74
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GalileoProtocolEncoder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class GalileoProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeText(String uniqueId, String text) {
+
+ ByteBuf buf = Unpooled.buffer(256);
+
+ buf.writeByte(0x01);
+ buf.writeShortLE(uniqueId.length() + text.length() + 11);
+
+ buf.writeByte(0x03); // imei tag
+ buf.writeBytes(uniqueId.getBytes(StandardCharsets.US_ASCII));
+
+ buf.writeByte(0x04); // device id tag
+ buf.writeShortLE(0); // not needed if imei provided
+
+ buf.writeByte(0xE0); // index tag
+ buf.writeIntLE(0); // index
+
+ buf.writeByte(0xE1); // command text tag
+ buf.writeByte(text.length());
+ buf.writeBytes(text.getBytes(StandardCharsets.US_ASCII));
+
+ buf.writeShortLE(Checksum.crc16(Checksum.CRC16_MODBUS, buf.nioBuffer(0, buf.writerIndex())));
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return encodeText(getUniqueId(command.getDeviceId()), command.getString(Command.KEY_DATA));
+ case Command.TYPE_OUTPUT_CONTROL:
+ return encodeText(getUniqueId(command.getDeviceId()),
+ "Out " + command.getInteger(Command.KEY_INDEX) + "," + command.getString(Command.KEY_DATA));
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GatorProtocol.java b/src/main/java/org/traccar/protocol/GatorProtocol.java
new file mode 100644
index 000000000..ca81caefb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GatorProtocol.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GatorProtocol extends BaseProtocol {
+
+ public GatorProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 3, 2));
+ pipeline.addLast(new GatorProtocolDecoder(GatorProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new GatorProtocolDecoder(GatorProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GatorProtocolDecoder.java b/src/main/java/org/traccar/protocol/GatorProtocolDecoder.java
new file mode 100644
index 000000000..31500bae6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GatorProtocolDecoder.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BcdUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class GatorProtocolDecoder extends BaseProtocolDecoder {
+
+ public GatorProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_HEARTBEAT = 0x21;
+ public static final int MSG_POSITION_DATA = 0x80;
+ public static final int MSG_ROLLCALL_RESPONSE = 0x81;
+ public static final int MSG_ALARM_DATA = 0x82;
+ public static final int MSG_TERMINAL_STATUS = 0x83;
+ public static final int MSG_MESSAGE = 0x84;
+ public static final int MSG_TERMINAL_ANSWER = 0x85;
+ public static final int MSG_BLIND_AREA = 0x8E;
+ public static final int MSG_PICTURE_FRAME = 0x54;
+ public static final int MSG_CAMERA_RESPONSE = 0x56;
+ public static final int MSG_PICTURE_DATA = 0x57;
+
+ public static String decodeId(int b1, int b2, int b3, int b4) {
+
+ int d1 = 30 + ((b1 >> 7) << 3) + ((b2 >> 7) << 2) + ((b3 >> 7) << 1) + (b4 >> 7);
+ int d2 = b1 & 0x7f;
+ int d3 = b2 & 0x7f;
+ int d4 = b3 & 0x7f;
+ int d5 = b4 & 0x7f;
+
+ return String.format("%02d%02d%02d%02d%02d", d1, d2, d3, d4, d5);
+ }
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress, byte calibration) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(0x24); response.writeByte(0x24); // header
+ response.writeByte(MSG_HEARTBEAT); // size
+ response.writeShort(5);
+ response.writeByte(calibration);
+ response.writeByte(0); // main order
+ response.writeByte(0); // slave order
+ response.writeByte(1); // calibration
+ response.writeByte(0x0D);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ int type = buf.readUnsignedByte();
+ buf.readUnsignedShort(); // length
+
+ String id = decodeId(
+ buf.readUnsignedByte(), buf.readUnsignedByte(),
+ buf.readUnsignedByte(), buf.readUnsignedByte());
+
+ sendResponse(channel, remoteAddress, buf.getByte(buf.writerIndex() - 2));
+
+ if (type == MSG_POSITION_DATA || type == MSG_ROLLCALL_RESPONSE
+ || type == MSG_ALARM_DATA || type == MSG_BLIND_AREA) {
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, "1" + id, id);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setYear(BcdUtil.readInteger(buf, 2))
+ .setMonth(BcdUtil.readInteger(buf, 2))
+ .setDay(BcdUtil.readInteger(buf, 2))
+ .setHour(BcdUtil.readInteger(buf, 2))
+ .setMinute(BcdUtil.readInteger(buf, 2))
+ .setSecond(BcdUtil.readInteger(buf, 2));
+ position.setTime(dateBuilder.getDate());
+
+ position.setLatitude(BcdUtil.readCoordinate(buf));
+ position.setLongitude(BcdUtil.readCoordinate(buf));
+ position.setSpeed(UnitsConverter.knotsFromKph(BcdUtil.readInteger(buf, 4)));
+ position.setCourse(BcdUtil.readInteger(buf, 4));
+
+ int flags = buf.readUnsignedByte();
+ position.setValid((flags & 0x80) != 0);
+ position.set(Position.KEY_SATELLITES, flags & 0x0f);
+
+ position.set(Position.KEY_STATUS, buf.readUnsignedByte());
+ position.set("key", buf.readUnsignedByte());
+ position.set("oil", buf.readUnsignedShort() / 10.0);
+ position.set(Position.KEY_POWER, buf.readUnsignedByte() + buf.readUnsignedByte() * 0.01);
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GenxProtocol.java b/src/main/java/org/traccar/protocol/GenxProtocol.java
new file mode 100644
index 000000000..c87ba946a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GenxProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GenxProtocol extends BaseProtocol {
+
+ public GenxProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new GenxProtocolDecoder(GenxProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GenxProtocolDecoder.java b/src/main/java/org/traccar/protocol/GenxProtocolDecoder.java
new file mode 100644
index 000000000..2ae9de7a0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GenxProtocolDecoder.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.text.SimpleDateFormat;
+
+public class GenxProtocolDecoder extends BaseProtocolDecoder {
+
+ private int[] reportColumns;
+
+ public GenxProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ setReportColumns(Context.getConfig().getString(getProtocolName() + ".reportColumns", "1,2,3,4"));
+ }
+
+ public void setReportColumns(String format) {
+ String[] columns = format.split(",");
+ reportColumns = new int[columns.length];
+ for (int i = 0; i < columns.length; i++) {
+ reportColumns[i] = Integer.parseInt(columns[i]);
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String[] values = ((String) msg).split(",");
+
+ Position position = new Position(getProtocolName());
+ position.setValid(true);
+
+ for (int i = 0; i < Math.min(values.length, reportColumns.length); i++) {
+ switch (reportColumns[i]) {
+ case 1:
+ case 28:
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[i]);
+ if (deviceSession != null) {
+ position.setDeviceId(deviceSession.getDeviceId());
+ }
+ break;
+ case 2:
+ position.setTime(new SimpleDateFormat("MM/dd/yy HH:mm:ss").parse(values[i]));
+ break;
+ case 3:
+ position.setLatitude(Double.parseDouble(values[i]));
+ break;
+ case 4:
+ position.setLongitude(Double.parseDouble(values[i]));
+ break;
+ case 11:
+ position.set(Position.KEY_IGNITION, values[i].equals("ON"));
+ break;
+ case 13:
+ position.setSpeed(UnitsConverter.knotsFromKph(Integer.parseInt(values[i])));
+ break;
+ case 17:
+ position.setCourse(Integer.parseInt(values[i]));
+ break;
+ case 23:
+ position.set(Position.KEY_ODOMETER, Double.parseDouble(values[i]) * 1000);
+ break;
+ case 27:
+ position.setAltitude(UnitsConverter.metersFromFeet(Integer.parseInt(values[i])));
+ break;
+ case 46:
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(values[i]));
+ break;
+ default:
+ break;
+ }
+ }
+
+ return position.getDeviceId() != 0 ? position : null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl100Protocol.java b/src/main/java/org/traccar/protocol/Gl100Protocol.java
new file mode 100644
index 000000000..063e606db
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gl100Protocol.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Gl100Protocol extends BaseProtocol {
+
+ public Gl100Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '\0'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Gl100ProtocolDecoder(Gl100Protocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Gl100ProtocolDecoder(Gl100Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl100ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gl100ProtocolDecoder.java
new file mode 100644
index 000000000..ae0383e5c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gl100ProtocolDecoder.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Gl100ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Gl100ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("+RESP:")
+ .expression("GT...,")
+ .number("(d{15}),") // imei
+ .groupBegin()
+ .number("d+,") // number
+ .number("d,") // reserved / geofence id
+ .number("d+") // reserved / geofence alert // battery
+ .or()
+ .number("[^,]*") // calling number
+ .groupEnd(",")
+ .expression("([01]),") // gps fix
+ .number("(d+.d),") // speed
+ .number("(d+),") // course
+ .number("(-?d+.d),") // altitude
+ .number("d*,") // gps accuracy
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+.d+),") // latitude
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.contains("AT+GTHBD=") && channel != null) {
+ String response = "+RESP:GTHBD,GPRS ACTIVE,";
+ response += sentence.substring(9, sentence.lastIndexOf(','));
+ response += '\0';
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress)); // heartbeat response
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.nextInt(0) == 0);
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setLatitude(parser.nextDouble(0));
+
+ position.setTime(parser.nextDateTime());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl200BinaryProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gl200BinaryProtocolDecoder.java
new file mode 100644
index 000000000..c3339bea5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gl200BinaryProtocolDecoder.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitBuffer;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class Gl200BinaryProtocolDecoder extends BaseProtocolDecoder {
+
+ public Gl200BinaryProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private Date decodeTime(ByteBuf buf) {
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(buf.readUnsignedShort(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ return dateBuilder.getDate();
+ }
+
+ public static final int MSG_RSP_LCB = 3;
+ public static final int MSG_RSP_GEO = 8;
+ public static final int MSG_RSP_COMPRESSED = 100;
+
+ private List<Position> decodeLocation(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ List<Position> positions = new LinkedList<>();
+
+ int type = buf.readUnsignedByte();
+
+ buf.readUnsignedInt(); // mask
+ buf.readUnsignedShort(); // length
+ buf.readUnsignedByte(); // device type
+ buf.readUnsignedShort(); // protocol version
+ buf.readUnsignedShort(); // firmware version
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.format("%015d", buf.readLong()));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int battery = buf.readUnsignedByte();
+ int power = buf.readUnsignedShort();
+
+ if (type == MSG_RSP_GEO) {
+ buf.readUnsignedByte(); // reserved
+ buf.readUnsignedByte(); // reserved
+ }
+
+ buf.readUnsignedByte(); // motion status
+ int satellites = buf.readUnsignedByte();
+
+ if (type != MSG_RSP_COMPRESSED) {
+ buf.readUnsignedByte(); // index
+ }
+
+ if (type == MSG_RSP_LCB) {
+ buf.readUnsignedByte(); // phone length
+ for (int b = buf.readUnsignedByte();; b = buf.readUnsignedByte()) {
+ if ((b & 0xf) == 0xf || (b & 0xf0) == 0xf0) {
+ break;
+ }
+ }
+ }
+
+ if (type == MSG_RSP_COMPRESSED) {
+
+ int count = buf.readUnsignedShort();
+
+ BitBuffer bits;
+ int speed = 0;
+ int heading = 0;
+ int latitude = 0;
+ int longitude = 0;
+ long time = 0;
+
+ for (int i = 0; i < count; i++) {
+
+ if (time > 0) {
+ time += 1;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ switch (BitUtil.from(buf.getUnsignedByte(buf.readerIndex()), 8 - 2)) {
+ case 1:
+ bits = new BitBuffer(buf.readSlice(3));
+ bits.readUnsigned(2); // point attribute
+ bits.readUnsigned(1); // fix type
+ speed = bits.readUnsigned(12);
+ heading = bits.readUnsigned(9);
+ longitude = buf.readInt();
+ latitude = buf.readInt();
+ if (time == 0) {
+ time = buf.readUnsignedInt();
+ }
+ break;
+ case 2:
+ bits = new BitBuffer(buf.readSlice(5));
+ bits.readUnsigned(2); // point attribute
+ bits.readUnsigned(1); // fix type
+ speed += bits.readSigned(7);
+ heading += bits.readSigned(7);
+ longitude += bits.readSigned(12);
+ latitude += bits.readSigned(11);
+ break;
+ default:
+ buf.readUnsignedByte(); // invalid or same
+ continue;
+ }
+
+ position.setValid(true);
+ position.setTime(new Date(time * 1000));
+ position.setSpeed(UnitsConverter.knotsFromKph(speed * 0.1));
+ position.setCourse(heading);
+ position.setLongitude(longitude * 0.000001);
+ position.setLatitude(latitude * 0.000001);
+
+ positions.add(position);
+
+ }
+
+ } else {
+
+ int count = buf.readUnsignedByte();
+
+ for (int i = 0; i < count; i++) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_BATTERY_LEVEL, battery);
+ position.set(Position.KEY_POWER, power);
+ position.set(Position.KEY_SATELLITES, satellites);
+
+ int hdop = buf.readUnsignedByte();
+ position.setValid(hdop > 0);
+ position.set(Position.KEY_HDOP, hdop);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedMedium() * 0.1));
+ position.setCourse(buf.readUnsignedShort());
+ position.setAltitude(buf.readShort());
+ position.setLongitude(buf.readInt() * 0.000001);
+ position.setLatitude(buf.readInt() * 0.000001);
+
+ position.setTime(decodeTime(buf));
+
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedShort(),
+ buf.readUnsignedShort(), buf.readUnsignedShort())));
+
+ buf.readUnsignedByte(); // reserved
+
+ positions.add(position);
+
+ }
+
+ }
+
+ return positions;
+ }
+
+ public static final int MSG_EVT_BPL = 6;
+ public static final int MSG_EVT_VGN = 45;
+ public static final int MSG_EVT_VGF = 46;
+ public static final int MSG_EVT_UPD = 15;
+ public static final int MSG_EVT_IDF = 17;
+ public static final int MSG_EVT_GSS = 21;
+ public static final int MSG_EVT_GES = 26;
+ public static final int MSG_EVT_GPJ = 31;
+ public static final int MSG_EVT_RMD = 35;
+ public static final int MSG_EVT_JDS = 33;
+ public static final int MSG_EVT_CRA = 23;
+ public static final int MSG_EVT_UPC = 34;
+
+ private Position decodeEvent(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ Position position = new Position(getProtocolName());
+
+ int type = buf.readUnsignedByte();
+
+ buf.readUnsignedInt(); // mask
+ buf.readUnsignedShort(); // length
+ buf.readUnsignedByte(); // device type
+ buf.readUnsignedShort(); // protocol version
+
+ position.set(Position.KEY_VERSION_FW, String.valueOf(buf.readUnsignedShort()));
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.format("%015d", buf.readLong()));
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte());
+ position.set(Position.KEY_POWER, buf.readUnsignedShort());
+
+ buf.readUnsignedByte(); // motion status
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ switch (type) {
+ case MSG_EVT_BPL:
+ buf.readUnsignedShort(); // backup battery voltage
+ break;
+ case MSG_EVT_VGN:
+ case MSG_EVT_VGF:
+ buf.readUnsignedShort(); // reserved
+ buf.readUnsignedByte(); // report type
+ buf.readUnsignedInt(); // ignition duration
+ break;
+ case MSG_EVT_UPD:
+ buf.readUnsignedShort(); // code
+ buf.readUnsignedByte(); // retry
+ break;
+ case MSG_EVT_IDF:
+ buf.readUnsignedInt(); // idling duration
+ break;
+ case MSG_EVT_GSS:
+ buf.readUnsignedByte(); // gps signal status
+ buf.readUnsignedInt(); // reserved
+ break;
+ case MSG_EVT_GES:
+ buf.readUnsignedShort(); // trigger geo id
+ buf.readUnsignedByte(); // trigger geo enable
+ buf.readUnsignedByte(); // trigger mode
+ buf.readUnsignedInt(); // radius
+ buf.readUnsignedInt(); // check interval
+ break;
+ case MSG_EVT_GPJ:
+ buf.readUnsignedByte(); // cw jamming value
+ buf.readUnsignedByte(); // gps jamming state
+ break;
+ case MSG_EVT_RMD:
+ buf.readUnsignedByte(); // roaming state
+ break;
+ case MSG_EVT_JDS:
+ buf.readUnsignedByte(); // jamming state
+ break;
+ case MSG_EVT_CRA:
+ buf.readUnsignedByte(); // crash counter
+ break;
+ case MSG_EVT_UPC:
+ buf.readUnsignedByte(); // command id
+ buf.readUnsignedShort(); // result
+ break;
+ default:
+ break;
+ }
+
+ buf.readUnsignedByte(); // count
+
+ int hdop = buf.readUnsignedByte();
+ position.setValid(hdop > 0);
+ position.set(Position.KEY_HDOP, hdop);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedMedium() * 0.1));
+ position.setCourse(buf.readUnsignedShort());
+ position.setAltitude(buf.readShort());
+ position.setLongitude(buf.readInt() * 0.000001);
+ position.setLatitude(buf.readInt() * 0.000001);
+
+ position.setTime(decodeTime(buf));
+
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedShort(),
+ buf.readUnsignedShort(), buf.readUnsignedShort())));
+
+ buf.readUnsignedByte(); // reserved
+
+ return position;
+ }
+
+ public static final int MSG_INF_GPS = 2;
+ public static final int MSG_INF_CID = 4;
+ public static final int MSG_INF_CSQ = 5;
+ public static final int MSG_INF_VER = 6;
+ public static final int MSG_INF_BAT = 7;
+ public static final int MSG_INF_TMZ = 9;
+ public static final int MSG_INF_GIR = 10;
+
+ private Position decodeInformation(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ Position position = new Position(getProtocolName());
+
+ int type = buf.readUnsignedByte();
+
+ buf.readUnsignedInt(); // mask
+ buf.readUnsignedShort(); // length
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.format("%015d", buf.readLong()));
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedByte(); // device type
+ buf.readUnsignedShort(); // protocol version
+
+ position.set(Position.KEY_VERSION_FW, String.valueOf(buf.readUnsignedShort()));
+
+ if (type == MSG_INF_VER) {
+ buf.readUnsignedShort(); // hardware version
+ buf.readUnsignedShort(); // mcu version
+ buf.readUnsignedShort(); // reserved
+ }
+
+ buf.readUnsignedByte(); // motion status
+ buf.readUnsignedByte(); // reserved
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ buf.readUnsignedByte(); // mode
+ buf.skipBytes(7); // last fix time
+ buf.readUnsignedByte(); // reserved
+ buf.readUnsignedByte();
+ buf.readUnsignedShort(); // response report mask
+ buf.readUnsignedShort(); // ign interval
+ buf.readUnsignedShort(); // igf interval
+ buf.readUnsignedInt(); // reserved
+ buf.readUnsignedByte(); // reserved
+
+ if (type == MSG_INF_BAT) {
+ position.set(Position.KEY_CHARGE, buf.readUnsignedByte() != 0);
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.001);
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.001);
+ position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte());
+ }
+
+ buf.skipBytes(10); // iccid
+
+ if (type == MSG_INF_CSQ) {
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ buf.readUnsignedByte();
+ }
+
+ buf.readUnsignedByte(); // time zone flags
+ buf.readUnsignedShort(); // time zone offset
+
+ if (type == MSG_INF_GIR) {
+ buf.readUnsignedByte(); // gir trigger
+ buf.readUnsignedByte(); // cell number
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedShort(),
+ buf.readUnsignedShort(), buf.readUnsignedShort())));
+ buf.readUnsignedByte(); // ta
+ buf.readUnsignedByte(); // rx level
+ }
+
+ getLastLocation(position, decodeTime(buf));
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ switch (buf.readSlice(4).toString(StandardCharsets.US_ASCII)) {
+ case "+RSP":
+ return decodeLocation(channel, remoteAddress, buf);
+ case "+INF":
+ return decodeInformation(channel, remoteAddress, buf);
+ case "+EVT":
+ return decodeEvent(channel, remoteAddress, buf);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl200FrameDecoder.java b/src/main/java/org/traccar/protocol/Gl200FrameDecoder.java
new file mode 100644
index 000000000..c192cc28d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gl200FrameDecoder.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Gl200FrameDecoder extends BaseFrameDecoder {
+
+ private static final int MINIMUM_LENGTH = 11;
+
+ private static final Set<String> BINARY_HEADERS = new HashSet<>(
+ Arrays.asList("+RSP", "+BSP", "+EVT", "+BVT", "+INF", "+BNF", "+HBD", "+CRD", "+BRD"));
+
+ public static boolean isBinary(ByteBuf buf) {
+ String header = buf.toString(buf.readerIndex(), 4, StandardCharsets.US_ASCII);
+ if (header.equals("+ACK")) {
+ return buf.getByte(buf.readerIndex() + header.length()) != (byte) ':';
+ } else {
+ return BINARY_HEADERS.contains(header);
+ }
+ }
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < MINIMUM_LENGTH) {
+ return null;
+ }
+
+ if (isBinary(buf)) {
+
+ int length;
+ switch (buf.toString(buf.readerIndex(), 4, StandardCharsets.US_ASCII)) {
+ case "+ACK":
+ length = buf.getUnsignedByte(buf.readerIndex() + 6);
+ break;
+ case "+INF":
+ case "+BNF":
+ length = buf.getUnsignedShort(buf.readerIndex() + 7);
+ break;
+ case "+HBD":
+ length = buf.getUnsignedByte(buf.readerIndex() + 5);
+ break;
+ case "+CRD":
+ case "+BRD":
+ length = buf.getUnsignedShort(buf.readerIndex() + 6);
+ break;
+ default:
+ length = buf.getUnsignedShort(buf.readerIndex() + 9);
+ break;
+ }
+
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ } else {
+
+ int endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '$');
+ if (endIndex < 0) {
+ endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0);
+ }
+ if (endIndex > 0) {
+ ByteBuf frame = buf.readRetainedSlice(endIndex - buf.readerIndex());
+ buf.readByte(); // delimiter
+ return frame;
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl200Protocol.java b/src/main/java/org/traccar/protocol/Gl200Protocol.java
new file mode 100644
index 000000000..c5343dae0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gl200Protocol.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+import io.netty.handler.codec.string.StringEncoder;
+
+public class Gl200Protocol extends BaseProtocol {
+
+ public Gl200Protocol() {
+ setSupportedDataCommands(
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_IDENTIFICATION,
+ Command.TYPE_REBOOT_DEVICE);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Gl200FrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Gl200ProtocolEncoder());
+ pipeline.addLast(new Gl200ProtocolDecoder(Gl200Protocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Gl200ProtocolEncoder());
+ pipeline.addLast(new Gl200ProtocolDecoder(Gl200Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl200ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gl200ProtocolDecoder.java
new file mode 100644
index 000000000..ca1df7a13
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gl200ProtocolDecoder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocolDecoder;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.Protocol;
+
+import java.net.SocketAddress;
+
+public class Gl200ProtocolDecoder extends BaseProtocolDecoder {
+
+ private final Gl200TextProtocolDecoder textProtocolDecoder;
+ private final Gl200BinaryProtocolDecoder binaryProtocolDecoder;
+
+ public Gl200ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ textProtocolDecoder = new Gl200TextProtocolDecoder(protocol);
+ binaryProtocolDecoder = new Gl200BinaryProtocolDecoder(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (Gl200FrameDecoder.isBinary(buf)) {
+ return binaryProtocolDecoder.decode(channel, remoteAddress, msg);
+ } else {
+ return textProtocolDecoder.decode(channel, remoteAddress, msg);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl200ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Gl200ProtocolEncoder.java
new file mode 100644
index 000000000..285106c67
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gl200ProtocolEncoder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class Gl200ProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ initDevicePassword(command, "");
+
+ switch (command.getType()) {
+ case Command.TYPE_POSITION_SINGLE:
+ return formatCommand(command, "AT+GTRTO={%s},1,,,,,,FFFF$", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, "AT+GTOUT={%s},1,,,0,0,0,0,0,0,0,,,,,,,FFFF$",
+ Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, "AT+GTOUT={%s},0,,,0,0,0,0,0,0,0,,,,,,,FFFF$",
+ Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_IDENTIFICATION:
+ return formatCommand(command, "AT+GTRTO={%s},8,,,,,,FFFF$", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_REBOOT_DEVICE:
+ return formatCommand(command, "AT+GTRTO={%s},3,,,,,,FFFF$", Command.KEY_DEVICE_PASSWORD);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java
new file mode 100644
index 000000000..aeb57a116
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java
@@ -0,0 +1,1266 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+import org.traccar.model.WifiAccessPoint;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Gl200TextProtocolDecoder extends BaseProtocolDecoder {
+
+ private boolean ignoreFixTime;
+
+ public Gl200TextProtocolDecoder(Protocol protocol) {
+ super(protocol);
+
+ ignoreFixTime = Context.getConfig().getBoolean(getProtocolName() + ".ignoreFixTime");
+ }
+
+ private static final Pattern PATTERN_ACK = new PatternBuilder()
+ .text("+ACK:GT")
+ .expression("...,") // type
+ .number("([0-9A-Z]{2}xxxx),") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .any().text(",")
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(xxxx)") // counter
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_INF = new PatternBuilder()
+ .text("+").expression("(?:RESP|BUFF):GTINF,")
+ .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("(?:[0-9A-Z]{17},)?") // vin
+ .expression("(?:[^,]+)?,") // device name
+ .number("(xx),") // state
+ .expression("(?:[0-9Ff]{20})?,") // iccid
+ .number("(d{1,2}),") // rssi
+ .number("d{1,2},")
+ .expression("[01],") // external power
+ .number("([d.]+)?,") // odometer or external power
+ .number("d*,") // backup battery or lightness
+ .number("(d+.d+),") // battery
+ .expression("([01]),") // charging
+ .number("(?:d),") // led
+ .number("(?:d)?,") // gps on need
+ .number("(?:d)?,") // gps antenna type
+ .number("(?:d)?,").optional() // gps antenna state
+ .number("d{14},") // last fix time
+ .groupBegin()
+ .number("(d+),") // battery percentage
+ .number("[d.]*,") // flash type / power
+ .number("(-?[d.]+)?,,,") // temperature
+ .or()
+ .expression("(?:[01])?,").optional() // pin15 mode
+ .number("(d+)?,") // adc1
+ .number("(d+)?,").optional() // adc2
+ .number("(xx)?,") // digital input
+ .number("(xx)?,") // digital output
+ .number("[-+]dddd,") // timezone
+ .expression("[01],") // daylight saving
+ .groupEnd()
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(xxxx)") // counter
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_VER = new PatternBuilder()
+ .text("+").expression("(?:RESP|BUFF):GTVER,")
+ .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("[^,]*,") // device name
+ .expression("([^,]*),") // device type
+ .number("(xxxx),") // firmware version
+ .number("(xxxx),") // hardware version
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(xxxx)") // counter
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_LOCATION = new PatternBuilder()
+ .number("(d{1,2})?,") // hdop
+ .number("(d{1,3}.d)?,") // speed
+ .number("(d{1,3})?,") // course
+ .number("(-?d{1,5}.d)?,") // altitude
+ .number("(-?d{1,3}.d{6})?,") // longitude
+ .number("(-?d{1,2}.d{6})?,") // latitude
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(d+)?,") // mcc
+ .number("(d+)?,") // mnc
+ .groupBegin()
+ .number("(d+),") // lac
+ .number("(d+),") // cid
+ .or()
+ .number("(x+)?,") // lac
+ .number("(x+)?,") // cid
+ .groupEnd()
+ .number("(?:d+|(d+.d))?,") // odometer
+ .compile();
+
+ private static final Pattern PATTERN_OBD = new PatternBuilder()
+ .text("+RESP:GTOBD,")
+ .number("[0-9A-Z]{2}xxxx,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("(?:[0-9A-Z]{17})?,") // vin
+ .expression("[^,]{0,20},") // device name
+ .expression("[01],") // report type
+ .number("x{1,8},") // report mask
+ .expression("(?:[0-9A-Z]{17})?,") // vin
+ .number("[01],") // obd connect
+ .number("(?:d{1,5})?,") // obd voltage
+ .number("(?:x{8})?,") // support pids
+ .number("(d{1,5})?,") // engine rpm
+ .number("(d{1,3})?,") // speed
+ .number("(-?d{1,3})?,") // coolant temp
+ .number("(d+.?d*|Inf|NaN)?,") // fuel consumption
+ .number("(d{1,5})?,") // dtcs cleared distance
+ .number("(?:d{1,5})?,")
+ .expression("([01])?,") // obd connect
+ .number("(d{1,3})?,") // number of dtcs
+ .number("(x*),") // dtcs
+ .number("(d{1,3})?,") // throttle
+ .number("(?:d{1,3})?,") // engine load
+ .number("(d{1,3})?,") // fuel level
+ .expression("(?:[0-9A],)?") // obd protocol
+ .number("(d+),") // odometer
+ .expression(PATTERN_LOCATION.pattern())
+ .number("(d{1,7}.d)?,") // odometer
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_FRI = new PatternBuilder()
+ .text("+").expression("(?:RESP|BUFF):GT...,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("(?:([0-9A-Z]{17}),)?") // vin
+ .expression("[^,]*,") // device name
+ .number("(d+)?,") // power
+ .number("d{1,2},").optional() // report type
+ .number("d{1,2},").optional() // count
+ .number(",").optional() // reserved
+ .number("(d+),").optional() // battery
+ .expression("((?:")
+ .expression(PATTERN_LOCATION.pattern())
+ .expression(")+)")
+ .groupBegin()
+ .number("(d{1,7}.d)?,") // odometer
+ .number("(d{5}:dd:dd)?,") // hour meter
+ .number("(x+)?,") // adc 1
+ .number("(x+)?,") // adc 2
+ .number("(d{1,3})?,") // battery
+ .number("(?:(xx)(xx)(xx))?,") // device status
+ .number("(d+)?,") // rpm
+ .number("(?:d+.?d*|Inf|NaN)?,") // fuel consumption
+ .number("(d+)?,") // fuel level
+ .or()
+ .number("(d{1,7}.d)?,").optional() // odometer
+ .number("(d{1,3})?,") // battery
+ .groupEnd()
+ .any()
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_ERI = new PatternBuilder()
+ .text("+").expression("(?:RESP|BUFF):GTERI,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("[^,]*,") // device name
+ .number("(x{8}),") // mask
+ .number("(d+)?,") // power
+ .number("d{1,2},") // report type
+ .number("d{1,2},") // count
+ .expression("((?:")
+ .expression(PATTERN_LOCATION.pattern())
+ .expression(")+)")
+ .number("(d{1,7}.d)?,") // odometer
+ .number("(d{5}:dd:dd)?,") // hour meter
+ .number("(x+)?,") // adc 1
+ .number("(x+)?,").optional() // adc 2
+ .number("(d{1,3})?,") // battery
+ .number("(?:(xx)(xx)(xx))?,") // device status
+ .expression("(.*)") // additional data
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_IGN = new PatternBuilder()
+ .text("+").expression("(?:RESP|BUFF):GTIG[NF],")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("[^,]*,") // device name
+ .number("d+,") // ignition off duration
+ .expression(PATTERN_LOCATION.pattern())
+ .number("(d{5}:dd:dd)?,") // hour meter
+ .number("(d{1,7}.d)?,") // odometer
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_LSW = new PatternBuilder()
+ .text("+RESP:").expression("GT[LT]SW,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("[^,]*,") // device name
+ .number("[01],") // type
+ .number("([01]),") // state
+ .expression(PATTERN_LOCATION.pattern())
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_IDA = new PatternBuilder()
+ .text("+RESP:GTIDA,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("[^,]*,,") // device name
+ .number("([^,]+),") // rfid
+ .expression("[01],") // report type
+ .number("1,") // count
+ .expression(PATTERN_LOCATION.pattern())
+ .number("(d+.d),") // odometer
+ .text(",,,,")
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_WIF = new PatternBuilder()
+ .text("+RESP:GTWIF,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("[^,]*,") // device name
+ .number("(d+),") // count
+ .number("((?:x{12},-?d+,,,,)+),,,,") // wifi
+ .number("(d{1,3}),") // battery
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_GSM = new PatternBuilder()
+ .text("+RESP:GTGSM,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("(?:STR|CTN|NMR|RTL),") // fix type
+ .expression("(.*)") // cells
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_PNA = new PatternBuilder()
+ .text("+RESP:GT").expression("P[NF]A,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("[^,]*,") // device name
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("+").expression("(?:RESP|BUFF):GT...,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .expression("[^,]*,") // device name
+ .number("d*,")
+ .number("(x{1,2}),") // report type
+ .number("d{1,2},") // count
+ .expression(PATTERN_LOCATION.pattern())
+ .groupBegin()
+ .number("(d{1,7}.d)?,").optional() // odometer
+ .number("(d{1,3})?,") // battery
+ .or()
+ .number("(d{1,7}.d)?,") // odometer
+ .groupEnd()
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private static final Pattern PATTERN_BASIC = new PatternBuilder()
+ .text("+").expression("(?:RESP|BUFF)").text(":")
+ .expression("GT...,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,").optional() // protocol version
+ .number("(d{15}|x{14}),") // imei
+ .any()
+ .number("(d{1,2})?,") // hdop
+ .number("(d{1,3}.d)?,") // speed
+ .number("(d{1,3})?,") // course
+ .number("(-?d{1,5}.d)?,") // altitude
+ .number("(-?d{1,3}.d{6})?,") // longitude
+ .number("(-?d{1,2}.d{6})?,") // latitude
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(x+),") // lac
+ .number("(x+),").optional(4) // cell
+ .any()
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)").optional(2) // time (hhmmss)
+ .text(",")
+ .number("(xxxx)") // count number
+ .text("$").optional()
+ .compile();
+
+ private Object decodeAck(Channel channel, SocketAddress remoteAddress, String sentence, String type) {
+ Parser parser = new Parser(PATTERN_ACK, sentence);
+ if (parser.matches()) {
+ String protocolVersion = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ if (type.equals("HBD")) {
+ if (channel != null) {
+ parser.skip(6);
+ channel.writeAndFlush(new NetworkMessage(
+ "+SACK:GTHBD," + protocolVersion + "," + parser.next() + "$", remoteAddress));
+ }
+ } else {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ getLastLocation(position, parser.nextDateTime());
+ position.setValid(false);
+ position.set(Position.KEY_RESULT, "Command " + type + " accepted");
+ return position;
+ }
+ }
+ return null;
+ }
+
+ private Position initPosition(Parser parser, Channel channel, SocketAddress remoteAddress) {
+ if (parser.matches()) {
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession != null) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ return position;
+ }
+ }
+ return null;
+ }
+
+ private void decodeDeviceTime(Position position, Parser parser) {
+ if (parser.hasNext(6)) {
+ if (ignoreFixTime) {
+ position.setTime(parser.nextDateTime());
+ } else {
+ position.setDeviceTime(parser.nextDateTime());
+ }
+ }
+ }
+
+ private Long parseHours(String hoursString) {
+ if (hoursString != null) {
+ String[] hours = hoursString.split(":");
+ return (long) (Integer.parseInt(hours[0]) * 3600
+ + (hours.length > 1 ? Integer.parseInt(hours[1]) * 60 : 0)
+ + (hours.length > 2 ? Integer.parseInt(hours[2]) : 0)) * 1000;
+ }
+ return null;
+ }
+
+ private Object decodeInf(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_INF, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ switch (parser.nextHexInt()) {
+ case 0x16:
+ case 0x1A:
+ case 0x12:
+ position.set(Position.KEY_IGNITION, false);
+ position.set(Position.KEY_MOTION, true);
+ break;
+ case 0x11:
+ position.set(Position.KEY_IGNITION, false);
+ position.set(Position.KEY_MOTION, false);
+ break;
+ case 0x21:
+ position.set(Position.KEY_IGNITION, true);
+ position.set(Position.KEY_MOTION, false);
+ break;
+ case 0x22:
+ position.set(Position.KEY_IGNITION, true);
+ position.set(Position.KEY_MOTION, true);
+ break;
+ case 0x41:
+ position.set(Position.KEY_MOTION, false);
+ break;
+ case 0x42:
+ position.set(Position.KEY_MOTION, true);
+ break;
+ default:
+ break;
+ }
+
+ position.set(Position.KEY_RSSI, parser.nextInt());
+
+ parser.next(); // odometer or external power
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.KEY_CHARGE, parser.nextInt() == 1);
+
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+
+ position.set(Position.PREFIX_TEMP + 1, parser.next());
+
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+
+ getLastLocation(position, parser.nextDateTime());
+
+ position.set(Position.KEY_INDEX, parser.nextHexInt());
+
+ return position;
+ }
+
+ private Object decodeVer(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_VER, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ position.set("deviceType", parser.next());
+ position.set(Position.KEY_VERSION_FW, parser.nextHexInt());
+ position.set(Position.KEY_VERSION_HW, parser.nextHexInt());
+
+ getLastLocation(position, parser.nextDateTime());
+
+ return position;
+ }
+
+ private void skipLocation(Parser parser) {
+ parser.skip(19);
+ }
+
+ private void decodeLocation(Position position, Parser parser) {
+ Integer hdop = parser.nextInt();
+ position.setValid(hdop == null || hdop > 0);
+ position.set(Position.KEY_HDOP, hdop);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ if (parser.hasNext(8)) {
+ position.setValid(true);
+ position.setLongitude(parser.nextDouble());
+ position.setLatitude(parser.nextDouble());
+ position.setTime(parser.nextDateTime());
+ } else {
+ getLastLocation(position, null);
+ }
+
+ if (parser.hasNext(6)) {
+ int mcc = parser.nextInt();
+ int mnc = parser.nextInt();
+ if (parser.hasNext(2)) {
+ position.setNetwork(new Network(CellTower.from(mcc, mnc, parser.nextInt(), parser.nextInt())));
+ }
+ if (parser.hasNext(2)) {
+ position.setNetwork(new Network(CellTower.from(mcc, mnc, parser.nextHexInt(), parser.nextHexInt())));
+ }
+ }
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+ }
+ }
+
+ private Object decodeObd(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_OBD, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ position.set(Position.KEY_RPM, parser.nextInt());
+ position.set(Position.KEY_OBD_SPEED, parser.nextInt());
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt());
+ position.set(Position.KEY_FUEL_CONSUMPTION, parser.next());
+ position.set("dtcsClearedDistance", parser.nextInt());
+ if (parser.hasNext()) {
+ position.set("odbConnect", parser.nextInt() == 1);
+ }
+ position.set("dtcsNumber", parser.nextInt());
+ position.set("dtcsCodes", parser.next());
+ position.set(Position.KEY_THROTTLE, parser.nextInt());
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt());
+ if (parser.hasNext()) {
+ position.set(Position.KEY_OBD_ODOMETER, parser.nextInt() * 1000);
+ }
+
+ decodeLocation(position, parser);
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_OBD_ODOMETER, (int) (parser.nextDouble() * 1000));
+ }
+
+ decodeDeviceTime(position, parser);
+
+ return position;
+ }
+
+ private Object decodeCan(Channel channel, SocketAddress remoteAddress, String sentence) throws ParseException {
+ Position position = new Position(getProtocolName());
+
+ int index = 0;
+ String[] values = sentence.split(",");
+
+ index += 1; // header
+ index += 1; // protocol version
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ index += 1; // device name
+ index += 1; // report type
+ index += 1; // canbus state
+ long reportMask = Long.parseLong(values[index++], 16);
+ long reportMaskExt = 0;
+
+ if (BitUtil.check(reportMask, 0)) {
+ position.set(Position.KEY_VIN, values[index++]);
+ }
+ if (BitUtil.check(reportMask, 1)) {
+ position.set(Position.KEY_IGNITION, Integer.parseInt(values[index++]) > 0);
+ }
+ if (BitUtil.check(reportMask, 2)) {
+ position.set(Position.KEY_OBD_ODOMETER, values[index++]);
+ }
+ if (BitUtil.check(reportMask, 3) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_FUEL_USED, Double.parseDouble(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 5) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_RPM, Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 4) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_OBD_SPEED, UnitsConverter.knotsFromKph(Integer.parseInt(values[index - 1])));
+ }
+ if (BitUtil.check(reportMask, 6) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_COOLANT_TEMP, Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 7) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_FUEL_CONSUMPTION, Double.parseDouble(values[index - 1].substring(1)));
+ }
+ if (BitUtil.check(reportMask, 8) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_FUEL_LEVEL, Double.parseDouble(values[index - 1].substring(1)));
+ }
+ if (BitUtil.check(reportMask, 9) && !values[index++].isEmpty()) {
+ position.set("range", Long.parseLong(values[index - 1]) * 100);
+ }
+ if (BitUtil.check(reportMask, 10) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_THROTTLE, Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 11) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromHours(Double.parseDouble(values[index - 1])));
+ }
+ if (BitUtil.check(reportMask, 12)) {
+ position.set("drivingHours", Double.parseDouble(values[index++]));
+ }
+ if (BitUtil.check(reportMask, 13)) {
+ position.set("idleHours", Double.parseDouble(values[index++]));
+ }
+ if (BitUtil.check(reportMask, 14) && !values[index++].isEmpty()) {
+ position.set("idleFuelConsumption", Double.parseDouble(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 15) && !values[index++].isEmpty()) {
+ position.set(Position.KEY_AXLE_WEIGHT, Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 16) && !values[index++].isEmpty()) {
+ position.set("tachographInfo", Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 17) && !values[index++].isEmpty()) {
+ position.set("indicators", Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 18) && !values[index++].isEmpty()) {
+ position.set("lights", Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 19) && !values[index++].isEmpty()) {
+ position.set("doors", Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 20) && !values[index++].isEmpty()) {
+ position.set("vehicleOverspeed", Double.parseDouble(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 21) && !values[index++].isEmpty()) {
+ position.set("engineOverspeed", Double.parseDouble(values[index - 1]));
+ }
+ if (BitUtil.check(reportMask, 29)) {
+ reportMaskExt = Long.parseLong(values[index++], 16);
+ }
+ if (BitUtil.check(reportMaskExt, 0) && !values[index++].isEmpty()) {
+ position.set("adBlueLevel", Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMaskExt, 1) && !values[index++].isEmpty()) {
+ position.set("axleWeight1", Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMaskExt, 2) && !values[index++].isEmpty()) {
+ position.set("axleWeight3", Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMaskExt, 3) && !values[index++].isEmpty()) {
+ position.set("axleWeight4", Integer.parseInt(values[index - 1]));
+ }
+ if (BitUtil.check(reportMaskExt, 4)) {
+ index += 1; // tachograph overspeed
+ }
+ if (BitUtil.check(reportMaskExt, 5)) {
+ index += 1; // tachograph motion
+ }
+ if (BitUtil.check(reportMaskExt, 6)) {
+ index += 1; // tachograph direction
+ }
+ if (BitUtil.check(reportMaskExt, 7) && !values[index++].isEmpty()) {
+ position.set(Position.PREFIX_ADC + 1, Integer.parseInt(values[index - 1]) * 0.001);
+ }
+ if (BitUtil.check(reportMaskExt, 8)) {
+ index += 1; // pedal breaking factor
+ }
+ if (BitUtil.check(reportMaskExt, 9)) {
+ index += 1; // engine breaking factor
+ }
+ if (BitUtil.check(reportMaskExt, 10)) {
+ index += 1; // total accelerator kick-downs
+ }
+ if (BitUtil.check(reportMaskExt, 11)) {
+ index += 1; // total effective engine speed
+ }
+ if (BitUtil.check(reportMaskExt, 12)) {
+ index += 1; // total cruise control time
+ }
+ if (BitUtil.check(reportMaskExt, 13)) {
+ index += 1; // total accelerator kick-down time
+ }
+ if (BitUtil.check(reportMaskExt, 14)) {
+ index += 1; // total brake application
+ }
+ if (BitUtil.check(reportMaskExt, 15) && !values[index++].isEmpty()) {
+ position.set("driver1Card", values[index - 1]);
+ }
+ if (BitUtil.check(reportMaskExt, 16) && !values[index++].isEmpty()) {
+ position.set("driver2Card", values[index - 1]);
+ }
+ if (BitUtil.check(reportMaskExt, 17) && !values[index++].isEmpty()) {
+ position.set("driver1Name", values[index - 1]);
+ }
+ if (BitUtil.check(reportMaskExt, 18) && !values[index++].isEmpty()) {
+ position.set("driver2Name", values[index - 1]);
+ }
+ if (BitUtil.check(reportMaskExt, 19) && !values[index++].isEmpty()) {
+ position.set("registration", values[index - 1]);
+ }
+ if (BitUtil.check(reportMaskExt, 20)) {
+ index += 1; // expansion information
+ }
+ if (BitUtil.check(reportMaskExt, 21)) {
+ index += 1; // rapid brakings
+ }
+ if (BitUtil.check(reportMaskExt, 22)) {
+ index += 1; // rapid accelerations
+ }
+ if (BitUtil.check(reportMaskExt, 23)) {
+ index += 1; // engine torque
+ }
+
+ DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ if (BitUtil.check(reportMask, 30)) {
+ while (values[index].isEmpty()) {
+ index += 1;
+ }
+ position.setValid(Integer.parseInt(values[index++]) > 0);
+ if (!values[index].isEmpty()) {
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
+ position.setCourse(Integer.parseInt(values[index++]));
+ position.setAltitude(Double.parseDouble(values[index++]));
+ position.setLongitude(Double.parseDouble(values[index++]));
+ position.setLatitude(Double.parseDouble(values[index++]));
+ position.setTime(dateFormat.parse(values[index++]));
+ } else {
+ index += 6; // no location
+ getLastLocation(position, null);
+ }
+ } else {
+ getLastLocation(position, null);
+ }
+
+ if (BitUtil.check(reportMask, 31)) {
+ index += 4; // cell
+ index += 1; // reserved
+ }
+
+ if (ignoreFixTime) {
+ position.setTime(dateFormat.parse(values[index]));
+ } else {
+ position.setDeviceTime(dateFormat.parse(values[index]));
+ }
+
+ return position;
+ }
+
+ private void decodeStatus(Position position, Parser parser) {
+ if (parser.hasNext(3)) {
+ int ignition = parser.nextHexInt();
+ if (BitUtil.check(ignition, 4)) {
+ position.set(Position.KEY_IGNITION, false);
+ } else if (BitUtil.check(ignition, 5)) {
+ position.set(Position.KEY_IGNITION, true);
+ }
+ position.set(Position.KEY_INPUT, parser.nextHexInt());
+ position.set(Position.KEY_OUTPUT, parser.nextHexInt());
+ }
+ }
+
+ private Object decodeFri(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_FRI, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ LinkedList<Position> positions = new LinkedList<>();
+
+ String vin = parser.next();
+ Integer power = parser.nextInt();
+ Integer battery = parser.nextInt();
+
+ Parser itemParser = new Parser(PATTERN_LOCATION, parser.next());
+ while (itemParser.find()) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_VIN, vin);
+
+ decodeLocation(position, itemParser);
+
+ positions.add(position);
+ }
+
+ Position position = positions.getLast();
+
+ skipLocation(parser);
+
+ if (power != null && power > 10) {
+ position.set(Position.KEY_POWER, power * 0.001); // only on some devices
+ }
+ if (battery != null) {
+ position.set(Position.KEY_BATTERY_LEVEL, battery);
+ }
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+ }
+ position.set(Position.KEY_HOURS, parseHours(parser.next()));
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+
+ decodeStatus(position, parser);
+
+ position.set(Position.KEY_RPM, parser.nextInt());
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt());
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+ }
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+
+ decodeDeviceTime(position, parser);
+ if (ignoreFixTime) {
+ positions.clear();
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+ private Object decodeEri(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_ERI, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ long mask = parser.nextHexLong();
+
+ LinkedList<Position> positions = new LinkedList<>();
+
+ Integer power = parser.nextInt();
+
+ Parser itemParser = new Parser(PATTERN_LOCATION, parser.next());
+ while (itemParser.find()) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ decodeLocation(position, itemParser);
+
+ positions.add(position);
+ }
+
+ Position position = positions.getLast();
+
+ skipLocation(parser);
+
+ if (power != null) {
+ position.set(Position.KEY_POWER, power * 0.001);
+ }
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+ position.set(Position.KEY_HOURS, parseHours(parser.next()));
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+
+ decodeStatus(position, parser);
+
+ int index = 0;
+ String[] data = parser.next().split(",");
+
+ index += 1; // device type
+
+ if (BitUtil.check(mask, 0)) {
+ index += 1; // digital fuel sensor data
+ }
+
+ if (BitUtil.check(mask, 1)) {
+ int deviceCount = Integer.parseInt(data[index++]);
+ for (int i = 1; i <= deviceCount; i++) {
+ index += 1; // id
+ index += 1; // type
+ if (!data[index++].isEmpty()) {
+ position.set(Position.PREFIX_TEMP + i, (short) Integer.parseInt(data[index - 1], 16) * 0.0625);
+ }
+ }
+ }
+
+ if (BitUtil.check(mask, 2)) {
+ index += 1; // can data
+ }
+
+ if (BitUtil.check(mask, 3) || BitUtil.check(mask, 4)) {
+ int deviceCount = Integer.parseInt(data[index++]);
+ for (int i = 1; i <= deviceCount; i++) {
+ index += 1; // type
+ if (BitUtil.check(mask, 3)) {
+ position.set(Position.KEY_FUEL_LEVEL, Double.parseDouble(data[index++]));
+ }
+ if (BitUtil.check(mask, 4)) {
+ index += 1; // volume
+ }
+ }
+ }
+
+ decodeDeviceTime(position, parser);
+ if (ignoreFixTime) {
+ positions.clear();
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+ private Object decodeIgn(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_IGN, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ decodeLocation(position, parser);
+
+ position.set(Position.KEY_IGNITION, sentence.contains("IGN"));
+ position.set(Position.KEY_HOURS, parseHours(parser.next()));
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+
+ decodeDeviceTime(position, parser);
+
+ return position;
+ }
+
+ private Object decodeLsw(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_LSW, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ position.set(Position.PREFIX_IN + (sentence.contains("LSW") ? 1 : 2), parser.nextInt() == 1);
+
+ decodeLocation(position, parser);
+
+ decodeDeviceTime(position, parser);
+
+ return position;
+ }
+
+ private Object decodeIda(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_IDA, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ decodeLocation(position, parser);
+
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+
+ decodeDeviceTime(position, parser);
+
+ return position;
+ }
+
+ private Object decodeWif(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_WIF, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ getLastLocation(position, null);
+
+ Network network = new Network();
+
+ parser.nextInt(); // count
+ Matcher matcher = Pattern.compile("([0-9a-fA-F]{12}),(-?\\d+),,,,").matcher(parser.next());
+ while (matcher.find()) {
+ String mac = matcher.group(1).replaceAll("(..)", "$1:");
+ network.addWifiAccessPoint(WifiAccessPoint.from(
+ mac.substring(0, mac.length() - 1), Integer.parseInt(matcher.group(2))));
+ }
+
+ position.setNetwork(network);
+
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+
+ return position;
+ }
+
+ private Object decodeGsm(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_GSM, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ getLastLocation(position, null);
+
+ Network network = new Network();
+
+ String[] data = parser.next().split(",");
+ for (int i = 0; i < 6; i++) {
+ if (!data[i * 6].isEmpty()) {
+ network.addCellTower(CellTower.from(
+ Integer.parseInt(data[i * 6]), Integer.parseInt(data[i * 6 + 1]),
+ Integer.parseInt(data[i * 6 + 2], 16), Integer.parseInt(data[i * 6 + 3], 16),
+ Integer.parseInt(data[i * 6 + 4])));
+ }
+ }
+
+ position.setNetwork(network);
+
+ return position;
+ }
+
+ private Object decodePna(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_PNA, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_ALARM, sentence.contains("PNA") ? Position.ALARM_POWER_ON : Position.ALARM_POWER_OFF);
+
+ return position;
+ }
+
+ private Object decodeOther(Channel channel, SocketAddress remoteAddress, String sentence, String type) {
+ Parser parser = new Parser(PATTERN, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ int reportType = parser.nextHexInt();
+ if (type.equals("NMR")) {
+ position.set(Position.KEY_MOTION, reportType == 1);
+ } else if (type.equals("SOS")) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ } else if (type.equals("DIS")) {
+ position.set(Position.PREFIX_IN + reportType / 0x10, reportType % 0x10 == 1);
+ } else if (type.equals("IGL")) {
+ position.set(Position.KEY_IGNITION, reportType % 0x10 == 1);
+ }
+
+ decodeLocation(position, parser);
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+ }
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+ }
+
+ decodeDeviceTime(position, parser);
+
+ if (Context.getConfig().getBoolean(getProtocolName() + ".ack") && channel != null) {
+ channel.writeAndFlush(new NetworkMessage("+SACK:" + parser.next() + "$", remoteAddress));
+ }
+
+ return position;
+ }
+
+ private Object decodeBasic(Channel channel, SocketAddress remoteAddress, String sentence, String type) {
+ Parser parser = new Parser(PATTERN_BASIC, sentence);
+ Position position = initPosition(parser, channel, remoteAddress);
+ if (position == null) {
+ return null;
+ }
+
+ if (parser.hasNext()) {
+ int hdop = parser.nextInt();
+ position.setValid(hdop > 0);
+ position.set(Position.KEY_HDOP, hdop);
+ }
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ if (parser.hasNext(2)) {
+ position.setLongitude(parser.nextDouble());
+ position.setLatitude(parser.nextDouble());
+ } else {
+ getLastLocation(position, null);
+ }
+
+ if (parser.hasNext(6)) {
+ position.setTime(parser.nextDateTime());
+ }
+
+ if (parser.hasNext(4)) {
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(), parser.nextInt(), parser.nextHexInt(), parser.nextHexInt())));
+ }
+
+ decodeDeviceTime(position, parser);
+
+ switch (type) {
+ case "TOW":
+ position.set(Position.KEY_ALARM, Position.ALARM_TOW);
+ break;
+ case "IDL":
+ position.set(Position.KEY_ALARM, Position.ALARM_IDLE);
+ break;
+ case "PNA":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_ON);
+ break;
+ case "PFA":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_OFF);
+ break;
+ case "EPN":
+ case "MPN":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_RESTORED);
+ break;
+ case "EPF":
+ case "MPF":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_CUT);
+ break;
+ case "BPL":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ case "STT":
+ position.set(Position.KEY_ALARM, Position.ALARM_MOVEMENT);
+ break;
+ case "SWG":
+ position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE);
+ break;
+ case "TMP":
+ case "TEM":
+ position.set(Position.KEY_ALARM, Position.ALARM_TEMPERATURE);
+ break;
+ case "JDR":
+ case "JDS":
+ position.set(Position.KEY_ALARM, Position.ALARM_JAMMING);
+ break;
+ default:
+ break;
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = ((ByteBuf) msg).toString(StandardCharsets.US_ASCII);
+
+ int typeIndex = sentence.indexOf(":GT");
+ if (typeIndex < 0) {
+ return null;
+ }
+
+ Object result;
+ String type = sentence.substring(typeIndex + 3, typeIndex + 6);
+ if (sentence.startsWith("+ACK")) {
+ result = decodeAck(channel, remoteAddress, sentence, type);
+ } else {
+ switch (type) {
+ case "INF":
+ result = decodeInf(channel, remoteAddress, sentence);
+ break;
+ case "OBD":
+ result = decodeObd(channel, remoteAddress, sentence);
+ break;
+ case "CAN":
+ result = decodeCan(channel, remoteAddress, sentence);
+ break;
+ case "FRI":
+ case "GEO":
+ case "STR":
+ result = decodeFri(channel, remoteAddress, sentence);
+ break;
+ case "ERI":
+ result = decodeEri(channel, remoteAddress, sentence);
+ break;
+ case "IGN":
+ case "IGF":
+ result = decodeIgn(channel, remoteAddress, sentence);
+ break;
+ case "LSW":
+ case "TSW":
+ result = decodeLsw(channel, remoteAddress, sentence);
+ break;
+ case "IDA":
+ result = decodeIda(channel, remoteAddress, sentence);
+ break;
+ case "WIF":
+ result = decodeWif(channel, remoteAddress, sentence);
+ break;
+ case "GSM":
+ result = decodeGsm(channel, remoteAddress, sentence);
+ break;
+ case "VER":
+ result = decodeVer(channel, remoteAddress, sentence);
+ break;
+ case "PNA":
+ case "PFA":
+ result = decodePna(channel, remoteAddress, sentence);
+ break;
+ default:
+ result = decodeOther(channel, remoteAddress, sentence, type);
+ break;
+ }
+
+ if (result == null) {
+ result = decodeBasic(channel, remoteAddress, sentence, type);
+ }
+
+ if (result != null) {
+ if (result instanceof Position) {
+ ((Position) result).set(Position.KEY_TYPE, type);
+ } else {
+ for (Position p : (List<Position>) result) {
+ p.set(Position.KEY_TYPE, type);
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GlobalSatProtocol.java b/src/main/java/org/traccar/protocol/GlobalSatProtocol.java
new file mode 100644
index 000000000..5612515c9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GlobalSatProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GlobalSatProtocol extends BaseProtocol {
+
+ public GlobalSatProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '!'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new GlobalSatProtocolDecoder(GlobalSatProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GlobalSatProtocolDecoder.java b/src/main/java/org/traccar/protocol/GlobalSatProtocolDecoder.java
new file mode 100644
index 000000000..3d4ab5760
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GlobalSatProtocolDecoder.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class GlobalSatProtocolDecoder extends BaseProtocolDecoder {
+
+ private String format0;
+ private String format1;
+
+ public GlobalSatProtocolDecoder(Protocol protocol) {
+ super(protocol);
+
+ format0 = Context.getConfig().getString(getProtocolName() + ".format0", "TSPRXAB27GHKLMnaicz*U!");
+ format1 = Context.getConfig().getString(getProtocolName() + ".format1", "SARY*U!");
+ }
+
+ public void setFormat0(String format) {
+ format0 = format;
+ }
+
+ public void setFormat1(String format) {
+ format1 = format;
+ }
+
+ private Position decodeOriginal(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("ACK\r", remoteAddress));
+ }
+
+ String format;
+ if (sentence.startsWith("GSr")) {
+ format = format0;
+ } else if (sentence.startsWith("GSh")) {
+ format = format1;
+ } else {
+ return null;
+ }
+
+ // Check that message contains required parameters
+ if (!format.contains("B") || !format.contains("S") || !(format.contains("1")
+ || format.contains("2") || format.contains("3")) || !(format.contains("6")
+ || format.contains("7") || format.contains("8"))) {
+ return null;
+ }
+
+ if (format.contains("*")) {
+ format = format.substring(0, format.indexOf('*'));
+ sentence = sentence.substring(0, sentence.indexOf('*'));
+ }
+ String[] values = sentence.split(",");
+
+ Position position = new Position(getProtocolName());
+
+ for (int formatIndex = 0, valueIndex = 1; formatIndex < format.length()
+ && valueIndex < values.length; formatIndex++) {
+ String value = values[valueIndex];
+
+ switch (format.charAt(formatIndex)) {
+ case 'S':
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, value);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+ break;
+ case 'A':
+ if (value.isEmpty()) {
+ position.setValid(false);
+ } else {
+ position.setValid(Integer.parseInt(value) != 1);
+ }
+ break;
+ case 'B':
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDay(Integer.parseInt(value.substring(0, 2)))
+ .setMonth(Integer.parseInt(value.substring(2, 4)))
+ .setYear(Integer.parseInt(value.substring(4)));
+ value = values[++valueIndex];
+ dateBuilder
+ .setHour(Integer.parseInt(value.substring(0, 2)))
+ .setMinute(Integer.parseInt(value.substring(2, 4)))
+ .setSecond(Integer.parseInt(value.substring(4)));
+ position.setTime(dateBuilder.getDate());
+ break;
+ case 'C':
+ valueIndex += 1;
+ break;
+ case '1':
+ double longitude = Double.parseDouble(value.substring(1));
+ if (value.charAt(0) == 'W') {
+ longitude = -longitude;
+ }
+ position.setLongitude(longitude);
+ break;
+ case '2':
+ longitude = Double.parseDouble(value.substring(4)) / 60;
+ longitude += Integer.parseInt(value.substring(1, 4));
+ if (value.charAt(0) == 'W') {
+ longitude = -longitude;
+ }
+ position.setLongitude(longitude);
+ break;
+ case '3':
+ position.setLongitude(Double.parseDouble(value) * 0.000001);
+ break;
+ case '6':
+ double latitude = Double.parseDouble(value.substring(1));
+ if (value.charAt(0) == 'S') {
+ latitude = -latitude;
+ }
+ position.setLatitude(latitude);
+ break;
+ case '7':
+ latitude = Double.parseDouble(value.substring(3)) / 60;
+ latitude += Integer.parseInt(value.substring(1, 3));
+ if (value.charAt(0) == 'S') {
+ latitude = -latitude;
+ }
+ position.setLatitude(latitude);
+ break;
+ case '8':
+ position.setLatitude(Double.parseDouble(value) * 0.000001);
+ break;
+ case 'G':
+ position.setAltitude(Double.parseDouble(value));
+ break;
+ case 'H':
+ position.setSpeed(Double.parseDouble(value));
+ break;
+ case 'I':
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(value)));
+ break;
+ case 'J':
+ position.setSpeed(UnitsConverter.knotsFromMph(Double.parseDouble(value)));
+ break;
+ case 'K':
+ position.setCourse(Double.parseDouble(value));
+ break;
+ case 'N':
+ if (value.endsWith("mV")) {
+ position.set(Position.KEY_BATTERY,
+ Integer.parseInt(value.substring(0, value.length() - 2)) / 1000.0);
+ } else {
+ position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(value));
+ }
+ break;
+ default:
+ // Unsupported
+ break;
+ }
+
+ valueIndex += 1;
+ }
+ return position;
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$")
+ .number("(d+),") // imei
+ .number("d+,") // mode
+ .number("(d+),") // fix
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([EW])")
+ .number("(ddd)(dd.d+),") // longitude (dddmm.mmmm)
+ .expression("([NS])")
+ .number("(dd)(dd.d+),") // latitude (ddmm.mmmm)
+ .number("(d+.?d*),") // altitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(d+)[,*]") // satellites
+ .number("(d+.?d*)") // hdop
+ .compile();
+
+ private Position decodeAlternative(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(!parser.next().equals("1"));
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setAltitude(parser.nextDouble(0));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("GS")) {
+ return decodeOriginal(channel, remoteAddress, sentence);
+ } else if (sentence.startsWith("$")) {
+ return decodeAlternative(channel, remoteAddress, sentence);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GnxProtocol.java b/src/main/java/org/traccar/protocol/GnxProtocol.java
new file mode 100644
index 000000000..3576bf805
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GnxProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GnxProtocol extends BaseProtocol {
+
+ public GnxProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "\n\r"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new GnxProtocolDecoder(GnxProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GnxProtocolDecoder.java b/src/main/java/org/traccar/protocol/GnxProtocolDecoder.java
new file mode 100644
index 000000000..c9c221a69
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GnxProtocolDecoder.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class GnxProtocolDecoder extends BaseProtocolDecoder {
+
+ public GnxProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_LOCATION = new PatternBuilder()
+ .number("(d+),") // imei
+ .number("d+,") // length
+ .expression("([01]),") // history
+ .number("(dd)(dd)(dd),") // device time (hhmmss)
+ .number("(dd)(dd)(dd),") // device date (ddmmyy)
+ .number("(dd)(dd)(dd),") // fix time (hhmmss)
+ .number("(dd)(dd)(dd),") // fix date (ddmmyy)
+ .number("(d),") // valid
+ .number("(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd.d+),") // longitude
+ .expression("([EW]),")
+ .compile();
+
+ private static final Pattern PATTERN_MIF = new PatternBuilder()
+ .text("$GNX_MIF,")
+ .expression(PATTERN_LOCATION.pattern())
+ .expression("[01],") // valid card
+ .expression("([^,]+),") // rfid
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_OTHER = new PatternBuilder()
+ .text("$GNX_")
+ .expression("...,")
+ .expression(PATTERN_LOCATION.pattern())
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ String type = sentence.substring(5, 8);
+
+ Pattern pattern;
+ if (type.equals("MIF")) {
+ pattern = PATTERN_MIF;
+ } else {
+ pattern = PATTERN_OTHER;
+ }
+
+ Parser parser = new Parser(pattern, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.nextInt(0) == 1) {
+ position.set(Position.KEY_ARCHIVE, true);
+ }
+
+ position.setDeviceTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY, "GMT+5:30"));
+ position.setFixTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY, "GMT+5:30"));
+
+ position.setValid(parser.nextInt(0) != 0);
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+
+ if (type.equals("MIF")) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GoSafeProtocol.java b/src/main/java/org/traccar/protocol/GoSafeProtocol.java
new file mode 100644
index 000000000..853b78a16
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GoSafeProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GoSafeProtocol extends BaseProtocol {
+
+ public GoSafeProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '#'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new GoSafeProtocolDecoder(GoSafeProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GoSafeProtocolDecoder.java b/src/main/java/org/traccar/protocol/GoSafeProtocolDecoder.java
new file mode 100644
index 000000000..95ef18f20
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GoSafeProtocolDecoder.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class GoSafeProtocolDecoder extends BaseProtocolDecoder {
+
+ public GoSafeProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("*GS") // header
+ .number("d+,") // protocol version
+ .number("(d+),") // imei
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .expression("([^#]*)#?") // data
+ .compile();
+
+ private static final Pattern PATTERN_OLD = new PatternBuilder()
+ .text("*GS") // header
+ .number("d+,") // protocol version
+ .number("(d+),") // imei
+ .text("GPS:")
+ .number("(dd)(dd)(dd);") // time (hhmmss)
+ .number("d;").optional() // fix type
+ .expression("([AV]);") // validity
+ .number("([NS])(d+.d+);") // latitude
+ .number("([EW])(d+.d+);") // longitude
+ .number("(d+)?;") // speed
+ .number("(d+);") // course
+ .number("(d+.?d*)").optional() // hdop
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .compile();
+
+ private void decodeFragment(Position position, String fragment) {
+ int dataIndex = fragment.indexOf(':');
+ int index = 0;
+ String[] values;
+ if (fragment.length() == dataIndex + 1) {
+ values = new String[0];
+ } else {
+ values = fragment.substring(dataIndex + 1).split(";");
+ }
+ switch (fragment.substring(0, dataIndex)) {
+ case "GPS":
+ position.setValid(values[index++].equals("A"));
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(values[index++]));
+ position.setLatitude(Double.parseDouble(values[index].substring(1)));
+ if (values[index++].charAt(0) == 'S') {
+ position.setLatitude(-position.getLatitude());
+ }
+ position.setLongitude(Double.parseDouble(values[index].substring(1)));
+ if (values[index++].charAt(0) == 'W') {
+ position.setLongitude(-position.getLongitude());
+ }
+ if (!values[index++].isEmpty()) {
+ position.setSpeed(UnitsConverter.knotsFromKph(Integer.parseInt(values[index - 1])));
+ }
+ position.setCourse(Integer.parseInt(values[index++]));
+ if (index < values.length) {
+ position.setAltitude(Integer.parseInt(values[index++]));
+ }
+ if (index < values.length) {
+ position.set(Position.KEY_HDOP, Double.parseDouble(values[index++]));
+ }
+ if (index < values.length) {
+ position.set(Position.KEY_VDOP, Double.parseDouble(values[index++]));
+ }
+ break;
+ case "GSM":
+ index += 1; // registration status
+ index += 1; // signal strength
+ position.setNetwork(new Network(CellTower.from(
+ Integer.parseInt(values[index++]), Integer.parseInt(values[index++]),
+ Integer.parseInt(values[index++], 16), Integer.parseInt(values[index++], 16),
+ Integer.parseInt(values[index++]))));
+ break;
+ case "COT":
+ if (index < values.length) {
+ position.set(Position.KEY_ODOMETER, Long.parseLong(values[index++]));
+ }
+ if (index < values.length) {
+ String[] hours = values[index].split("-");
+ position.set(Position.KEY_HOURS, (Integer.parseInt(hours[0]) * 3600
+ + (hours.length > 1 ? Integer.parseInt(hours[1]) * 60 : 0)
+ + (hours.length > 2 ? Integer.parseInt(hours[2]) : 0)) * 1000);
+ }
+ break;
+ case "ADC":
+ position.set(Position.KEY_POWER, Double.parseDouble(values[index++]));
+ if (index < values.length) {
+ position.set(Position.KEY_BATTERY, Double.parseDouble(values[index++]));
+ }
+ if (index < values.length) {
+ position.set(Position.PREFIX_ADC + 1, Double.parseDouble(values[index++]));
+ }
+ if (index < values.length) {
+ position.set(Position.PREFIX_ADC + 2, Double.parseDouble(values[index++]));
+ }
+ break;
+ case "DTT":
+ position.set(Position.KEY_STATUS, Integer.parseInt(values[index++], 16));
+ if (!values[index++].isEmpty()) {
+ int io = Integer.parseInt(values[index - 1], 16);
+ position.set(Position.KEY_IGNITION, BitUtil.check(io, 0));
+ position.set(Position.PREFIX_IN + 1, BitUtil.check(io, 1));
+ position.set(Position.PREFIX_IN + 2, BitUtil.check(io, 2));
+ position.set(Position.PREFIX_IN + 3, BitUtil.check(io, 3));
+ position.set(Position.PREFIX_IN + 4, BitUtil.check(io, 4));
+ position.set(Position.PREFIX_OUT + 1, BitUtil.check(io, 5));
+ position.set(Position.PREFIX_OUT + 2, BitUtil.check(io, 6));
+ position.set(Position.PREFIX_OUT + 3, BitUtil.check(io, 7));
+ }
+ position.set(Position.KEY_GEOFENCE, values[index++] + values[index++]);
+ position.set("eventStatus", values[index++]);
+ if (index < values.length) {
+ position.set("packetType", values[index++]);
+ }
+ break;
+ case "ETD":
+ position.set("eventData", values[index++]);
+ break;
+ case "OBD":
+ position.set("obd", values[index++]);
+ break;
+ case "TAG":
+ position.set("tagData", values[index++]);
+ break;
+ case "IWD":
+ if (index < values.length && values[index + 1].equals("0")) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, values[index + 2]);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private Object decodeData(DeviceSession deviceSession, Date time, String data) {
+
+ List<Position> positions = new LinkedList<>();
+ Position position = null;
+ int index = 0;
+ String[] fragments = data.split(",");
+
+ while (index < fragments.length) {
+
+ if (fragments[index].isEmpty() || Character.isDigit(fragments[index].charAt(0))) {
+
+ if (position != null) {
+ positions.add(position);
+ }
+
+ position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setTime(time);
+
+ if (!fragments[index++].isEmpty()) {
+ position.set(Position.KEY_EVENT, Integer.parseInt(fragments[index - 1]));
+ }
+
+ } else {
+
+ decodeFragment(position, fragments[index++]);
+
+ }
+
+ }
+
+ if (position != null) {
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("1234", remoteAddress));
+ }
+
+ String sentence = (String) msg;
+ Pattern pattern = PATTERN;
+ if (sentence.startsWith("*GS02")) {
+ pattern = PATTERN_OLD;
+ }
+
+ Parser parser = new Parser(pattern, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (pattern == PATTERN_OLD) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_HDOP, parser.next());
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+
+ } else {
+
+ Date time = new Date();
+ if (parser.hasNext(6)) {
+ time = parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY);
+ }
+
+ return decodeData(deviceSession, time, parser.next());
+
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GotopProtocol.java b/src/main/java/org/traccar/protocol/GotopProtocol.java
new file mode 100644
index 000000000..07fe02248
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GotopProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GotopProtocol extends BaseProtocol {
+
+ public GotopProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '#'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new GotopProtocolDecoder(GotopProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GotopProtocolDecoder.java b/src/main/java/org/traccar/protocol/GotopProtocolDecoder.java
new file mode 100644
index 000000000..2ef975fe5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GotopProtocolDecoder.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class GotopProtocolDecoder extends BaseProtocolDecoder {
+
+ public GotopProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(d+),") // imei
+ .expression("[^,]+,") // type
+ .expression("([AV]),") // validity
+ .number("DATE:(dd)(dd)(dd),") // date (yyddmm)
+ .number("TIME:(dd)(dd)(dd),") // time (hhmmss)
+ .number("LAT:(d+.d+)([NS]),") // latitude
+ .number("LOT:(d+.d+)([EW]),") // longitude
+ .text("Speed:").number("(d+.d+),") // speed
+ .expression("([^,]+),") // status
+ .number("(d+)?") // course
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.next().equals("A"));
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+
+ position.set(Position.KEY_STATUS, parser.next());
+
+ position.setCourse(parser.nextDouble(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gps056FrameDecoder.java b/src/main/java/org/traccar/protocol/Gps056FrameDecoder.java
new file mode 100644
index 000000000..0d84bf231
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gps056FrameDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+import java.nio.charset.StandardCharsets;
+
+public class Gps056FrameDecoder extends BaseFrameDecoder {
+
+ private static final int MESSAGE_HEADER = 4;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() >= MESSAGE_HEADER) {
+ int length = Integer.parseInt(buf.toString(2, 2, StandardCharsets.US_ASCII)) + 5;
+ if (buf.readableBytes() >= length) {
+ ByteBuf frame = buf.readRetainedSlice(length);
+ while (buf.isReadable() && buf.getUnsignedByte(buf.readerIndex()) != '$') {
+ buf.readByte();
+ }
+ return frame;
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gps056Protocol.java b/src/main/java/org/traccar/protocol/Gps056Protocol.java
new file mode 100644
index 000000000..b6ab10a19
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gps056Protocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Gps056Protocol extends BaseProtocol {
+
+ public Gps056Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Gps056FrameDecoder());
+ pipeline.addLast(new Gps056ProtocolDecoder(Gps056Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gps056ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gps056ProtocolDecoder.java
new file mode 100644
index 000000000..0ba79bb51
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gps056ProtocolDecoder.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+
+public class Gps056ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Gps056ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static void sendResponse(Channel channel, String type, String imei, ByteBuf content) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ String header = "*" + type + imei;
+ response.writeBytes(header.getBytes(StandardCharsets.US_ASCII));
+ if (content != null) {
+ response.writeBytes(content);
+ }
+ response.writeByte('#');
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ private static double decodeCoordinate(ByteBuf buf) {
+ double degrees = buf.getUnsignedShort(buf.readerIndex()) / 100;
+ double minutes = buf.readUnsignedShort() % 100 + buf.readUnsignedShort() * 0.0001;
+ degrees += minutes / 60;
+ byte hemisphere = buf.readByte();
+ if (hemisphere == 'S' || hemisphere == 'W') {
+ degrees = -degrees;
+ }
+ return degrees;
+ }
+
+ private static void decodeStatus(ByteBuf buf, Position position) {
+
+ position.set(Position.KEY_INPUT, buf.readUnsignedByte());
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedByte());
+
+ position.set(Position.PREFIX_ADC + 1, buf.readShortLE() * 5.06); // mV
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ buf.skipBytes(2); // length
+
+ String type = buf.readSlice(7).toString(StandardCharsets.US_ASCII);
+ String imei = buf.readSlice(15).toString(StandardCharsets.US_ASCII);
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (type.startsWith("LOGN")) {
+
+ ByteBuf content = Unpooled.copiedBuffer("1", StandardCharsets.US_ASCII);
+ try {
+ sendResponse(channel, "LGSA" + type.substring(4), imei, content);
+ } finally {
+ content.release();
+ }
+
+ } else if (type.startsWith("GPSL")) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDateReverse(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+
+ position.setValid(true);
+ position.setTime(dateBuilder.getDate());
+ position.setLatitude(decodeCoordinate(buf));
+ position.setLongitude(decodeCoordinate(buf));
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.setCourse(buf.readUnsignedShort());
+
+ decodeStatus(buf, position);
+
+ sendResponse(channel, "GPSA" + type.substring(4), imei, buf.readSlice(2));
+
+ return position;
+
+ } else if (type.startsWith("SYNC")) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ decodeStatus(buf, position);
+
+ sendResponse(channel, "SYSA" + type.substring(4), imei, null);
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gps103Protocol.java b/src/main/java/org/traccar/protocol/Gps103Protocol.java
new file mode 100644
index 000000000..6272a3fd1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gps103Protocol.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class Gps103Protocol extends BaseProtocol {
+
+ public Gps103Protocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_POSITION_PERIODIC,
+ Command.TYPE_POSITION_STOP,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_ALARM_ARM,
+ Command.TYPE_ALARM_DISARM,
+ Command.TYPE_REQUEST_PHOTO);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(2048, false, "\r\n", "\n", ";", "*"));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Gps103ProtocolEncoder());
+ pipeline.addLast(new Gps103ProtocolDecoder(Gps103Protocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Gps103ProtocolEncoder());
+ pipeline.addLast(new Gps103ProtocolDecoder(Gps103Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gps103ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gps103ProtocolDecoder.java
new file mode 100644
index 000000000..aa02e8ad4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gps103ProtocolDecoder.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DataConverter;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Gps103ProtocolDecoder extends BaseProtocolDecoder {
+
+ private int photoPackets = 0;
+ private ByteBuf photo;
+
+ public Gps103ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("imei:")
+ .number("(d+),") // imei
+ .expression("([^,]+),") // alarm
+ .groupBegin()
+ .number("(dd)/?(dd)/?(dd) ?") // local date (yymmdd)
+ .number("(dd):?(dd)(?:dd)?,") // local time (hhmmss)
+ .or()
+ .number("d*,")
+ .groupEnd()
+ .expression("([^,]+)?,") // rfid
+ .groupBegin()
+ .text("L,,,")
+ .number("(x+),,") // lac
+ .number("(x+),,,") // cid
+ .or()
+ .text("F,")
+ .groupBegin()
+ .number("(dd)(dd)(dd).d+") // time utc (hhmmss)
+ .or()
+ .number("(?:d{1,5}.d+)?")
+ .groupEnd()
+ .text(",")
+ .expression("([AV]),") // validity
+ .expression("([NS]),").optional()
+ .number("(d+)(dd.d+),") // latitude (ddmm.mmmm)
+ .expression("([NS]),").optional()
+ .expression("([EW]),").optional()
+ .number("(d+)(dd.d+),") // longitude (dddmm.mmmm)
+ .expression("([EW])?,").optional()
+ .number("(d+.?d*)?").optional() // speed
+ .number(",(d+.?d*)?").optional() // course
+ .number(",(d+.?d*)?").optional() // altitude
+ .number(",([01])?").optional() // ignition
+ .number(",([01])?").optional() // door
+ .groupBegin()
+ .number(",(?:(d+.d+)%)?") // fuel 1
+ .number(",(?:(d+.d+)%|d+)?") // fuel 2
+ .groupEnd("?")
+ .number(",([-+]?d+)?").optional() // temperature
+ .groupEnd()
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_OBD = new PatternBuilder()
+ .text("imei:")
+ .number("(d+),") // imei
+ .expression("OBD,") // type
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d+)?,") // odometer
+ .number("(d+.d+)?,") // fuel instant
+ .number("(d+.d+)?,") // fuel average
+ .number("(d+)?,") // hours
+ .number("(d+),") // speed
+ .number("(d+.?d*%),") // power load
+ .number("(?:([-+]?d+)|[-+]?),") // temperature
+ .number("(d+.?d*%),") // throttle
+ .number("(d+),") // rpm
+ .number("(d+.d+),") // battery
+ .number("([^;]*)") // dtcs
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_ALT = new PatternBuilder()
+ .text("imei:")
+ .number("(d+),") // imei
+ .expression("[^,]+,")
+ .expression("(?:-+|(.+)),") // event
+ .expression("(?:-+|(.+)),") // sensor id
+ .expression("(?:-+|(.+)),") // sensor voltage
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(d+),") // rssi
+ .number("(d),") // gps status
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(-?d+),") // altitude
+ .number("(d+.d+),") // hdop
+ .number("(d+),") // satellites
+ .number("([01]),") // ignition
+ .number("([01]),") // charge
+ .expression("(?:-+|(.+))") // error
+ .any()
+ .compile();
+
+ private String decodeAlarm(String value) {
+ if (value.startsWith("T:")) {
+ return Position.ALARM_TEMPERATURE;
+ } else if (value.startsWith("oil")) {
+ return Position.ALARM_FUEL_LEAK;
+ }
+ switch (value) {
+ case "tracker":
+ return null;
+ case "help me":
+ return Position.ALARM_SOS;
+ case "low battery":
+ return Position.ALARM_LOW_BATTERY;
+ case "stockade":
+ return Position.ALARM_GEOFENCE;
+ case "move":
+ return Position.ALARM_MOVEMENT;
+ case "speed":
+ return Position.ALARM_OVERSPEED;
+ case "acc on":
+ return Position.ALARM_POWER_ON;
+ case "acc off":
+ return Position.ALARM_POWER_OFF;
+ case "door alarm":
+ return Position.ALARM_DOOR;
+ case "ac alarm":
+ return Position.ALARM_POWER_CUT;
+ case "accident alarm":
+ return Position.ALARM_ACCIDENT;
+ case "sensor alarm":
+ return Position.ALARM_SHOCK;
+ case "bonnet alarm":
+ return Position.ALARM_BONNET;
+ case "footbrake alarm":
+ return Position.ALARM_FOOT_BRAKE;
+ case "DTC":
+ return Position.ALARM_FAULT;
+ default:
+ return null;
+ }
+ }
+
+ private Position decodeRegular(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String imei = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ String alarm = parser.next();
+ position.set(Position.KEY_ALARM, decodeAlarm(alarm));
+ if (alarm.equals("help me")) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("**,imei:" + imei + ",E;", remoteAddress));
+ }
+ } else if (alarm.startsWith("vt")) {
+ photoPackets = Integer.parseInt(alarm.substring(2));
+ photo = Unpooled.buffer();
+ } else if (alarm.equals("acc on")) {
+ position.set(Position.KEY_IGNITION, true);
+ } else if (alarm.equals("acc off")) {
+ position.set(Position.KEY_IGNITION, false);
+ } else if (alarm.startsWith("T:")) {
+ position.set(Position.PREFIX_TEMP + 1, Double.parseDouble(alarm.substring(2)));
+ } else if (alarm.startsWith("oil ")) {
+ position.set(Position.KEY_FUEL_LEVEL, Double.parseDouble(alarm.substring(4)));
+ } else if (!position.getAttributes().containsKey(Position.KEY_ALARM) && !alarm.equals("tracker")) {
+ position.set(Position.KEY_EVENT, alarm);
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ int localHours = parser.nextInt(0);
+ int localMinutes = parser.nextInt(0);
+
+ String rfid = parser.next();
+ if (alarm.equals("rfid")) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, rfid);
+ }
+
+ if (parser.hasNext(2)) {
+
+ getLastLocation(position, null);
+
+ position.setNetwork(new Network(CellTower.fromLacCid(parser.nextHexInt(0), parser.nextHexInt(0))));
+
+ } else {
+
+ String utcHours = parser.next();
+ String utcMinutes = parser.next();
+
+ dateBuilder.setTime(localHours, localMinutes, parser.nextInt(0));
+
+ // Timezone calculation
+ if (utcHours != null && utcMinutes != null) {
+ int deltaMinutes = (localHours - Integer.parseInt(utcHours)) * 60;
+ deltaMinutes += localMinutes - Integer.parseInt(utcMinutes);
+ if (deltaMinutes <= -12 * 60) {
+ deltaMinutes += 24 * 60;
+ } else if (deltaMinutes > 12 * 60) {
+ deltaMinutes -= 24 * 60;
+ }
+ dateBuilder.addMinute(-deltaMinutes);
+ }
+ position.setTime(dateBuilder.getDate());
+
+ position.setValid(parser.next().equals("A"));
+ position.setFixTime(position.getDeviceTime());
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN_HEM));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_IGNITION, parser.nextInt() == 1);
+ }
+ if (parser.hasNext()) {
+ position.set(Position.KEY_DOOR, parser.nextInt() == 1);
+ }
+ position.set("fuel1", parser.nextDouble());
+ position.set("fuel2", parser.nextDouble());
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt());
+
+ }
+
+ return position;
+ }
+
+ private Position decodeObd(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_OBD, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, parser.nextDateTime());
+
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+ parser.nextDouble(0); // instant fuel consumption
+ position.set(Position.KEY_FUEL_CONSUMPTION, parser.nextDouble(0));
+ if (parser.hasNext()) {
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromHours(parser.nextInt()));
+ }
+ position.set(Position.KEY_OBD_SPEED, parser.nextInt(0));
+ position.set(Position.KEY_ENGINE_LOAD, parser.next());
+ position.set(Position.KEY_COOLANT_TEMP, parser.nextInt());
+ position.set(Position.KEY_THROTTLE, parser.next());
+ position.set(Position.KEY_RPM, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0));
+ position.set(Position.KEY_DTCS, parser.next().replace(',', ' ').trim());
+
+ return position;
+ }
+
+
+ private Position decodeAlternative(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_ALT, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_EVENT, parser.next());
+ position.set("sensorId", parser.next());
+ position.set("sensorVoltage", parser.nextDouble());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ position.set(Position.KEY_RSSI, parser.nextInt());
+
+ position.setValid(parser.nextInt() > 0);
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt()));
+ position.setCourse(parser.nextInt());
+ position.setAltitude(parser.nextInt());
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_IGNITION, parser.nextInt() > 0);
+ position.set(Position.KEY_CHARGE, parser.nextInt() > 0);
+ position.set("error", parser.next());
+
+ return position;
+ }
+
+ private Position decodePhoto(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ String imei = sentence.substring(5, 5 + 15);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ ByteBuf buf = Unpooled.wrappedBuffer(DataConverter.parseHex(
+ sentence.substring(24, sentence.endsWith(";") ? sentence.length() - 1 : sentence.length())));
+ int index = buf.readUnsignedShortLE();
+ photo.writeBytes(buf, buf.readerIndex() + 2, buf.readableBytes() - 4);
+
+ if (index + 1 >= photoPackets) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ try {
+ position.set(Position.KEY_IMAGE, Context.getMediaManager().writeFile(imei, photo, "jpg"));
+ } finally {
+ photoPackets = 0;
+ photo.release();
+ photo = null;
+ }
+
+ return position;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.contains("imei:") && sentence.length() <= 30) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("LOAD", remoteAddress));
+ Matcher matcher = Pattern.compile("imei:(\\d+),").matcher(sentence);
+ if (matcher.find()) {
+ getDeviceSession(channel, remoteAddress, matcher.group(1));
+ }
+ }
+ return null;
+ }
+
+ if (!sentence.isEmpty() && Character.isDigit(sentence.charAt(0))) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("ON", remoteAddress));
+ }
+ int start = sentence.indexOf("imei:");
+ if (start >= 0) {
+ sentence = sentence.substring(start);
+ } else {
+ return null;
+ }
+ }
+
+ if (sentence.substring(21, 21 + 2).equals("vr")) {
+ return decodePhoto(channel, remoteAddress, sentence);
+ } else if (sentence.substring(21, 21 + 3).contains("OBD")) {
+ return decodeObd(channel, remoteAddress, sentence);
+ } else if (sentence.endsWith("*")) {
+ return decodeAlternative(channel, remoteAddress, sentence);
+ } else {
+ return decodeRegular(channel, remoteAddress, sentence);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gps103ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Gps103ProtocolEncoder.java
new file mode 100644
index 000000000..47ef2f333
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gps103ProtocolEncoder.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class Gps103ProtocolEncoder extends StringProtocolEncoder implements StringProtocolEncoder.ValueFormatter {
+
+ @Override
+ public String formatValue(String key, Object value) {
+
+ if (key.equals(Command.KEY_FREQUENCY)) {
+ long frequency = ((Number) value).longValue();
+ if (frequency / 60 / 60 > 0) {
+ return String.format("%02dh", frequency / 60 / 60);
+ } else if (frequency / 60 > 0) {
+ return String.format("%02dm", frequency / 60);
+ } else {
+ return String.format("%02ds", frequency);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatCommand(command, "**,imei:{%s},{%s}", Command.KEY_UNIQUE_ID, Command.KEY_DATA);
+ case Command.TYPE_POSITION_STOP:
+ return formatCommand(command, "**,imei:{%s},A", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_POSITION_SINGLE:
+ return formatCommand(command, "**,imei:{%s},B", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_POSITION_PERIODIC:
+ return formatCommand(
+ command, "**,imei:{%s},C,{%s}", this, Command.KEY_UNIQUE_ID, Command.KEY_FREQUENCY);
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, "**,imei:{%s},J", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, "**,imei:{%s},K", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ALARM_ARM:
+ return formatCommand(command, "**,imei:{%s},L", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ALARM_DISARM:
+ return formatCommand(command, "**,imei:{%s},M", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_REQUEST_PHOTO:
+ return formatCommand(command, "**,imei:{%s},160", Command.KEY_UNIQUE_ID);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GpsGateProtocol.java b/src/main/java/org/traccar/protocol/GpsGateProtocol.java
new file mode 100644
index 000000000..a131b6f48
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GpsGateProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GpsGateProtocol extends BaseProtocol {
+
+ public GpsGateProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "\0", "\n", "\r\n"));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new GpsGateProtocolDecoder(GpsGateProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GpsGateProtocolDecoder.java b/src/main/java/org/traccar/protocol/GpsGateProtocolDecoder.java
new file mode 100644
index 000000000..cc187225b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GpsGateProtocolDecoder.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class GpsGateProtocolDecoder extends BaseProtocolDecoder {
+
+ public GpsGateProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_GPRMC = new PatternBuilder()
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+)?,") // speed
+ .number("(d+.d+)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_FRCMD = new PatternBuilder()
+ .text("$FRCMD,")
+ .number("(d+),") // imei
+ .expression("[^,]*,") // command
+ .expression("[^,]*,")
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*),") // altitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([01])") // validity
+ .any()
+ .compile();
+
+ private void send(Channel channel, SocketAddress remoteAddress, String message) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(message + Checksum.nmea(message) + "\r\n", remoteAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("$FRLIN,")) {
+
+ int beginIndex = sentence.indexOf(',', 7);
+ if (beginIndex != -1) {
+ beginIndex += 1;
+ int endIndex = sentence.indexOf(',', beginIndex);
+ if (endIndex != -1) {
+ String imei = sentence.substring(beginIndex, endIndex);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession != null) {
+ if (channel != null) {
+ send(channel, remoteAddress, "$FRSES," + channel.id().asShortText());
+ }
+ } else {
+ send(channel, remoteAddress, "$FRERR,AuthError,Unknown device");
+ }
+ } else {
+ send(channel, remoteAddress, "$FRERR,AuthError,Parse error");
+ }
+ } else {
+ send(channel, remoteAddress, "$FRERR,AuthError,Parse error");
+ }
+
+ } else if (sentence.startsWith("$FRVER,")) {
+
+ send(channel, remoteAddress, "$FRVER,1,0,GpsGate Server 1.0");
+
+ } else if (sentence.startsWith("$GPRMC,")) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN_GPRMC, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+
+ } else if (sentence.startsWith("$FRCMD,")) {
+
+ Parser parser = new Parser(PATTERN_FRCMD, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setAltitude(parser.nextDouble(0));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setValid(parser.next().equals("1"));
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GpsMarkerProtocol.java b/src/main/java/org/traccar/protocol/GpsMarkerProtocol.java
new file mode 100644
index 000000000..ad23ece48
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GpsMarkerProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GpsMarkerProtocol extends BaseProtocol {
+
+ public GpsMarkerProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "\r"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new GpsMarkerProtocolDecoder(GpsMarkerProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GpsMarkerProtocolDecoder.java b/src/main/java/org/traccar/protocol/GpsMarkerProtocolDecoder.java
new file mode 100644
index 000000000..bbb2c31e2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GpsMarkerProtocolDecoder.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class GpsMarkerProtocolDecoder extends BaseProtocolDecoder {
+
+ public GpsMarkerProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$GM")
+ .number("d") // type
+ .number("(?:xx)?") // index
+ .number("(d{15})") // imei
+ .number("T(dd)(dd)(dd)") // date (ddmmyy)
+ .number("(dd)(dd)(dd)?") // time (hhmmss)
+ .expression("([NS])")
+ .number("(dd)(dd)(dddd)") // latitude
+ .expression("([EW])")
+ .number("(ddd)(dd)(dddd)") // longitude
+ .number("(ddd)") // speed
+ .number("(ddd)") // course
+ .number("(x)") // satellites
+ .number("(dd)") // battery
+ .number("(d)") // input
+ .number("(d)") // output
+ .number("(ddd)") // temperature
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN_MIN));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextHexInt(0));
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt(0));
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+ position.set(Position.PREFIX_TEMP + 1, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GpsmtaProtocol.java b/src/main/java/org/traccar/protocol/GpsmtaProtocol.java
new file mode 100644
index 000000000..ce6cc5929
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GpsmtaProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class GpsmtaProtocol extends BaseProtocol {
+
+ public GpsmtaProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new GpsmtaProtocolDecoder(GpsmtaProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GpsmtaProtocolDecoder.java b/src/main/java/org/traccar/protocol/GpsmtaProtocolDecoder.java
new file mode 100644
index 000000000..31f9401b4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GpsmtaProtocolDecoder.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class GpsmtaProtocolDecoder extends BaseProtocolDecoder {
+
+ public GpsmtaProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("([^ ]+) ") // uid
+ .number("(d+) ") // time (unix time)
+ .number("(-?d+.d+) ") // latitude
+ .number("(-?d+.d+) ") // longitude
+ .number("(d+) ") // speed
+ .number("(d+) ") // course
+ .number("(d+) ") // accuracy
+ .number("(d+) ") // altitude
+ .number("(d+) ") // flags
+ .number("(d+) ") // battery
+ .number("(d+) ") // temperature
+ .number("(d)") // charging status
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ String time = parser.next();
+ position.setTime(new Date(Long.parseLong(time) * 1000));
+
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setSpeed(parser.nextInt());
+ position.setCourse(parser.nextInt());
+ position.setAccuracy(parser.nextInt());
+ position.setAltitude(parser.nextInt());
+
+ position.set(Position.KEY_STATUS, parser.nextInt());
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt());
+ position.set(Position.KEY_CHARGE, parser.nextInt() == 1);
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(time, remoteAddress));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GranitFrameDecoder.java b/src/main/java/org/traccar/protocol/GranitFrameDecoder.java
new file mode 100644
index 000000000..bb7f4be44
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GranitFrameDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+import org.traccar.helper.BufferUtil;
+
+public class GranitFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int indexEnd = BufferUtil.indexOf("\r\n", buf);
+ if (indexEnd != -1) {
+ int indexTilde = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '~');
+ if (indexTilde != -1 && indexTilde < indexEnd) {
+ int length = buf.getUnsignedShortLE(indexTilde + 1);
+ indexEnd = BufferUtil.indexOf("\r\n", buf, indexTilde + 2 + length, buf.writerIndex());
+ if (indexEnd == -1) {
+ return null;
+ }
+ }
+ ByteBuf frame = buf.readRetainedSlice(indexEnd - buf.readerIndex());
+ buf.skipBytes(2);
+ return frame;
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GranitProtocol.java b/src/main/java/org/traccar/protocol/GranitProtocol.java
new file mode 100644
index 000000000..6785f2a2e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GranitProtocol.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class GranitProtocol extends BaseProtocol {
+
+ public GranitProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_IDENTIFICATION,
+ Command.TYPE_REBOOT_DEVICE,
+ Command.TYPE_POSITION_SINGLE);
+ setTextCommandEncoder(new GranitProtocolSmsEncoder());
+ setSupportedTextCommands(
+ Command.TYPE_REBOOT_DEVICE,
+ Command.TYPE_POSITION_PERIODIC);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new GranitFrameDecoder());
+ pipeline.addLast(new GranitProtocolEncoder());
+ pipeline.addLast(new GranitProtocolDecoder(GranitProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GranitProtocolDecoder.java b/src/main/java/org/traccar/protocol/GranitProtocolDecoder.java
new file mode 100644
index 000000000..8900e5b39
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GranitProtocolDecoder.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class GranitProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final int HEADER_LENGTH = 6;
+
+ private double adc1Ratio;
+ private double adc2Ratio;
+ private double adc3Ratio;
+ private double adc4Ratio;
+
+ public GranitProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ adc1Ratio = Context.getConfig().getDouble("granit.adc1Ratio", 1);
+ adc2Ratio = Context.getConfig().getDouble("granit.adc2Ratio", 1);
+ adc3Ratio = Context.getConfig().getDouble("granit.adc3Ratio", 1);
+ adc4Ratio = Context.getConfig().getDouble("granit.adc4Ratio", 1);
+ }
+
+ public static void appendChecksum(ByteBuf buffer, int length) {
+ buffer.writeByte('*');
+ int checksum = Checksum.xor(buffer.nioBuffer(0, length)) & 0xFF;
+ String checksumString = String.format("%02X", checksum);
+ buffer.writeBytes(checksumString.getBytes(StandardCharsets.US_ASCII));
+ buffer.writeByte('\r'); buffer.writeByte('\n');
+ }
+
+ private static void sendResponseCurrent(Channel channel, int deviceId, long time) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeBytes("BB+UGRC~".getBytes(StandardCharsets.US_ASCII));
+ response.writeShortLE(6); // length
+ response.writeInt((int) time);
+ response.writeShortLE(deviceId);
+ appendChecksum(response, 16);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+
+ private static void sendResponseArchive(Channel channel, int deviceId, int packNum) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeBytes("BB+ARCF~".getBytes(StandardCharsets.US_ASCII));
+ response.writeShortLE(4); // length
+ response.writeShortLE(packNum);
+ response.writeShortLE(deviceId);
+ appendChecksum(response, 14);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+
+ private void decodeStructure(ByteBuf buf, Position position) {
+ short flags = buf.readUnsignedByte();
+ position.setValid(BitUtil.check(flags, 7));
+ if (BitUtil.check(flags, 1)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+
+ short satDel = buf.readUnsignedByte();
+ position.set(Position.KEY_SATELLITES, BitUtil.from(satDel, 4));
+
+ int pdop = BitUtil.to(satDel, 4);
+ position.set(Position.KEY_PDOP, pdop);
+
+ int lonDegrees = buf.readUnsignedByte();
+ int latDegrees = buf.readUnsignedByte();
+ int lonMinutes = buf.readUnsignedShortLE();
+ int latMinutes = buf.readUnsignedShortLE();
+
+ double latitude = latDegrees + latMinutes / 60000.0;
+ double longitude = lonDegrees + lonMinutes / 60000.0;
+
+ if (position.getValid()) {
+ if (!BitUtil.check(flags, 4)) {
+ latitude = -latitude;
+ }
+ if (!BitUtil.check(flags, 5)) {
+ longitude = -longitude;
+ }
+ }
+
+ position.setLongitude(longitude);
+ position.setLatitude(latitude);
+
+ position.setSpeed(buf.readUnsignedByte());
+
+ int course = buf.readUnsignedByte();
+ if (BitUtil.check(flags, 6)) {
+ course = course | 0x100;
+ }
+ position.setCourse(course);
+
+ position.set(Position.KEY_DISTANCE, buf.readShortLE());
+
+ int analogIn1 = buf.readUnsignedByte();
+ int analogIn2 = buf.readUnsignedByte();
+ int analogIn3 = buf.readUnsignedByte();
+ int analogIn4 = buf.readUnsignedByte();
+
+ int analogInHi = buf.readUnsignedByte();
+
+ analogIn1 = analogInHi << 8 & 0x300 | analogIn1;
+ analogIn2 = analogInHi << 6 & 0x300 | analogIn2;
+ analogIn3 = analogInHi << 4 & 0x300 | analogIn3;
+ analogIn4 = analogInHi << 2 & 0x300 | analogIn4;
+
+ position.set(Position.PREFIX_ADC + 1, analogIn1 * adc1Ratio);
+ position.set(Position.PREFIX_ADC + 2, analogIn2 * adc2Ratio);
+ position.set(Position.PREFIX_ADC + 3, analogIn3 * adc3Ratio);
+ position.set(Position.PREFIX_ADC + 4, analogIn4 * adc4Ratio);
+
+ position.setAltitude(buf.readUnsignedByte() * 10);
+
+ int output = buf.readUnsignedByte();
+ for (int i = 0; i < 8; i++) {
+ position.set(Position.PREFIX_IO + (i + 1), BitUtil.check(output, i));
+ }
+ buf.readUnsignedByte(); // status message buffer
+ }
+
+ @Override
+ protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int indexTilde = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '~');
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+
+ if (deviceSession != null && indexTilde == -1) {
+ String bufString = buf.toString(StandardCharsets.US_ASCII);
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date());
+ getLastLocation(position, new Date());
+ position.setValid(false);
+ position.set(Position.KEY_RESULT, bufString);
+ return position;
+ }
+
+ if (buf.readableBytes() < HEADER_LENGTH) {
+ return null;
+ }
+ String header = buf.readSlice(HEADER_LENGTH).toString(StandardCharsets.US_ASCII);
+
+ if (header.equals("+RRCB~")) {
+
+ buf.skipBytes(2); // binary length 26
+ int deviceId = buf.readUnsignedShortLE();
+ deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(deviceId));
+ if (deviceSession == null) {
+ return null;
+ }
+ long unixTime = buf.readUnsignedIntLE();
+ if (channel != null) {
+ sendResponseCurrent(channel, deviceId, unixTime);
+ }
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(unixTime * 1000));
+
+ decodeStructure(buf, position);
+ return position;
+
+ } else if (header.equals("+DDAT~")) {
+
+ buf.skipBytes(2); // binary length
+ int deviceId = buf.readUnsignedShortLE();
+ deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(deviceId));
+ if (deviceSession == null) {
+ return null;
+ }
+ byte format = buf.readByte();
+ if (format != 4) {
+ return null;
+ }
+ byte nblocks = buf.readByte();
+ int packNum = buf.readUnsignedShortLE();
+ if (channel != null) {
+ sendResponseArchive(channel, deviceId, packNum);
+ }
+ List<Position> positions = new ArrayList<>();
+ while (nblocks > 0) {
+ nblocks--;
+ long unixTime = buf.readUnsignedIntLE();
+ int timeIncrement = buf.getUnsignedShortLE(buf.readerIndex() + 120);
+ for (int i = 0; i < 6; i++) {
+ if (buf.getUnsignedByte(buf.readerIndex()) != 0xFE) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setTime(new Date((unixTime + i * timeIncrement) * 1000));
+ decodeStructure(buf, position);
+ position.set(Position.KEY_ARCHIVE, true);
+ positions.add(position);
+ } else {
+ buf.skipBytes(20); // skip filled 0xFE structure
+ }
+ }
+ buf.skipBytes(2); // increment
+ }
+ return positions;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GranitProtocolEncoder.java b/src/main/java/org/traccar/protocol/GranitProtocolEncoder.java
new file mode 100644
index 000000000..6345ff971
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GranitProtocolEncoder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import java.nio.charset.StandardCharsets;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.model.Command;
+
+public class GranitProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeCommand(String commandString) {
+ ByteBuf buffer = Unpooled.buffer();
+ buffer.writeBytes(commandString.getBytes(StandardCharsets.US_ASCII));
+ GranitProtocolDecoder.appendChecksum(buffer, commandString.length());
+ return buffer;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+ switch (command.getType()) {
+ case Command.TYPE_IDENTIFICATION:
+ return encodeCommand("BB+IDNT");
+ case Command.TYPE_REBOOT_DEVICE:
+ return encodeCommand("BB+RESET");
+ case Command.TYPE_POSITION_SINGLE:
+ return encodeCommand("BB+RRCD");
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/GranitProtocolSmsEncoder.java b/src/main/java/org/traccar/protocol/GranitProtocolSmsEncoder.java
new file mode 100644
index 000000000..7d5518c17
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/GranitProtocolSmsEncoder.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class GranitProtocolSmsEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected String encodeCommand(Command command) {
+ switch (command.getType()) {
+ case Command.TYPE_REBOOT_DEVICE:
+ return "BB+RESET";
+ case Command.TYPE_POSITION_PERIODIC:
+ return formatCommand(command, "BB+BBMD={%s}", Command.KEY_FREQUENCY);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gt02Protocol.java b/src/main/java/org/traccar/protocol/Gt02Protocol.java
new file mode 100644
index 000000000..f412ee720
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gt02Protocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Gt02Protocol extends BaseProtocol {
+
+ public Gt02Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(256, 2, 1, 2, 0));
+ pipeline.addLast(new Gt02ProtocolDecoder(Gt02Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gt02ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gt02ProtocolDecoder.java
new file mode 100644
index 000000000..78a3fd3ee
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gt02ProtocolDecoder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+
+public class Gt02ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Gt02ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_DATA = 0x10;
+ public static final int MSG_HEARTBEAT = 0x1A;
+ public static final int MSG_RESPONSE = 0x1C;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ buf.readByte(); // size
+
+ Position position = new Position(getProtocolName());
+
+ // Zero for location messages
+ int power = buf.readUnsignedByte();
+ int gsm = buf.readUnsignedByte();
+
+ String imei = ByteBufUtil.hexDump(buf.readSlice(8)).substring(1);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_INDEX, buf.readUnsignedShort());
+
+ int type = buf.readUnsignedByte();
+
+ if (type == MSG_HEARTBEAT) {
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_POWER, power);
+ position.set(Position.KEY_RSSI, gsm);
+
+ if (channel != null) {
+ byte[] response = {0x54, 0x68, 0x1A, 0x0D, 0x0A};
+ channel.writeAndFlush(new NetworkMessage(Unpooled.wrappedBuffer(response), remoteAddress));
+ }
+
+ } else if (type == MSG_DATA) {
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ double latitude = buf.readUnsignedInt() / (60.0 * 30000.0);
+ double longitude = buf.readUnsignedInt() / (60.0 * 30000.0);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.setCourse(buf.readUnsignedShort());
+
+ buf.skipBytes(3); // reserved
+
+ long flags = buf.readUnsignedInt();
+ position.setValid(BitUtil.check(flags, 0));
+ if (!BitUtil.check(flags, 1)) {
+ latitude = -latitude;
+ }
+ if (!BitUtil.check(flags, 2)) {
+ longitude = -longitude;
+ }
+
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+
+ } else if (type == MSG_RESPONSE) {
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_RESULT,
+ buf.readSlice(buf.readUnsignedByte()).toString(StandardCharsets.US_ASCII));
+
+ } else {
+
+ return null;
+
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gt06FrameDecoder.java b/src/main/java/org/traccar/protocol/Gt06FrameDecoder.java
new file mode 100644
index 000000000..cc934be42
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gt06FrameDecoder.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class Gt06FrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 5) {
+ return null;
+ }
+
+ int length = 2 + 2; // head and tail
+
+ if (buf.getByte(buf.readerIndex()) == 0x78) {
+ length += 1 + buf.getUnsignedByte(buf.readerIndex() + 2);
+ } else {
+ length += 2 + buf.getUnsignedShort(buf.readerIndex() + 2);
+ }
+
+ if (buf.readableBytes() >= length && buf.getUnsignedShort(buf.readerIndex() + length - 2) == 0x0d0a) {
+ return buf.readRetainedSlice(length);
+ }
+
+ int endIndex = buf.readerIndex() - 1;
+ do {
+ endIndex = buf.indexOf(endIndex + 1, buf.writerIndex(), (byte) 0x0d);
+ if (endIndex > 0 && buf.writerIndex() > endIndex + 1 && buf.getByte(endIndex + 1) == 0x0a) {
+ return buf.readRetainedSlice(endIndex + 2 - buf.readerIndex());
+ }
+ } while (endIndex > 0);
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gt06Protocol.java b/src/main/java/org/traccar/protocol/Gt06Protocol.java
new file mode 100644
index 000000000..6e5435cd4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gt06Protocol.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class Gt06Protocol extends BaseProtocol {
+
+ public Gt06Protocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_CUSTOM);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Gt06FrameDecoder());
+ pipeline.addLast(new Gt06ProtocolEncoder());
+ pipeline.addLast(new Gt06ProtocolDecoder(Gt06Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java
new file mode 100644
index 000000000..1f8fb66dd
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java
@@ -0,0 +1,929 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BcdUtil;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Device;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+import org.traccar.model.WifiAccessPoint;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+public class Gt06ProtocolDecoder extends BaseProtocolDecoder {
+
+ private final Map<Integer, ByteBuf> photos = new HashMap<>();
+
+ public Gt06ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_LOGIN = 0x01;
+ public static final int MSG_GPS = 0x10;
+ public static final int MSG_LBS = 0x11;
+ public static final int MSG_GPS_LBS_1 = 0x12;
+ public static final int MSG_GPS_LBS_2 = 0x22;
+ public static final int MSG_STATUS = 0x13;
+ public static final int MSG_SATELLITE = 0x14;
+ public static final int MSG_STRING = 0x15;
+ public static final int MSG_GPS_LBS_STATUS_1 = 0x16;
+ public static final int MSG_WIFI = 0x17;
+ public static final int MSG_GPS_LBS_STATUS_2 = 0x26;
+ public static final int MSG_GPS_LBS_STATUS_3 = 0x27;
+ public static final int MSG_LBS_MULTIPLE = 0x28;
+ public static final int MSG_LBS_WIFI = 0x2C;
+ public static final int MSG_LBS_EXTEND = 0x18;
+ public static final int MSG_LBS_STATUS = 0x19;
+ public static final int MSG_GPS_PHONE = 0x1A;
+ public static final int MSG_GPS_LBS_EXTEND = 0x1E;
+ public static final int MSG_HEARTBEAT = 0x23;
+ public static final int MSG_ADDRESS_REQUEST = 0x2A;
+ public static final int MSG_ADDRESS_RESPONSE = 0x97;
+ public static final int MSG_AZ735_GPS = 0x32;
+ public static final int MSG_AZ735_ALARM = 0x33;
+ public static final int MSG_X1_GPS = 0x34;
+ public static final int MSG_X1_PHOTO_INFO = 0x35;
+ public static final int MSG_X1_PHOTO_DATA = 0x36;
+ public static final int MSG_WIFI_2 = 0x69;
+ public static final int MSG_COMMAND_0 = 0x80;
+ public static final int MSG_COMMAND_1 = 0x81;
+ public static final int MSG_COMMAND_2 = 0x82;
+ public static final int MSG_TIME_REQUEST = 0x8A;
+ public static final int MSG_INFO = 0x94;
+ public static final int MSG_STRING_INFO = 0x21;
+ public static final int MSG_GPS_2 = 0xA0;
+ public static final int MSG_LBS_2 = 0xA1;
+ public static final int MSG_WIFI_3 = 0xA2;
+ public static final int MSG_FENCE_SINGLE = 0xA3;
+ public static final int MSG_FENCE_MULTI = 0xA4;
+ public static final int MSG_LBS_ALARM = 0xA5;
+ public static final int MSG_LBS_ADDRESS = 0xA7;
+ public static final int MSG_OBD = 0x8C;
+ public static final int MSG_DTC = 0x65;
+ public static final int MSG_PID = 0x66;
+
+ private static boolean isSupported(int type) {
+ return hasGps(type) || hasLbs(type) || hasStatus(type);
+ }
+
+ private static boolean hasGps(int type) {
+ switch (type) {
+ case MSG_GPS:
+ case MSG_GPS_LBS_1:
+ case MSG_GPS_LBS_2:
+ case MSG_GPS_LBS_STATUS_1:
+ case MSG_GPS_LBS_STATUS_2:
+ case MSG_GPS_LBS_STATUS_3:
+ case MSG_GPS_PHONE:
+ case MSG_GPS_LBS_EXTEND:
+ case MSG_GPS_2:
+ case MSG_FENCE_SINGLE:
+ case MSG_FENCE_MULTI:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static boolean hasLbs(int type) {
+ switch (type) {
+ case MSG_LBS:
+ case MSG_LBS_STATUS:
+ case MSG_GPS_LBS_1:
+ case MSG_GPS_LBS_2:
+ case MSG_GPS_LBS_STATUS_1:
+ case MSG_GPS_LBS_STATUS_2:
+ case MSG_GPS_LBS_STATUS_3:
+ case MSG_GPS_2:
+ case MSG_FENCE_SINGLE:
+ case MSG_FENCE_MULTI:
+ case MSG_LBS_ALARM:
+ case MSG_LBS_ADDRESS:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static boolean hasStatus(int type) {
+ switch (type) {
+ case MSG_STATUS:
+ case MSG_LBS_STATUS:
+ case MSG_GPS_LBS_STATUS_1:
+ case MSG_GPS_LBS_STATUS_2:
+ case MSG_GPS_LBS_STATUS_3:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static boolean hasLanguage(int type) {
+ switch (type) {
+ case MSG_GPS_PHONE:
+ case MSG_HEARTBEAT:
+ case MSG_GPS_LBS_STATUS_3:
+ case MSG_LBS_MULTIPLE:
+ case MSG_LBS_2:
+ case MSG_FENCE_MULTI:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void sendResponse(Channel channel, boolean extended, int type, int index, ByteBuf content) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ int length = 5 + (content != null ? content.readableBytes() : 0);
+ if (extended) {
+ response.writeShort(0x7979);
+ response.writeShort(length);
+ } else {
+ response.writeShort(0x7878);
+ response.writeByte(length);
+ }
+ response.writeByte(type);
+ if (content != null) {
+ response.writeBytes(content);
+ content.release();
+ }
+ response.writeShort(index);
+ response.writeShort(Checksum.crc16(Checksum.CRC16_X25,
+ response.nioBuffer(2, response.writerIndex() - 2)));
+ response.writeByte('\r'); response.writeByte('\n'); // ending
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ private void sendPhotoRequest(Channel channel, int pictureId) {
+ ByteBuf photo = photos.get(pictureId);
+ ByteBuf content = Unpooled.buffer();
+ content.writeInt(pictureId);
+ content.writeInt(photo.writerIndex());
+ content.writeShort(Math.min(photo.writableBytes(), 1024));
+ sendResponse(channel, false, MSG_X1_PHOTO_DATA, 0, content);
+ }
+
+ private boolean decodeGps(Position position, ByteBuf buf, boolean hasLength, TimeZone timezone) {
+
+ DateBuilder dateBuilder = new DateBuilder(timezone)
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ if (hasLength && buf.readUnsignedByte() == 0) {
+ return false;
+ }
+
+ position.set(Position.KEY_SATELLITES, BitUtil.to(buf.readUnsignedByte(), 4));
+
+ double latitude = buf.readUnsignedInt() / 60.0 / 30000.0;
+ double longitude = buf.readUnsignedInt() / 60.0 / 30000.0;
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+
+ int flags = buf.readUnsignedShort();
+ position.setCourse(BitUtil.to(flags, 10));
+ position.setValid(BitUtil.check(flags, 12));
+
+ if (!BitUtil.check(flags, 10)) {
+ latitude = -latitude;
+ }
+ if (BitUtil.check(flags, 11)) {
+ longitude = -longitude;
+ }
+
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+
+ if (BitUtil.check(flags, 14)) {
+ position.set(Position.KEY_IGNITION, BitUtil.check(flags, 15));
+ }
+
+ return true;
+ }
+
+ private boolean decodeLbs(Position position, ByteBuf buf, boolean hasLength) {
+
+ int length = 0;
+ if (hasLength) {
+ length = buf.readUnsignedByte();
+ if (length == 0) {
+ return false;
+ }
+ }
+
+ int mcc = buf.readUnsignedShort();
+ int mnc = BitUtil.check(mcc, 15) ? buf.readUnsignedShort() : buf.readUnsignedByte();
+
+ position.setNetwork(new Network(CellTower.from(
+ BitUtil.to(mcc, 15), mnc, buf.readUnsignedShort(), buf.readUnsignedMedium())));
+
+ if (length > 9) {
+ buf.skipBytes(length - 9);
+ }
+
+ return true;
+ }
+
+ private boolean decodeStatus(Position position, ByteBuf buf) {
+
+ int status = buf.readUnsignedByte();
+
+ position.set(Position.KEY_STATUS, status);
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 1));
+ position.set(Position.KEY_CHARGE, BitUtil.check(status, 2));
+ position.set(Position.KEY_BLOCKED, BitUtil.check(status, 7));
+
+ switch (BitUtil.between(status, 3, 6)) {
+ case 1:
+ position.set(Position.KEY_ALARM, Position.ALARM_SHOCK);
+ break;
+ case 2:
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_CUT);
+ break;
+ case 3:
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ case 4:
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ break;
+ case 7:
+ position.set(Position.KEY_ALARM, Position.ALARM_REMOVING);
+ break;
+ default:
+ break;
+ }
+
+ position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte() * 100 / 6);
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte()));
+
+ return true;
+ }
+
+ private String decodeAlarm(short value) {
+ switch (value) {
+ case 0x01:
+ return Position.ALARM_SOS;
+ case 0x02:
+ return Position.ALARM_POWER_CUT;
+ case 0x03:
+ case 0x09:
+ return Position.ALARM_VIBRATION;
+ case 0x04:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 0x05:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 0x06:
+ return Position.ALARM_OVERSPEED;
+ case 0x0E:
+ case 0x0F:
+ return Position.ALARM_LOW_BATTERY;
+ case 0x11:
+ return Position.ALARM_POWER_OFF;
+ case 0x13:
+ return Position.ALARM_TAMPERING;
+ case 0x14:
+ return Position.ALARM_DOOR;
+ case 0x29:
+ return Position.ALARM_ACCELERATION;
+ case 0x30:
+ return Position.ALARM_BRAKING;
+ case 0x2A:
+ case 0x2B:
+ return Position.ALARM_CORNERING;
+ case 0x2C:
+ return Position.ALARM_ACCIDENT;
+ case 0x23:
+ return Position.ALARM_FALL_DOWN;
+ default:
+ return null;
+ }
+ }
+
+ private static final Pattern PATTERN_FUEL = new PatternBuilder()
+ .text("!AIOIL,")
+ .number("d+,") // device address
+ .number("d+.d+,") // output value
+ .number("(d+.d+),") // temperature
+ .expression("[^,]+,") // version
+ .number("dd") // back wave
+ .number("d") // software status code
+ .number("d,") // hardware status code
+ .number("(d+.d+),") // measured value
+ .expression("[01],") // movement status
+ .number("d+,") // excited wave times
+ .number("xx") // checksum
+ .compile();
+
+ private Position decodeFuelData(Position position, String sentence) {
+ Parser parser = new Parser(PATTERN_FUEL, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ position.set(Position.PREFIX_TEMP + 1, parser.nextDouble(0));
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextDouble(0));
+
+ return position;
+ }
+
+ private static final Pattern PATTERN_LOCATION = new PatternBuilder()
+ .text("Current position!")
+ .number("Lat:([NS])(d+.d+),") // latitude
+ .number("Lon:([EW])(d+.d+),") // longitude
+ .text("Course:").number("(d+.d+),") // course
+ .text("Speed:").number("(d+.d+),") // speed
+ .text("DateTime:")
+ .number("(dddd)-(dd)-(dd) +") // date
+ .number("(dd):(dd):(dd)") // time
+ .compile();
+
+ private Position decodeLocationString(Position position, String sentence) {
+ Parser parser = new Parser(PATTERN_LOCATION, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setCourse(parser.nextDouble());
+ position.setSpeed(parser.nextDouble());
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.YMD_HMS));
+
+ return position;
+ }
+
+ private Object decodeBasic(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ int length = buf.readUnsignedByte();
+ int dataLength = length - 5;
+ int type = buf.readUnsignedByte();
+
+ DeviceSession deviceSession = null;
+ if (type != MSG_LOGIN) {
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+ if (deviceSession.getTimeZone() == null) {
+ deviceSession.setTimeZone(getTimeZone(deviceSession.getDeviceId()));
+ }
+ }
+
+ if (type == MSG_LOGIN) {
+
+ String imei = ByteBufUtil.hexDump(buf.readSlice(8)).substring(1);
+ buf.readUnsignedShort(); // type
+
+ deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession != null && deviceSession.getTimeZone() == null) {
+ deviceSession.setTimeZone(getTimeZone(deviceSession.getDeviceId()));
+ }
+
+ if (dataLength > 10) {
+ int extensionBits = buf.readUnsignedShort();
+ int hours = (extensionBits >> 4) / 100;
+ int minutes = (extensionBits >> 4) % 100;
+ int offset = (hours * 60 + minutes) * 60;
+ if ((extensionBits & 0x8) != 0) {
+ offset = -offset;
+ }
+ if (deviceSession != null) {
+ TimeZone timeZone = deviceSession.getTimeZone();
+ if (timeZone.getRawOffset() == 0) {
+ timeZone.setRawOffset(offset * 1000);
+ deviceSession.setTimeZone(timeZone);
+ }
+ }
+
+ }
+
+ if (deviceSession != null) {
+ sendResponse(channel, false, type, buf.getShort(buf.writerIndex() - 6), null);
+ }
+
+ } else if (type == MSG_HEARTBEAT) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ int status = buf.readUnsignedByte();
+ position.set(Position.KEY_ARMED, BitUtil.check(status, 0));
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 1));
+ position.set(Position.KEY_CHARGE, BitUtil.check(status, 2));
+
+ if (buf.readableBytes() >= 2 + 6) {
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.01);
+ }
+ if (buf.readableBytes() >= 1 + 6) {
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ }
+
+ sendResponse(channel, false, type, buf.getShort(buf.writerIndex() - 6), null);
+
+ return position;
+
+ } else if (type == MSG_ADDRESS_REQUEST) {
+
+ String response = "NA&&NA&&0##";
+ ByteBuf content = Unpooled.buffer();
+ content.writeByte(response.length());
+ content.writeInt(0);
+ content.writeBytes(response.getBytes(StandardCharsets.US_ASCII));
+ sendResponse(channel, true, MSG_ADDRESS_RESPONSE, 0, content);
+
+ } else if (type == MSG_TIME_REQUEST) {
+
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ ByteBuf content = Unpooled.buffer();
+ content.writeByte(calendar.get(Calendar.YEAR) - 2000);
+ content.writeByte(calendar.get(Calendar.MONTH) + 1);
+ content.writeByte(calendar.get(Calendar.DAY_OF_MONTH));
+ content.writeByte(calendar.get(Calendar.HOUR_OF_DAY));
+ content.writeByte(calendar.get(Calendar.MINUTE));
+ content.writeByte(calendar.get(Calendar.SECOND));
+ sendResponse(channel, false, MSG_TIME_REQUEST, 0, content);
+
+ } else if (type == MSG_X1_GPS || type == MSG_X1_PHOTO_INFO) {
+
+ return decodeX1(channel, buf, deviceSession, type);
+
+ } else if (type == MSG_WIFI || type == MSG_WIFI_2) {
+
+ return decodeWifi(channel, buf, deviceSession, type);
+
+ } else if (type == MSG_INFO) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_POWER, buf.readShort() * 0.01);
+
+ return position;
+
+ } else {
+
+ return decodeBasicOther(channel, buf, deviceSession, type, dataLength);
+
+ }
+
+ return null;
+ }
+
+ private Object decodeX1(Channel channel, ByteBuf buf, DeviceSession deviceSession, int type) {
+
+ if (type == MSG_X1_GPS) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedInt(); // data and alarm
+
+ decodeGps(position, buf, false, deviceSession.getTimeZone());
+
+ buf.readUnsignedShort(); // terminal info
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedByte(),
+ buf.readUnsignedShort(), buf.readUnsignedInt())));
+
+ long driverId = buf.readUnsignedInt();
+ if (driverId > 0) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, String.valueOf(driverId));
+ }
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.01);
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.01);
+
+ return position;
+
+ } else if (type == MSG_X1_PHOTO_INFO) {
+
+ buf.skipBytes(6); // time
+ buf.readUnsignedByte(); // fix status
+ buf.readUnsignedInt(); // latitude
+ buf.readUnsignedInt(); // longitude
+ buf.readUnsignedByte(); // camera id
+ buf.readUnsignedByte(); // photo source
+ buf.readUnsignedByte(); // picture format
+
+ ByteBuf photo = Unpooled.buffer(buf.readInt());
+ int pictureId = buf.readInt();
+ photos.put(pictureId, photo);
+ sendPhotoRequest(channel, pictureId);
+
+ }
+
+ return null;
+ }
+
+ private Object decodeWifi(Channel channel, ByteBuf buf, DeviceSession deviceSession, int type) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ ByteBuf time = buf.readSlice(6);
+ DateBuilder dateBuilder = new DateBuilder()
+ .setYear(BcdUtil.readInteger(time, 2))
+ .setMonth(BcdUtil.readInteger(time, 2))
+ .setDay(BcdUtil.readInteger(time, 2))
+ .setHour(BcdUtil.readInteger(time, 2))
+ .setMinute(BcdUtil.readInteger(time, 2))
+ .setSecond(BcdUtil.readInteger(time, 2));
+ getLastLocation(position, dateBuilder.getDate());
+
+ Network network = new Network();
+
+ int wifiCount = buf.getByte(2);
+ for (int i = 0; i < wifiCount; i++) {
+ String mac = String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+ buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte(),
+ buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ network.addWifiAccessPoint(WifiAccessPoint.from(mac, buf.readUnsignedByte()));
+ }
+
+ int cellCount = buf.readUnsignedByte();
+ int mcc = buf.readUnsignedShort();
+ int mnc = buf.readUnsignedByte();
+ for (int i = 0; i < cellCount; i++) {
+ network.addCellTower(CellTower.from(
+ mcc, mnc, buf.readUnsignedShort(), buf.readUnsignedShort(), buf.readUnsignedByte()));
+ }
+
+ position.setNetwork(network);
+
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeShort(0x7878);
+ response.writeByte(0);
+ response.writeByte(type);
+ response.writeBytes(time.resetReaderIndex());
+ response.writeByte('\r');
+ response.writeByte('\n');
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+
+ return position;
+ }
+
+ private Object decodeBasicOther(Channel channel, ByteBuf buf,
+ DeviceSession deviceSession, int type, int dataLength) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (type == MSG_LBS_MULTIPLE || type == MSG_LBS_EXTEND || type == MSG_LBS_WIFI
+ || type == MSG_LBS_2 || type == MSG_WIFI_3) {
+
+ boolean longFormat = type == MSG_LBS_2 || type == MSG_WIFI_3;
+
+ DateBuilder dateBuilder = new DateBuilder(deviceSession.getTimeZone())
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+
+ getLastLocation(position, dateBuilder.getDate());
+
+ int mcc = buf.readUnsignedShort();
+ int mnc = BitUtil.check(mcc, 15) ? buf.readUnsignedShort() : buf.readUnsignedByte();
+ Network network = new Network();
+ for (int i = 0; i < 7; i++) {
+ int lac = longFormat ? buf.readInt() : buf.readUnsignedShort();
+ int cid = longFormat ? (int) buf.readLong() : buf.readUnsignedMedium();
+ int rssi = -buf.readUnsignedByte();
+ if (lac > 0) {
+ network.addCellTower(CellTower.from(BitUtil.to(mcc, 15), mnc, lac, cid, rssi));
+ }
+ }
+
+ buf.readUnsignedByte(); // time leads
+
+ if (type != MSG_LBS_MULTIPLE && type != MSG_LBS_2) {
+ int wifiCount = buf.readUnsignedByte();
+ for (int i = 0; i < wifiCount; i++) {
+ String mac = ByteBufUtil.hexDump(buf.readSlice(6)).replaceAll("(..)", "$1:");
+ network.addWifiAccessPoint(WifiAccessPoint.from(
+ mac.substring(0, mac.length() - 1), buf.readUnsignedByte()));
+ }
+ }
+
+ position.setNetwork(network);
+
+ } else if (type == MSG_STRING) {
+
+ getLastLocation(position, null);
+
+ int commandLength = buf.readUnsignedByte();
+
+ if (commandLength > 0) {
+ buf.readUnsignedByte(); // server flag (reserved)
+ position.set(Position.KEY_RESULT,
+ buf.readSlice(commandLength - 1).toString(StandardCharsets.US_ASCII));
+ }
+
+ } else if (isSupported(type)) {
+
+ if (hasGps(type)) {
+ decodeGps(position, buf, false, deviceSession.getTimeZone());
+ } else {
+ getLastLocation(position, null);
+ }
+
+ if (hasLbs(type)) {
+ decodeLbs(position, buf, hasStatus(type));
+ }
+
+ if (hasStatus(type)) {
+ decodeStatus(position, buf);
+ }
+
+ if (type == MSG_GPS_LBS_1 && buf.readableBytes() == 2 + 6) {
+ int mask = buf.readUnsignedShort();
+ position.set(Position.KEY_IGNITION, BitUtil.check(mask, 8 + 7));
+ position.set(Position.PREFIX_IN + 2, BitUtil.check(mask, 8 + 6));
+ if (BitUtil.check(mask, 8 + 4)) {
+ int value = BitUtil.to(mask, 8 + 1);
+ if (BitUtil.check(mask, 8 + 1)) {
+ value = -value;
+ }
+ position.set(Position.PREFIX_TEMP + 1, value);
+ } else {
+ int value = BitUtil.to(mask, 8 + 2);
+ if (BitUtil.check(mask, 8 + 5)) {
+ position.set(Position.PREFIX_ADC + 1, value);
+ } else {
+ position.set(Position.PREFIX_ADC + 1, value * 0.1);
+ }
+ }
+ }
+
+ if (type == MSG_GPS_LBS_1 && buf.readableBytes() == 4 + 6) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+ }
+
+ if (type == MSG_GPS_LBS_2 && buf.readableBytes() == 3 + 6) {
+ position.set(Position.KEY_IGNITION, buf.readUnsignedByte() > 0);
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte()); // reason
+ position.set(Position.KEY_ARCHIVE, buf.readUnsignedByte() > 0);
+ }
+
+ } else {
+
+ if (dataLength > 0) {
+ buf.skipBytes(dataLength);
+ }
+ if (type != MSG_COMMAND_0 && type != MSG_COMMAND_1 && type != MSG_COMMAND_2) {
+ sendResponse(channel, false, type, buf.getShort(buf.writerIndex() - 6), null);
+ }
+ return null;
+
+ }
+
+ if (hasLanguage(type)) {
+ buf.readUnsignedShort();
+ }
+
+ if (type == MSG_GPS_LBS_STATUS_3 || type == MSG_FENCE_MULTI) {
+ position.set(Position.KEY_GEOFENCE, buf.readUnsignedByte());
+ }
+
+ sendResponse(channel, false, type, buf.getShort(buf.writerIndex() - 6), null);
+
+ return position;
+ }
+
+ private Object decodeExtended(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (deviceSession.getTimeZone() == null) {
+ deviceSession.setTimeZone(getTimeZone(deviceSession.getDeviceId()));
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedShort(); // length
+ int type = buf.readUnsignedByte();
+
+ if (type == MSG_STRING_INFO) {
+
+ buf.readUnsignedInt(); // server flag
+ String data;
+ if (buf.readUnsignedByte() == 1) {
+ data = buf.readSlice(buf.readableBytes() - 6).toString(StandardCharsets.US_ASCII);
+ } else {
+ data = buf.readSlice(buf.readableBytes() - 6).toString(StandardCharsets.UTF_16BE);
+ }
+
+ if (decodeLocationString(position, data) == null) {
+ getLastLocation(position, null);
+ position.set(Position.KEY_RESULT, data);
+ }
+
+ return position;
+
+ } else if (type == MSG_INFO) {
+
+ int subType = buf.readUnsignedByte();
+
+ getLastLocation(position, null);
+
+ if (subType == 0x00) {
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.01);
+ return position;
+ } else if (subType == 0x05) {
+ int flags = buf.readUnsignedByte();
+ position.set(Position.KEY_DOOR, BitUtil.check(flags, 0));
+ position.set(Position.PREFIX_IO + 1, BitUtil.check(flags, 2));
+ return position;
+ } else if (subType == 0x0a) {
+ buf.skipBytes(8); // imei
+ buf.skipBytes(8); // imsi
+ position.set("iccid", ByteBufUtil.hexDump(buf.readSlice(8)));
+ return position;
+ } else if (subType == 0x0d) {
+ if (buf.getByte(buf.readerIndex()) != '!') {
+ buf.skipBytes(6);
+ }
+ return decodeFuelData(position, buf.toString(
+ buf.readerIndex(), buf.readableBytes() - 4 - 2, StandardCharsets.US_ASCII));
+ }
+
+ } else if (type == MSG_X1_PHOTO_DATA) {
+
+ int pictureId = buf.readInt();
+
+ ByteBuf photo = photos.get(pictureId);
+
+ buf.readUnsignedInt(); // offset
+ buf.readBytes(photo, buf.readUnsignedShort());
+
+ if (photo.writableBytes() > 0) {
+ sendPhotoRequest(channel, pictureId);
+ } else {
+ Device device = Context.getDeviceManager().getById(deviceSession.getDeviceId());
+ position.set(
+ Position.KEY_IMAGE, Context.getMediaManager().writeFile(device.getUniqueId(), photo, "jpg"));
+ photos.remove(pictureId).release();
+ }
+
+ } else if (type == MSG_AZ735_GPS || type == MSG_AZ735_ALARM) {
+
+ if (!decodeGps(position, buf, true, deviceSession.getTimeZone())) {
+ getLastLocation(position, position.getDeviceTime());
+ }
+
+ if (decodeLbs(position, buf, true)) {
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ }
+
+ buf.skipBytes(buf.readUnsignedByte()); // additional cell towers
+ buf.skipBytes(buf.readUnsignedByte()); // wifi access point
+
+ int status = buf.readUnsignedByte();
+ position.set(Position.KEY_STATUS, status);
+
+ if (type == MSG_AZ735_ALARM) {
+ switch (status) {
+ case 0xA0:
+ position.set(Position.KEY_ARMED, true);
+ break;
+ case 0xA1:
+ position.set(Position.KEY_ARMED, false);
+ break;
+ case 0xA2:
+ case 0xA3:
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ case 0xA4:
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ break;
+ case 0xA5:
+ position.set(Position.KEY_ALARM, Position.ALARM_DOOR);
+ break;
+ default:
+ break;
+ }
+ }
+
+ buf.skipBytes(buf.readUnsignedByte()); // reserved extension
+
+ sendResponse(channel, true, type, buf.getShort(buf.writerIndex() - 6), null);
+
+ return position;
+
+ } else if (type == MSG_OBD) {
+
+ DateBuilder dateBuilder = new DateBuilder(deviceSession.getTimeZone())
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+
+ getLastLocation(position, dateBuilder.getDate());
+
+ position.set(Position.KEY_IGNITION, buf.readUnsignedByte() > 0);
+
+ String data = buf.readCharSequence(buf.readableBytes() - 18, StandardCharsets.US_ASCII).toString();
+ for (String pair : data.split(",")) {
+ String[] values = pair.split("=");
+ switch (Integer.parseInt(values[0].substring(0, 2), 16)) {
+ case 40:
+ position.set(Position.KEY_ODOMETER, Integer.parseInt(values[1], 16) * 0.01);
+ break;
+ case 43:
+ position.set(Position.KEY_FUEL_LEVEL, Integer.parseInt(values[1], 16) * 0.01);
+ break;
+ case 45:
+ position.set(Position.KEY_COOLANT_TEMP, Integer.parseInt(values[1], 16) * 0.01);
+ break;
+ case 53:
+ position.set(Position.KEY_OBD_SPEED, Integer.parseInt(values[1], 16) * 0.01);
+ break;
+ case 54:
+ position.set(Position.KEY_RPM, Integer.parseInt(values[1], 16) * 0.01);
+ break;
+ case 71:
+ position.set(Position.KEY_FUEL_USED, Integer.parseInt(values[1], 16) * 0.01);
+ break;
+ case 73:
+ position.set(Position.KEY_HOURS, Integer.parseInt(values[1], 16) * 0.01);
+ break;
+ case 74:
+ position.set(Position.KEY_VIN, values[1]);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int header = buf.readShort();
+
+ if (header == 0x7878) {
+ return decodeBasic(channel, remoteAddress, buf);
+ } else if (header == 0x7979) {
+ return decodeExtended(channel, remoteAddress, buf);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gt06ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Gt06ProtocolEncoder.java
new file mode 100644
index 000000000..05560229f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gt06ProtocolEncoder.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.Context;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class Gt06ProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeContent(long deviceId, String content) {
+
+ boolean language = Context.getIdentityManager().lookupAttributeBoolean(deviceId, "gt06.language", false, true);
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeByte(0x78);
+ buf.writeByte(0x78);
+
+ buf.writeByte(1 + 1 + 4 + content.length() + 2 + 2 + (language ? 2 : 0)); // message length
+
+ buf.writeByte(0x80); // message type
+
+ buf.writeByte(4 + content.length()); // command length
+ buf.writeInt(0);
+ buf.writeBytes(content.getBytes(StandardCharsets.US_ASCII)); // command
+
+ if (language) {
+ buf.writeShort(2); // english language
+ }
+
+ buf.writeShort(0); // message index
+
+ buf.writeShort(Checksum.crc16(Checksum.CRC16_X25, buf.nioBuffer(2, buf.writerIndex() - 2)));
+
+ buf.writeByte('\r');
+ buf.writeByte('\n');
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ boolean alternative = Context.getIdentityManager().lookupAttributeBoolean(
+ command.getDeviceId(), "gt06.alternative", false, true);
+
+ switch (command.getType()) {
+ case Command.TYPE_ENGINE_STOP:
+ return encodeContent(command.getDeviceId(), alternative ? "DYD,123456#" : "Relay,1#");
+ case Command.TYPE_ENGINE_RESUME:
+ return encodeContent(command.getDeviceId(), alternative ? "HFYD,123456#" : "Relay,0#");
+ case Command.TYPE_CUSTOM:
+ return encodeContent(command.getDeviceId(), command.getString(Command.KEY_DATA));
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gt30Protocol.java b/src/main/java/org/traccar/protocol/Gt30Protocol.java
new file mode 100644
index 000000000..aa4ad20b1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gt30Protocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Gt30Protocol extends BaseProtocol {
+
+ public Gt30Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Gt30ProtocolDecoder(Gt30Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Gt30ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gt30ProtocolDecoder.java
new file mode 100644
index 000000000..abf208a46
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Gt30ProtocolDecoder.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Gt30ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Gt30ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$$")
+ .number("x{4}") // length
+ .expression("(.{14})") // device id
+ .number("x{4}") // type
+ .expression("(.)?") // alarm
+ .number("(dd)(dd)(dd).(ddd),") // time (hhmmss.sss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+)?,") // speed
+ .number("(d+.d+)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .expression("[^\\|]*")
+ .number("|(d+.d+)") // hdop
+ .number("|(-?d+)") // altitude
+ .number("x{4}") // checksum
+ .compile();
+
+ private String decodeAlarm(int value) {
+ switch (value) {
+ case 0x01:
+ case 0x02:
+ case 0x03:
+ return Position.ALARM_SOS;
+ case 0x10:
+ return Position.ALARM_LOW_BATTERY;
+ case 0x11:
+ return Position.ALARM_OVERSPEED;
+ case 0x12:
+ return Position.ALARM_GEOFENCE;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next().trim());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.next().charAt(0)));
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ position.setAltitude(parser.nextDouble(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/H02FrameDecoder.java b/src/main/java/org/traccar/protocol/H02FrameDecoder.java
new file mode 100644
index 000000000..583ad599f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/H02FrameDecoder.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class H02FrameDecoder extends BaseFrameDecoder {
+
+ private static final int MESSAGE_SHORT = 32;
+ private static final int MESSAGE_LONG = 45;
+
+ private int messageLength;
+
+ public H02FrameDecoder(int messageLength) {
+ this.messageLength = messageLength;
+ }
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ char marker = (char) buf.getByte(buf.readerIndex());
+
+ while (marker != '*' && marker != '$' && marker != 'X' && buf.readableBytes() > 0) {
+ buf.skipBytes(1);
+ if (buf.readableBytes() > 0) {
+ marker = (char) buf.getByte(buf.readerIndex());
+ }
+ }
+
+ switch (marker) {
+ case '*':
+
+ // Return text message
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '#');
+ if (index != -1) {
+ ByteBuf result = buf.readRetainedSlice(index + 1 - buf.readerIndex());
+ while (buf.isReadable()
+ && (buf.getByte(buf.readerIndex()) == '\r' || buf.getByte(buf.readerIndex()) == '\n')) {
+ buf.readByte(); // skip new line
+ }
+ return result;
+ }
+
+ break;
+
+ case '$':
+
+ if (messageLength == 0) {
+ if (buf.readableBytes() == MESSAGE_LONG) {
+ messageLength = MESSAGE_LONG;
+ } else {
+ messageLength = MESSAGE_SHORT;
+ }
+ }
+
+ if (buf.readableBytes() >= messageLength) {
+ return buf.readRetainedSlice(messageLength);
+ }
+
+ break;
+
+ case 'X':
+
+ if (buf.readableBytes() >= MESSAGE_SHORT) {
+ return buf.readRetainedSlice(MESSAGE_SHORT);
+ }
+
+ break;
+
+ default:
+
+ return null;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/H02Protocol.java b/src/main/java/org/traccar/protocol/H02Protocol.java
new file mode 100644
index 000000000..251beac5e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/H02Protocol.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.Context;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class H02Protocol extends BaseProtocol {
+
+ public H02Protocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ALARM_ARM,
+ Command.TYPE_ALARM_DISARM,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_POSITION_PERIODIC
+ );
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ int messageLength = Context.getConfig().getInteger(getName() + ".messageLength");
+ pipeline.addLast(new H02FrameDecoder(messageLength));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new H02ProtocolEncoder());
+ pipeline.addLast(new H02ProtocolDecoder(H02Protocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new H02ProtocolEncoder());
+ pipeline.addLast(new H02ProtocolDecoder(H02Protocol.this));
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/traccar/protocol/H02ProtocolDecoder.java b/src/main/java/org/traccar/protocol/H02ProtocolDecoder.java
new file mode 100644
index 000000000..c4443a00b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/H02ProtocolDecoder.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BcdUtil;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+public class H02ProtocolDecoder extends BaseProtocolDecoder {
+
+ public H02ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static double readCoordinate(ByteBuf buf, boolean lon) {
+
+ int degrees = BcdUtil.readInteger(buf, 2);
+ if (lon) {
+ degrees = degrees * 10 + (buf.getUnsignedByte(buf.readerIndex()) >> 4);
+ }
+
+ double result = 0;
+ if (lon) {
+ result = buf.readUnsignedByte() & 0x0f;
+ }
+
+ int length = 6;
+ if (lon) {
+ length = 5;
+ }
+
+ result = result * 10 + BcdUtil.readInteger(buf, length) * 0.0001;
+
+ result /= 60;
+ result += degrees;
+
+ return result;
+ }
+
+ private void processStatus(Position position, long status) {
+
+ if (!BitUtil.check(status, 0)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_VIBRATION);
+ } else if (!BitUtil.check(status, 1) || !BitUtil.check(status, 18)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ } else if (!BitUtil.check(status, 2)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ } else if (!BitUtil.check(status, 19)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_CUT);
+ }
+
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 10));
+ position.set(Position.KEY_STATUS, status);
+
+ }
+
+ private Integer decodeBattery(int value) {
+ switch (value) {
+ case 6:
+ return 100;
+ case 5:
+ return 80;
+ case 4:
+ return 60;
+ case 3:
+ return 20;
+ case 2:
+ return 10;
+ default:
+ return null;
+ }
+ }
+
+ private Position decodeBinary(ByteBuf buf, Channel channel, SocketAddress remoteAddress) {
+
+ Position position = new Position(getProtocolName());
+
+ boolean longId = buf.readableBytes() == 42;
+
+ buf.readByte(); // marker
+
+ String id;
+ if (longId) {
+ id = ByteBufUtil.hexDump(buf.readSlice(8)).substring(0, 15);
+ } else {
+ id = ByteBufUtil.hexDump(buf.readSlice(5));
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setHour(BcdUtil.readInteger(buf, 2))
+ .setMinute(BcdUtil.readInteger(buf, 2))
+ .setSecond(BcdUtil.readInteger(buf, 2))
+ .setDay(BcdUtil.readInteger(buf, 2))
+ .setMonth(BcdUtil.readInteger(buf, 2))
+ .setYear(BcdUtil.readInteger(buf, 2));
+ position.setTime(dateBuilder.getDate());
+
+ double latitude = readCoordinate(buf, false);
+ position.set(Position.KEY_BATTERY_LEVEL, decodeBattery(buf.readUnsignedByte()));
+ double longitude = readCoordinate(buf, true);
+
+ int flags = buf.readUnsignedByte() & 0x0f;
+ position.setValid((flags & 0x02) != 0);
+ if ((flags & 0x04) == 0) {
+ latitude = -latitude;
+ }
+ if ((flags & 0x08) == 0) {
+ longitude = -longitude;
+ }
+
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+
+ position.setSpeed(BcdUtil.readInteger(buf, 3));
+ position.setCourse((buf.readUnsignedByte() & 0x0f) * 100.0 + BcdUtil.readInteger(buf, 2));
+
+ processStatus(position, buf.readUnsignedInt());
+
+ return position;
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("*")
+ .expression("..,") // manufacturer
+ .number("(d+)?,") // imei
+ .groupBegin()
+ .text("V4,")
+ .expression("(.*),") // response
+ .or()
+ .expression("(V[^,]*),")
+ .groupEnd()
+ .number("(?:(dd)(dd)(dd))?,") // time (hhmmss)
+ .groupBegin()
+ .expression("([ABV])?,") // validity
+ .or()
+ .number("(d+),") // coding scheme
+ .groupEnd()
+ .groupBegin()
+ .number("-(d+)-(d+.d+),") // latitude
+ .or()
+ .number("(d+)(dd.d+),") // latitude
+ .groupEnd()
+ .expression("([NS]),")
+ .groupBegin()
+ .number("-(d+)-(d+.d+),") // longitude
+ .or()
+ .number("(d+)(dd.d+),") // longitude
+ .groupEnd()
+ .expression("([EW]),")
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(?:d+,)?") // battery
+ .number("(?:(dd)(dd)(dd))?") // date (ddmmyy)
+ .groupBegin()
+ .expression(",[^,]*,")
+ .expression("[^,]*,")
+ .expression("[^,]*") // sim info
+ .groupEnd("?")
+ .groupBegin()
+ .number(",(x{8})")
+ .groupBegin()
+ .number(",(d+),") // odometer
+ .number("(-?d+),") // temperature
+ .number("(d+.d+),") // fuel
+ .number("(-?d+),") // altitude
+ .number("(x+),") // lac
+ .number("(x+)") // cid
+ .or()
+ .text(",")
+ .expression("(.*)") // data
+ .or()
+ .groupEnd()
+ .or()
+ .groupEnd()
+ .text("#")
+ .compile();
+
+ private static final Pattern PATTERN_NBR = new PatternBuilder()
+ .text("*")
+ .expression("..,") // manufacturer
+ .number("(d+),") // imei
+ .text("NBR,")
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("d+,") // gsm delay time
+ .number("d+,") // count
+ .number("((?:d+,d+,d+,)+)") // cells
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(x{8})") // status
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_LINK = new PatternBuilder()
+ .text("*")
+ .expression("..,") // manufacturer
+ .number("(d+),") // imei
+ .text("LINK,")
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d+),") // rssi
+ .number("(d+),") // satellites
+ .number("(d+),") // battery
+ .number("(d+),") // steps
+ .number("(d+),") // turnovers
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(x{8})") // status
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_V3 = new PatternBuilder()
+ .text("*")
+ .expression("..,") // manufacturer
+ .number("(d+),") // imei
+ .text("V3,")
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(ddd)") // mcc
+ .number("(d+),") // mnc
+ .number("(d+),") // count
+ .expression("(.*),") // cell info
+ .number("(x{4}),") // battery
+ .number("d+,") // reboot info
+ .text("X,")
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(x{8})") // status
+ .text("#").optional()
+ .compile();
+
+ private static final Pattern PATTERN_VP1 = new PatternBuilder()
+ .text("*hq,")
+ .number("(d{15}),") // imei
+ .text("VP1,")
+ .groupBegin()
+ .text("V,")
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .expression("([^#]+)") // cells
+ .or()
+ .expression("[AB],") // validity
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+),") // speed
+ .number("(d+.d+),") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .groupEnd()
+ .any()
+ .compile();
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress, String id, String type) {
+ if (channel != null && id != null) {
+ DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String response = String.format("*HQ,%s,V4,%s,%s#", id, type, dateFormat.format(new Date()));
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ private Position decodeText(String sentence, Channel channel, SocketAddress remoteAddress) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String id = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_RESULT, parser.next());
+ }
+
+ if (parser.hasNext() && parser.next().equals("V1")) {
+ sendResponse(channel, remoteAddress, id, "V1");
+ }
+
+ DateBuilder dateBuilder = new DateBuilder();
+ if (parser.hasNext(3)) {
+ dateBuilder.setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ }
+
+ if (parser.hasNext()) {
+ position.setValid(parser.next().equals("A"));
+ }
+ if (parser.hasNext()) {
+ parser.nextInt(); // coding scheme
+ position.setValid(true);
+ }
+
+ if (parser.hasNext(2)) {
+ position.setLatitude(-parser.nextCoordinate());
+ }
+ if (parser.hasNext(2)) {
+ position.setLatitude(parser.nextCoordinate());
+ }
+
+ if (parser.hasNext(2)) {
+ position.setLongitude(-parser.nextCoordinate());
+ }
+ if (parser.hasNext(2)) {
+ position.setLongitude(parser.nextCoordinate());
+ }
+
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ if (parser.hasNext(3)) {
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+ } else {
+ position.setTime(new Date());
+ }
+
+ if (parser.hasNext()) {
+ processStatus(position, parser.nextLong(16, 0));
+ }
+
+ if (parser.hasNext(6)) {
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt(0));
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextDouble(0));
+
+ position.setAltitude(parser.nextInt(0));
+
+ position.setNetwork(new Network(CellTower.fromLacCid(parser.nextHexInt(0), parser.nextHexInt(0))));
+ }
+
+ if (parser.hasNext(4)) {
+ String[] values = parser.next().split(",");
+ for (int i = 0; i < values.length; i++) {
+ position.set(Position.PREFIX_IO + (i + 1), values[i].trim());
+ }
+ }
+
+ return position;
+ }
+
+ private Position decodeLbs(String sentence, Channel channel, SocketAddress remoteAddress) {
+
+ Parser parser = new Parser(PATTERN_NBR, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String id = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ sendResponse(channel, remoteAddress, id, "NBR");
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ Network network = new Network();
+ int mcc = parser.nextInt(0);
+ int mnc = parser.nextInt(0);
+
+ String[] cells = parser.next().split(",");
+ for (int i = 0; i < cells.length / 3; i++) {
+ network.addCellTower(CellTower.from(mcc, mnc, Integer.parseInt(cells[i * 3]),
+ Integer.parseInt(cells[i * 3 + 1]), Integer.parseInt(cells[i * 3 + 2])));
+ }
+
+ position.setNetwork(network);
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ getLastLocation(position, dateBuilder.getDate());
+
+ processStatus(position, parser.nextLong(16, 0));
+
+ return position;
+ }
+
+ private Position decodeLink(String sentence, Channel channel, SocketAddress remoteAddress) {
+
+ Parser parser = new Parser(PATTERN_LINK, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.set(Position.KEY_RSSI, parser.nextInt());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+ position.set(Position.KEY_STEPS, parser.nextInt());
+ position.set("turnovers", parser.nextInt());
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ getLastLocation(position, dateBuilder.getDate());
+
+ processStatus(position, parser.nextLong(16, 0));
+
+ return position;
+ }
+
+ private Position decodeV3(String sentence, Channel channel, SocketAddress remoteAddress) {
+
+ Parser parser = new Parser(PATTERN_V3, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ int mcc = parser.nextInt();
+ int mnc = parser.nextInt();
+
+ int count = parser.nextInt();
+ Network network = new Network();
+ String[] values = parser.next().split(",");
+ for (int i = 0; i < count; i++) {
+ network.addCellTower(CellTower.from(
+ mcc, mnc, Integer.parseInt(values[i * 4]), Integer.parseInt(values[i * 4 + 1])));
+ }
+ position.setNetwork(network);
+
+ position.set(Position.KEY_BATTERY, parser.nextHexInt());
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ getLastLocation(position, dateBuilder.getDate());
+
+ processStatus(position, parser.nextLong(16, 0));
+
+ return position;
+ }
+
+ private Position decodeVp1(String sentence, Channel channel, SocketAddress remoteAddress) {
+
+ Parser parser = new Parser(PATTERN_VP1, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.hasNext(3)) {
+
+ getLastLocation(position, null);
+
+ int mcc = parser.nextInt();
+ int mnc = parser.nextInt();
+
+ Network network = new Network();
+ for (String cell : parser.next().split("Y")) {
+ String[] values = cell.split(",");
+ network.addCellTower(CellTower.from(mcc, mnc,
+ Integer.parseInt(values[0]), Integer.parseInt(values[1]), Integer.parseInt(values[2])));
+ }
+
+ position.setNetwork(network);
+
+ } else {
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble());
+ position.setCourse(parser.nextDouble());
+
+ position.setTime(new DateBuilder()
+ .setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0)).getDate());
+
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+ String marker = buf.toString(0, 1, StandardCharsets.US_ASCII);
+
+ switch (marker) {
+ case "*":
+ String sentence = buf.toString(StandardCharsets.US_ASCII).trim();
+ int typeStart = sentence.indexOf(',', sentence.indexOf(',') + 1) + 1;
+ int typeEnd = sentence.indexOf(',', typeStart);
+ if (typeEnd > 0) {
+ String type = sentence.substring(typeStart, typeEnd);
+ switch (type) {
+ case "NBR":
+ return decodeLbs(sentence, channel, remoteAddress);
+ case "LINK":
+ return decodeLink(sentence, channel, remoteAddress);
+ case "V3":
+ return decodeV3(sentence, channel, remoteAddress);
+ case "VP1":
+ return decodeVp1(sentence, channel, remoteAddress);
+ default:
+ return decodeText(sentence, channel, remoteAddress);
+ }
+ } else {
+ return null;
+ }
+ case "$":
+ return decodeBinary(buf, channel, remoteAddress);
+ case "X":
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/H02ProtocolEncoder.java b/src/main/java/org/traccar/protocol/H02ProtocolEncoder.java
new file mode 100644
index 000000000..614a07dd1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/H02ProtocolEncoder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Gabor Somogyi (gabor.g.somogyi@gmail.com)
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.Context;
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+import java.util.Date;
+
+public class H02ProtocolEncoder extends StringProtocolEncoder {
+
+ private static final String MARKER = "HQ";
+
+ private Object formatCommand(Date time, String uniqueId, String type, String... params) {
+
+ StringBuilder result = new StringBuilder(
+ String.format("*%s,%s,%s,%4$tH%4$tM%4$tS", MARKER, uniqueId, type, time));
+
+ for (String param : params) {
+ result.append(",").append(param);
+ }
+
+ result.append("#");
+
+ return result.toString();
+ }
+
+ protected Object encodeCommand(Command command, Date time) {
+ String uniqueId = getUniqueId(command.getDeviceId());
+
+ switch (command.getType()) {
+ case Command.TYPE_ALARM_ARM:
+ return formatCommand(time, uniqueId, "SCF", "0", "0");
+ case Command.TYPE_ALARM_DISARM:
+ return formatCommand(time, uniqueId, "SCF", "1", "1");
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(time, uniqueId, "S20", "1", "1");
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(time, uniqueId, "S20", "1", "0");
+ case Command.TYPE_POSITION_PERIODIC:
+ String frequency = command.getAttributes().get(Command.KEY_FREQUENCY).toString();
+ if (Context.getIdentityManager().lookupAttributeBoolean(
+ command.getDeviceId(), "h02.alternative", false, true)) {
+ return formatCommand(time, uniqueId, "D1", frequency);
+ } else {
+ return formatCommand(time, uniqueId, "S71", "22", frequency);
+ }
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+ return encodeCommand(command, new Date());
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HaicomProtocol.java b/src/main/java/org/traccar/protocol/HaicomProtocol.java
new file mode 100644
index 000000000..6e5760bd4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HaicomProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class HaicomProtocol extends BaseProtocol {
+
+ public HaicomProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '*'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new HaicomProtocolDecoder(HaicomProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HaicomProtocolDecoder.java b/src/main/java/org/traccar/protocol/HaicomProtocolDecoder.java
new file mode 100644
index 000000000..dd20f2aeb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HaicomProtocolDecoder.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class HaicomProtocolDecoder extends BaseProtocolDecoder {
+
+ public HaicomProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$GPRS")
+ .number("(d+),") // imei
+ .expression("([^,]+),") // version
+ .number("(dd)(dd)(dd),") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d)") // flags
+ .number("(dd)(d{5})") // latitude
+ .number("(ddd)(d{5}),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // status
+ .number("(d+)?,") // gprs counting value
+ .number("(d+)?,") // gps power saving counting value
+ .number("(d+),") // switch status
+ .number("(d+)") // relay status
+ .expression("(?:[LH]{2})?") // power status
+ .number("#V(d+)") // battery
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_VERSION_FW, parser.next());
+
+ position.setTime(parser.nextDateTime());
+
+ int flags = parser.nextInt(0);
+
+ position.setValid(BitUtil.check(flags, 0));
+
+ double latitude = parser.nextDouble(0) + parser.nextDouble(0) / 60000;
+ if (BitUtil.check(flags, 2)) {
+ position.setLatitude(latitude);
+ } else {
+ position.setLatitude(-latitude);
+ }
+
+ double longitude = parser.nextDouble(0) + parser.nextDouble(0) / 60000;
+ if (BitUtil.check(flags, 1)) {
+ position.setLongitude(longitude);
+ } else {
+ position.setLongitude(-longitude);
+ }
+
+ position.setSpeed(parser.nextDouble(0) / 10);
+ position.setCourse(parser.nextDouble(0) / 10);
+
+ position.set(Position.KEY_STATUS, parser.next());
+ position.set("gprsCount", parser.next());
+ position.set("powersaveCountdown", parser.next());
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0) * 0.1);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HomtecsProtocol.java b/src/main/java/org/traccar/protocol/HomtecsProtocol.java
new file mode 100644
index 000000000..34dbf0f51
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HomtecsProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class HomtecsProtocol extends BaseProtocol {
+
+ public HomtecsProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new HomtecsProtocolDecoder(HomtecsProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HomtecsProtocolDecoder.java b/src/main/java/org/traccar/protocol/HomtecsProtocolDecoder.java
new file mode 100644
index 000000000..a93572b5c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HomtecsProtocolDecoder.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class HomtecsProtocolDecoder extends BaseProtocolDecoder {
+
+ public HomtecsProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("([^_]+)") // id
+ .text("_R")
+ .number("(x{8}),") // mac ending
+ .number("(dd)(dd)(dd),") // date (yymmdd)
+ .number("(dd)(dd)(dd).d+,") // time (hhmmss)
+ .number("(d+),") // satellites
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(d),") // fix status
+ .number("(d+.?d*)?,") // hdop
+ .number("(d+.?d*)?") // altitude
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String id = parser.next();
+ String mac = parser.next();
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id, id + "_R" + mac);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.YMD_HMS));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.setValid(parser.nextInt(0) > 0);
+
+ position.set(Position.KEY_HDOP, parser.nextDouble(0));
+
+ position.setAltitude(parser.nextDouble(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HuaShengFrameDecoder.java b/src/main/java/org/traccar/protocol/HuaShengFrameDecoder.java
new file mode 100644
index 000000000..bd52aa9e7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HuaShengFrameDecoder.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class HuaShengFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 2) {
+ return null;
+ }
+
+ int index = buf.indexOf(buf.readerIndex() + 1, buf.writerIndex(), (byte) 0xC0);
+ if (index != -1) {
+ ByteBuf result = Unpooled.buffer(index + 1 - buf.readerIndex());
+
+ while (buf.readerIndex() <= index) {
+ int b = buf.readUnsignedByte();
+ if (b == 0xDB) {
+ int ext = buf.readUnsignedByte();
+ if (ext == 0xDC) {
+ result.writeByte(0xC0);
+ } else if (ext == 0xDD) {
+ result.writeByte(0xDB);
+ }
+ } else {
+ result.writeByte(b);
+ }
+ }
+
+ return result;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HuaShengProtocol.java b/src/main/java/org/traccar/protocol/HuaShengProtocol.java
new file mode 100644
index 000000000..103f2d501
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HuaShengProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class HuaShengProtocol extends BaseProtocol {
+
+ public HuaShengProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HuaShengFrameDecoder());
+ pipeline.addLast(new HuaShengProtocolDecoder(HuaShengProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HuaShengProtocolDecoder.java b/src/main/java/org/traccar/protocol/HuaShengProtocolDecoder.java
new file mode 100644
index 000000000..8a937a194
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HuaShengProtocolDecoder.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+
+public class HuaShengProtocolDecoder extends BaseProtocolDecoder {
+
+ public HuaShengProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_POSITION = 0xAA00;
+ public static final int MSG_POSITION_RSP = 0xFF01;
+ public static final int MSG_LOGIN = 0xAA02;
+ public static final int MSG_LOGIN_RSP = 0xFF03;
+ public static final int MSG_HSO_REQ = 0x0002;
+ public static final int MSG_HSO_RSP = 0x0003;
+
+ private void sendResponse(Channel channel, int type, int index, ByteBuf content) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(0xC0);
+ response.writeShort(0x0100);
+ response.writeShort(12 + (content != null ? content.readableBytes() : 0));
+ response.writeShort(type);
+ response.writeShort(0);
+ response.writeInt(index);
+ if (content != null) {
+ response.writeBytes(content);
+ content.release();
+ }
+ response.writeByte(0xC0);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(1); // start marker
+ buf.readUnsignedByte(); // flag
+ buf.readUnsignedByte(); // reserved
+ buf.readUnsignedShort(); // length
+
+ int type = buf.readUnsignedShort();
+
+ buf.readUnsignedShort(); // checksum
+ int index = buf.readInt();
+
+ if (type == MSG_LOGIN) {
+
+ while (buf.readableBytes() > 4) {
+ int subtype = buf.readUnsignedShort();
+ int length = buf.readUnsignedShort() - 4;
+ if (subtype == 0x0003) {
+ String imei = buf.readSlice(length).toString(StandardCharsets.US_ASCII);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession != null && channel != null) {
+ ByteBuf content = Unpooled.buffer();
+ content.writeByte(0); // success
+ sendResponse(channel, MSG_LOGIN_RSP, index, content);
+ }
+ } else {
+ buf.skipBytes(length);
+ }
+ }
+
+ } else if (type == MSG_HSO_REQ) {
+
+ sendResponse(channel, MSG_HSO_RSP, index, null);
+
+ } else if (type == MSG_POSITION) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int status = buf.readUnsignedShort();
+
+ position.setValid(BitUtil.check(status, 15));
+
+ position.set(Position.KEY_STATUS, status);
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 14));
+ position.set(Position.KEY_EVENT, buf.readUnsignedShort());
+
+ String time = buf.readSlice(12).toString(StandardCharsets.US_ASCII);
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setYear(Integer.parseInt(time.substring(0, 2)))
+ .setMonth(Integer.parseInt(time.substring(2, 4)))
+ .setDay(Integer.parseInt(time.substring(4, 6)))
+ .setHour(Integer.parseInt(time.substring(6, 8)))
+ .setMinute(Integer.parseInt(time.substring(8, 10)))
+ .setSecond(Integer.parseInt(time.substring(10, 12)));
+ position.setTime(dateBuilder.getDate());
+
+ position.setLongitude(buf.readInt() * 0.00001);
+ position.setLatitude(buf.readInt() * 0.00001);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+ position.setCourse(buf.readUnsignedShort());
+ position.setAltitude(buf.readUnsignedShort());
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedShort() * 1000);
+
+ while (buf.readableBytes() > 4) {
+ buf.readUnsignedShort(); // subtype
+ int length = buf.readUnsignedShort() - 4;
+ buf.skipBytes(length);
+ }
+
+ sendResponse(channel, MSG_POSITION_RSP, index, null);
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HuabaoFrameDecoder.java b/src/main/java/org/traccar/protocol/HuabaoFrameDecoder.java
new file mode 100644
index 000000000..b520f6be9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HuabaoFrameDecoder.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class HuabaoFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 2) {
+ return null;
+ }
+
+ int index = buf.indexOf(buf.readerIndex() + 1, buf.writerIndex(), (byte) 0x7e);
+ if (index != -1) {
+ ByteBuf result = Unpooled.buffer(index + 1 - buf.readerIndex());
+
+ while (buf.readerIndex() <= index) {
+ int b = buf.readUnsignedByte();
+ if (b == 0x7d) {
+ int ext = buf.readUnsignedByte();
+ if (ext == 0x01) {
+ result.writeByte(0x7d);
+ } else if (ext == 0x02) {
+ result.writeByte(0x7e);
+ }
+ } else {
+ result.writeByte(b);
+ }
+ }
+
+ return result;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HuabaoProtocol.java b/src/main/java/org/traccar/protocol/HuabaoProtocol.java
new file mode 100644
index 000000000..44c9f7ac7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HuabaoProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class HuabaoProtocol extends BaseProtocol {
+
+ public HuabaoProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HuabaoFrameDecoder());
+ pipeline.addLast(new HuabaoProtocolEncoder());
+ pipeline.addLast(new HuabaoProtocolDecoder(HuabaoProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java
new file mode 100644
index 000000000..6e2e1377b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BcdUtil;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+
+public class HuabaoProtocolDecoder extends BaseProtocolDecoder {
+
+ public HuabaoProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_GENERAL_RESPONSE = 0x8001;
+ public static final int MSG_TERMINAL_REGISTER = 0x0100;
+ public static final int MSG_TERMINAL_REGISTER_RESPONSE = 0x8100;
+ public static final int MSG_TERMINAL_CONTROL = 0x8105;
+ public static final int MSG_TERMINAL_AUTH = 0x0102;
+ public static final int MSG_LOCATION_REPORT = 0x0200;
+ public static final int MSG_LOCATION_BATCH = 0x0704;
+ public static final int MSG_OIL_CONTROL = 0XA006;
+
+ public static final int RESULT_SUCCESS = 0;
+
+ public static ByteBuf formatMessage(int type, ByteBuf id, ByteBuf data) {
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeByte(0x7e);
+ buf.writeShort(type);
+ buf.writeShort(data.readableBytes());
+ buf.writeBytes(id);
+ buf.writeShort(1); // index
+ buf.writeBytes(data);
+ data.release();
+ buf.writeByte(Checksum.xor(buf.nioBuffer(1, buf.readableBytes() - 1)));
+ buf.writeByte(0x7e);
+ return buf;
+ }
+
+ private void sendGeneralResponse(
+ Channel channel, SocketAddress remoteAddress, ByteBuf id, int type, int index) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeShort(index);
+ response.writeShort(type);
+ response.writeByte(RESULT_SUCCESS);
+ channel.writeAndFlush(new NetworkMessage(
+ formatMessage(MSG_GENERAL_RESPONSE, id, response), remoteAddress));
+ }
+ }
+
+ private String decodeAlarm(long value) {
+ if (BitUtil.check(value, 0)) {
+ return Position.ALARM_SOS;
+ }
+ if (BitUtil.check(value, 1)) {
+ return Position.ALARM_OVERSPEED;
+ }
+ if (BitUtil.check(value, 5)) {
+ return Position.ALARM_GPS_ANTENNA_CUT;
+ }
+ if (BitUtil.check(value, 4) || BitUtil.check(value, 9)
+ || BitUtil.check(value, 10) || BitUtil.check(value, 11)) {
+ return Position.ALARM_FAULT;
+ }
+ if (BitUtil.check(value, 8)) {
+ return Position.ALARM_POWER_OFF;
+ }
+ if (BitUtil.check(value, 20)) {
+ return Position.ALARM_GEOFENCE;
+ }
+ if (BitUtil.check(value, 29)) {
+ return Position.ALARM_ACCIDENT;
+ }
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedByte(); // start marker
+ int type = buf.readUnsignedShort();
+ buf.readUnsignedShort(); // body length
+ ByteBuf id = buf.readSlice(6); // phone number
+ int index = buf.readUnsignedShort();
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, ByteBufUtil.hexDump(id));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (deviceSession.getTimeZone() == null) {
+ deviceSession.setTimeZone(getTimeZone(deviceSession.getDeviceId(), "GMT+8"));
+ }
+
+ if (type == MSG_TERMINAL_REGISTER) {
+
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeShort(index);
+ response.writeByte(RESULT_SUCCESS);
+ response.writeBytes("authentication".getBytes(StandardCharsets.US_ASCII));
+ channel.writeAndFlush(new NetworkMessage(
+ formatMessage(MSG_TERMINAL_REGISTER_RESPONSE, id, response), remoteAddress));
+ }
+
+ } else if (type == MSG_TERMINAL_AUTH) {
+
+ sendGeneralResponse(channel, remoteAddress, id, type, index);
+
+ } else if (type == MSG_LOCATION_REPORT) {
+
+ return decodeLocation(deviceSession, buf);
+
+ } else if (type == MSG_LOCATION_BATCH) {
+
+ return decodeLocationBatch(deviceSession, buf);
+
+ }
+
+ return null;
+ }
+
+ private Position decodeLocation(DeviceSession deviceSession, ByteBuf buf) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedInt()));
+
+ int flags = buf.readInt();
+
+ position.set(Position.KEY_IGNITION, BitUtil.check(flags, 0));
+
+ position.setValid(BitUtil.check(flags, 1));
+
+ double lat = buf.readUnsignedInt() * 0.000001;
+ double lon = buf.readUnsignedInt() * 0.000001;
+
+ if (BitUtil.check(flags, 2)) {
+ position.setLatitude(-lat);
+ } else {
+ position.setLatitude(lat);
+ }
+
+ if (BitUtil.check(flags, 3)) {
+ position.setLongitude(-lon);
+ } else {
+ position.setLongitude(lon);
+ }
+
+ position.setAltitude(buf.readShort());
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort() * 0.1));
+ position.setCourse(buf.readUnsignedShort());
+
+ DateBuilder dateBuilder = new DateBuilder(deviceSession.getTimeZone())
+ .setYear(BcdUtil.readInteger(buf, 2))
+ .setMonth(BcdUtil.readInteger(buf, 2))
+ .setDay(BcdUtil.readInteger(buf, 2))
+ .setHour(BcdUtil.readInteger(buf, 2))
+ .setMinute(BcdUtil.readInteger(buf, 2))
+ .setSecond(BcdUtil.readInteger(buf, 2));
+ position.setTime(dateBuilder.getDate());
+
+ while (buf.readableBytes() > 2) {
+ int subtype = buf.readUnsignedByte();
+ int length = buf.readUnsignedByte();
+ switch (subtype) {
+ case 0x01:
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt() * 100);
+ break;
+ case 0x30:
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ break;
+ case 0x31:
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ break;
+ default:
+ buf.skipBytes(length);
+ break;
+ }
+ }
+
+ return position;
+ }
+
+ private List<Position> decodeLocationBatch(DeviceSession deviceSession, ByteBuf buf) {
+
+ List<Position> positions = new LinkedList<>();
+
+ int count = buf.readUnsignedShort();
+ buf.readUnsignedByte(); // location type
+
+ for (int i = 0; i < count; i++) {
+ int endIndex = buf.readUnsignedShort() + buf.readerIndex();
+ positions.add(decodeLocation(deviceSession, buf));
+ buf.readerIndex(endIndex);
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HuabaoProtocolEncoder.java b/src/main/java/org/traccar/protocol/HuabaoProtocolEncoder.java
new file mode 100644
index 000000000..7759790c4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HuabaoProtocolEncoder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.Context;
+import org.traccar.helper.DataConverter;
+import org.traccar.model.Command;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class HuabaoProtocolEncoder extends BaseProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ boolean alternative = Context.getIdentityManager().lookupAttributeBoolean(
+ command.getDeviceId(), "huabao.alternative", false, true);
+
+ ByteBuf id = Unpooled.wrappedBuffer(
+ DataConverter.parseHex(getUniqueId(command.getDeviceId())));
+ try {
+ ByteBuf data = Unpooled.buffer();
+ byte[] time = DataConverter.parseHex(new SimpleDateFormat("yyMMddHHmmss").format(new Date()));
+
+ switch (command.getType()) {
+ case Command.TYPE_ENGINE_STOP:
+ if (alternative) {
+ data.writeByte(0x01);
+ data.writeBytes(time);
+ return HuabaoProtocolDecoder.formatMessage(
+ HuabaoProtocolDecoder.MSG_OIL_CONTROL, id, data);
+ } else {
+ data.writeByte(0xf0);
+ return HuabaoProtocolDecoder.formatMessage(
+ HuabaoProtocolDecoder.MSG_TERMINAL_CONTROL, id, data);
+ }
+ case Command.TYPE_ENGINE_RESUME:
+ if (alternative) {
+ data.writeByte(0x00);
+ data.writeBytes(time);
+ return HuabaoProtocolDecoder.formatMessage(
+ HuabaoProtocolDecoder.MSG_OIL_CONTROL, id, data);
+ } else {
+ data.writeByte(0xf1);
+ return HuabaoProtocolDecoder.formatMessage(
+ HuabaoProtocolDecoder.MSG_TERMINAL_CONTROL, id, data);
+ }
+ default:
+ return null;
+ }
+ } finally {
+ id.release();
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HunterProProtocol.java b/src/main/java/org/traccar/protocol/HunterProProtocol.java
new file mode 100644
index 000000000..9f6424a57
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HunterProProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class HunterProProtocol extends BaseProtocol {
+
+ public HunterProProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "\r"));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new HunterProProtocolDecoder(HunterProProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/HunterProProtocolDecoder.java b/src/main/java/org/traccar/protocol/HunterProProtocolDecoder.java
new file mode 100644
index 000000000..06bc12d59
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/HunterProProtocolDecoder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class HunterProProtocolDecoder extends BaseProtocolDecoder {
+
+ public HunterProProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number(">(d+)<") // identifier
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder();
+ dateBuilder.setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/IdplProtocol.java b/src/main/java/org/traccar/protocol/IdplProtocol.java
new file mode 100644
index 000000000..418178756
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/IdplProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class IdplProtocol extends BaseProtocol {
+
+ public IdplProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new IdplProtocolDecoder(IdplProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/IdplProtocolDecoder.java b/src/main/java/org/traccar/protocol/IdplProtocolDecoder.java
new file mode 100644
index 000000000..cf3c03d7f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/IdplProtocolDecoder.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.Parser.CoordinateFormat;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+public class IdplProtocolDecoder extends BaseProtocolDecoder {
+
+ public IdplProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("*ID") // start of frame
+ .number("(d+),") // command code
+ .number("(d+),") // imei
+ .number("(dd)(dd)(dd),") // current date (ddmmyy)
+ .number("(dd)(dd)(dd),") // current time (hhmmss)
+ .expression("([A|V]),") // gps fix
+ .number("(dd)(dd).?(d+),([NS]),") // latitude
+ .number("(ddd)(dd).?(d+),([EW]),") // longitude
+ .number("(d{1,3}.dd),") // speed
+ .number("(d{1,3}.dd),") // course
+ .number("(d{1,2}),") // sats
+ .number("(d{1,3}),") // gsm signal strength
+ .expression("([A|N|S]),") // vehicle status
+ .expression("([0|1]),") // main power status
+ .number("(d.dd),") // internal battery voltage
+ .expression("([0|1]),") // sos alert
+ .expression("([0|1]),") // body tamper
+ .expression("([0|1])([0|1]),") // ac status + ign status
+ .expression("([0|1|2]),") // output1 status
+ .number("(d{1,3}),") // adc1
+ .number("(d{1,3}),") // adc2
+ .expression("([0-9A-Z]{3}),") // software version
+ .expression("([L|R]),") // message type
+ .number("(x{4})#") // crc
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ position.set(Position.KEY_TYPE, parser.nextInt(0));
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate(CoordinateFormat.DEG_MIN_MIN_HEM));
+ position.setLongitude(parser.nextCoordinate(CoordinateFormat.DEG_MIN_MIN_HEM));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_RSSI, parser.nextInt(0));
+ position.set("vehicleStatus", parser.next());
+ position.set(Position.KEY_POWER, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0));
+ if (parser.nextInt(0) == 1) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ }
+ parser.nextInt(0); // body tamper
+ position.set("acStatus", parser.nextInt(0));
+ position.set(Position.KEY_IGNITION, parser.nextInt(0) == 1);
+ position.set(Position.KEY_OUTPUT, parser.nextInt(0));
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt(0));
+ position.set(Position.PREFIX_ADC + 2, parser.nextInt(0));
+ position.set(Position.KEY_VERSION_FW, parser.next());
+ position.set(Position.KEY_ARCHIVE, parser.next().equals("R"));
+
+ parser.next(); // checksum
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/IntellitracFrameDecoder.java b/src/main/java/org/traccar/protocol/IntellitracFrameDecoder.java
new file mode 100644
index 000000000..8322e65ba
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/IntellitracFrameDecoder.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import org.traccar.NetworkMessage;
+
+public class IntellitracFrameDecoder extends LineBasedFrameDecoder {
+
+ private static final int MESSAGE_MINIMUM_LENGTH = 0;
+
+ public IntellitracFrameDecoder(int maxFrameLength) {
+ super(maxFrameLength);
+ }
+
+ // example of sync header: 0xFA 0xF8 0x1B 0x01 0x81 0x60 0x33 0x3C
+
+ @Override
+ protected Object decode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
+
+ // Check minimum length
+ if (buf.readableBytes() < MESSAGE_MINIMUM_LENGTH) {
+ return null;
+ }
+
+ // Check for sync packet
+ if (buf.getUnsignedShort(buf.readerIndex()) == 0xFAF8) {
+ ByteBuf syncMessage = buf.readRetainedSlice(8);
+ if (ctx != null && ctx.channel() != null) {
+ ctx.channel().writeAndFlush(new NetworkMessage(syncMessage, ctx.channel().remoteAddress()));
+ }
+ }
+
+ return super.decode(ctx, buf);
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/IntellitracProtocol.java b/src/main/java/org/traccar/protocol/IntellitracProtocol.java
new file mode 100644
index 000000000..3abf40da7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/IntellitracProtocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class IntellitracProtocol extends BaseProtocol {
+
+ public IntellitracProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new IntellitracFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new IntellitracProtocolDecoder(IntellitracProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/IntellitracProtocolDecoder.java b/src/main/java/org/traccar/protocol/IntellitracProtocolDecoder.java
new file mode 100644
index 000000000..897606270
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/IntellitracProtocolDecoder.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class IntellitracProtocolDecoder extends BaseProtocolDecoder {
+
+ public IntellitracProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression(".+,").optional()
+ .number("(d+),") // identifier
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+.d+),") // latitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(-?d+.?d*),") // altitude
+ .number("(d+),") // satellites
+ .number("(d+),") // index
+ .number("(d+),") // input
+ .number("(d+),?") // output
+ .number("(d+.d+)?,?") // adc1
+ .number("(d+.d+)?,?") // adc2
+ .groupBegin()
+ .number("d{14},d+,")
+ .number("(d+),") // vss
+ .number("(d+),") // rpm
+ .number("(-?d+),") // coolant
+ .number("(d+),") // fuel
+ .number("(d+),") // fuel consumption
+ .number("(-?d+),") // fuel temperature
+ .number("(d+),") // charger pressure
+ .number("(d+),") // tpl
+ .number("(d+),") // axle weight
+ .number("(d+)") // odometer
+ .groupEnd("?")
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(true);
+ position.setLongitude(parser.nextDouble());
+ position.setLatitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setCourse(parser.nextDouble());
+ position.setAltitude(parser.nextDouble());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_INDEX, parser.nextLong());
+ position.set(Position.KEY_INPUT, parser.nextInt());
+ position.set(Position.KEY_OUTPUT, parser.nextInt());
+
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.PREFIX_ADC + 2, parser.nextDouble());
+
+ // J1939 data
+ position.set(Position.KEY_OBD_SPEED, parser.nextInt());
+ position.set(Position.KEY_RPM, parser.nextInt());
+ position.set("coolant", parser.nextInt());
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt());
+ position.set(Position.KEY_FUEL_CONSUMPTION, parser.nextInt());
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt());
+ position.set("chargerPressure", parser.nextInt());
+ position.set("tpl", parser.nextInt());
+ position.set(Position.KEY_AXLE_WEIGHT, parser.nextInt());
+ position.set(Position.KEY_OBD_ODOMETER, parser.nextInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ItsProtocol.java b/src/main/java/org/traccar/protocol/ItsProtocol.java
new file mode 100644
index 000000000..f53600dc9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ItsProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ItsProtocol extends BaseProtocol {
+
+ public ItsProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '*'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new ItsProtocolDecoder(ItsProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ItsProtocolDecoder.java b/src/main/java/org/traccar/protocol/ItsProtocolDecoder.java
new file mode 100644
index 000000000..482f34e65
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ItsProtocolDecoder.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2018 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class ItsProtocolDecoder extends BaseProtocolDecoder {
+
+ public ItsProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("[^$]*")
+ .text("$")
+ .expression(",?[^,]+,") // event
+ .groupBegin()
+ .expression("[^,]+,") // vendor
+ .expression("[^,]+,") // firmware version
+ .expression("[^,]+,") // type
+ .number("d+,")
+ .expression("[LH],") // history
+ .or()
+ .expression("[^,]+,") // type
+ .groupEnd()
+ .number("(d{15}),") // imei
+ .groupBegin()
+ .expression("(..),") // status
+ .or()
+ .expression("[^,]*,") // vehicle registration
+ .number("([01]),") // valid
+ .groupEnd()
+ .number("(dd),?(dd),?(dddd),") // date (ddmmyyyy)
+ .number("(dd),?(dd),?(dd),") // time (hhmmss)
+ .expression("([AV]),").optional() // valid
+ .number("(d+.d+),([NS]),") // latitude
+ .number("(d+.d+),([EW]),") // longitude
+ .groupBegin()
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(d+),") // satellites
+ .groupBegin()
+ .number("(d+.?d*),") // altitude
+ .number("d+.?d*,") // pdop
+ .number("d+.?d*,") // hdop
+ .expression("[^,]*,")
+ .number("([01]),") // ignition
+ .number("([01]),") // charging
+ .number("(d+.?d*),") // power
+ .number("(d+.?d*),") // battery
+ .number("[01],") // emergency
+ .expression("[CO]?,") // tamper
+ .number("(?:x+,){5}") // main cell
+ .number("(?:-?x+,){12}") // other cells
+ .number("([01]{4}),") // inputs
+ .number("([01]{2}),") // outputs
+ .groupEnd("?")
+ .or()
+ .number("(-?d+.d+),") // altitude
+ .number("(d+.d+),") // speed
+ .groupEnd()
+ .any()
+ .compile();
+
+ private String decodeAlarm(String status) {
+ switch (status) {
+ case "WD":
+ case "EA":
+ return Position.ALARM_SOS;
+ case "BL":
+ return Position.ALARM_LOW_BATTERY;
+ case "HB":
+ return Position.ALARM_BRAKING;
+ case "HA":
+ return Position.ALARM_ACCELERATION;
+ case "RT":
+ return Position.ALARM_CORNERING;
+ case "OS":
+ return Position.ALARM_OVERSPEED;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (channel != null && sentence.startsWith("$,01,")) {
+ channel.writeAndFlush(new NetworkMessage("$,1,*", remoteAddress));
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.next()));
+ }
+
+ if (parser.hasNext()) {
+ position.setValid(parser.nextInt() == 1);
+ }
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ if (parser.hasNext()) {
+ position.setValid(parser.next().equals("A"));
+ }
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+
+ if (parser.hasNext(3)) {
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setCourse(parser.nextDouble());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ }
+
+ if (parser.hasNext(7)) {
+ position.setAltitude(parser.nextDouble());
+ position.set(Position.KEY_IGNITION, parser.nextInt() > 0);
+ position.set(Position.KEY_CHARGE, parser.nextInt() > 0);
+ position.set(Position.KEY_POWER, parser.nextDouble());
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.KEY_INPUT, parser.nextBinInt());
+ position.set(Position.KEY_OUTPUT, parser.nextBinInt());
+ }
+
+ if (parser.hasNext(2)) {
+ position.setAltitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Ivt401Protocol.java b/src/main/java/org/traccar/protocol/Ivt401Protocol.java
new file mode 100644
index 000000000..fb44e4fe9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Ivt401Protocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Ivt401Protocol extends BaseProtocol {
+
+ public Ivt401Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, ';'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Ivt401ProtocolDecoder(Ivt401Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Ivt401ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Ivt401ProtocolDecoder.java
new file mode 100644
index 000000000..63556e7a9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Ivt401ProtocolDecoder.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Ivt401ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Ivt401ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("(")
+ .expression("TL[ABLN],") // header
+ .number("(d+),") // imei
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("([-+]d+.d+),") // latitude
+ .number("([-+]d+.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(-?d+.?d*),") // altitude
+ .number("d+,") // satellites or battery status
+ .number("(d),") // gps status
+ .number("(d+),") // rssi
+ .number("(d+),") // input
+ .number("(d+),") // output
+ .number("(d+.d+),") // adc
+ .number("(d+.d+),") // power
+ .number("(d+.d+),") // battery
+ .number("(-?d+.?d*),") // pcb temp
+ .expression("([^,]+),") // temp
+ .number("(d+),") // movement
+ .number("(d+.d+),") // acceleration
+ .number("(-?d+),") // tilt
+ .number("(d+),") // trip
+ .number("(d+),") // odometer
+ .groupBegin()
+ .number("([01]),") // overspeed
+ .number("[01],") // input 2 misuse
+ .number("[01],") // immobilizer
+ .number("[01],") // temperature alert
+ .number("[0-2]+,") // geofence
+ .number("([0-3]),") // harsh driving
+ .number("[01],") // reconnect
+ .number("([01]),") // low battery
+ .number("([01]),") // power disconnected
+ .number("[01],") // gps failure
+ .number("([01]),") // towing
+ .number("[01],") // server unreachable
+ .number("[128],") // sleep mode
+ .expression("([^,]+)?,") // driver id
+ .number("d+,") // sms count
+ .groupEnd("?")
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt()));
+ position.setCourse(parser.nextInt());
+ position.setAltitude(parser.nextDouble());
+ position.setValid(parser.nextInt() > 0);
+
+ position.set(Position.KEY_RSSI, parser.nextInt());
+
+ String input = parser.next();
+ for (int i = 0; i < input.length(); i++) {
+ int value = Character.getNumericValue(input.charAt(i));
+ if (value < 2) {
+ position.set(Position.PREFIX_IN + (i + 1), value > 0);
+ }
+ }
+
+ String output = parser.next();
+ for (int i = 0; i < output.length(); i++) {
+ position.set(Position.PREFIX_OUT + (i + 1), Character.getNumericValue(output.charAt(i)) > 0);
+ }
+
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.KEY_POWER, parser.nextDouble());
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.KEY_DEVICE_TEMP, parser.nextDouble());
+
+ String temp = parser.next();
+ if (temp.startsWith("M")) {
+ int index = 1;
+ int startIndex = 1;
+ int endIndex;
+ while (startIndex < temp.length()) {
+ endIndex = temp.indexOf('-', startIndex + 1);
+ if (endIndex < 0) {
+ endIndex = temp.indexOf('+', startIndex + 1);
+ }
+ if (endIndex < 0) {
+ endIndex = temp.length();
+ }
+ if (endIndex > 0) {
+ double value = Double.parseDouble(temp.substring(startIndex, endIndex));
+ position.set(Position.PREFIX_TEMP + index++, value);
+ }
+ startIndex = endIndex;
+ }
+ } else {
+ position.set(Position.PREFIX_TEMP + 1, Double.parseDouble(temp));
+ }
+
+ position.set(Position.KEY_MOTION, parser.nextInt() > 0);
+ position.set(Position.KEY_ACCELERATION, parser.nextDouble());
+
+ parser.nextInt(); // tilt
+ parser.nextInt(); // trip state
+
+ position.set(Position.KEY_ODOMETER, parser.nextLong());
+
+ if (parser.hasNext(6)) {
+ position.set(Position.KEY_ALARM, parser.nextInt() == 1 ? Position.ALARM_OVERSPEED : null);
+ switch (parser.nextInt()) {
+ case 1:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ break;
+ case 2:
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ break;
+ case 3:
+ position.set(Position.KEY_ALARM, Position.ALARM_CORNERING);
+ break;
+ default:
+ break;
+ }
+ position.set(Position.KEY_ALARM, parser.nextInt() == 1 ? Position.ALARM_LOW_BATTERY : null);
+ position.set(Position.KEY_ALARM, parser.nextInt() == 1 ? Position.ALARM_POWER_CUT : null);
+ position.set(Position.KEY_ALARM, parser.nextInt() == 1 ? Position.ALARM_TOW : null);
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/JpKorjarFrameDecoder.java b/src/main/java/org/traccar/protocol/JpKorjarFrameDecoder.java
new file mode 100644
index 000000000..0eb65c8ef
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/JpKorjarFrameDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Nyash (nyashh@gmail.com)
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class JpKorjarFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 80) {
+ return null;
+ }
+
+ int spaceIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ' ');
+ if (spaceIndex == -1) {
+ return null;
+ }
+
+ int endIndex = buf.indexOf(spaceIndex, buf.writerIndex(), (byte) ',');
+ if (endIndex == -1) {
+ return null;
+ }
+
+ return buf.readRetainedSlice(endIndex + 1);
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/JpKorjarProtocol.java b/src/main/java/org/traccar/protocol/JpKorjarProtocol.java
new file mode 100644
index 000000000..fe5b2480d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/JpKorjarProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 Nyash (nyashh@gmail.com)
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class JpKorjarProtocol extends BaseProtocol {
+
+ public JpKorjarProtocol() {
+ addServer(new TrackerServer(false, this.getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new JpKorjarFrameDecoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new JpKorjarProtocolDecoder(JpKorjarProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/JpKorjarProtocolDecoder.java b/src/main/java/org/traccar/protocol/JpKorjarProtocolDecoder.java
new file mode 100644
index 000000000..33026918a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/JpKorjarProtocolDecoder.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 Nyash (nyashh@gmail.com)
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class JpKorjarProtocolDecoder extends BaseProtocolDecoder {
+
+ public JpKorjarProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("KORJAR.PL,")
+ .number("(d+),") // imei
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d+.d+)([NS]),") // latitude
+ .number("(d+.d+)([EW]),") // longitude
+ .number("(d+.d+),") // speed
+ .number("(d+),") // course
+ .number("[FL]:(d+.d+)V,") // battery
+ .number("([01]) ") // valid
+ .number("(d+) ") // mcc
+ .number("(d+) ") // mnc
+ .number("(x+) ") // lac
+ .number("(x+),") // cid
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0));
+
+ position.setValid(parser.nextInt(0) == 1);
+
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(0), parser.nextInt(0), parser.nextHexInt(0), parser.nextHexInt(0))));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Jt600FrameDecoder.java b/src/main/java/org/traccar/protocol/Jt600FrameDecoder.java
new file mode 100644
index 000000000..b5d060ecc
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Jt600FrameDecoder.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+import java.text.ParseException;
+
+public class Jt600FrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ char type = (char) buf.getByte(buf.readerIndex());
+
+ if (type == '$') {
+ boolean longFormat = buf.getUnsignedByte(buf.readerIndex() + 1) == 0x75;
+ int length = buf.getUnsignedShort(buf.readerIndex() + (longFormat ? 8 : 7)) + 10;
+ if (length <= buf.readableBytes()) {
+ return buf.readRetainedSlice(length);
+ }
+ } else if (type == '(') {
+ int endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ')');
+ if (endIndex != -1) {
+ return buf.readRetainedSlice(endIndex + 1);
+ }
+ } else {
+ throw new ParseException(null, 0); // unknown message
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Jt600Protocol.java b/src/main/java/org/traccar/protocol/Jt600Protocol.java
new file mode 100644
index 000000000..97c5fa6ce
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Jt600Protocol.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class Jt600Protocol extends BaseProtocol {
+
+ public Jt600Protocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_SET_TIMEZONE,
+ Command.TYPE_REBOOT_DEVICE);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Jt600FrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Jt600ProtocolEncoder());
+ pipeline.addLast(new Jt600ProtocolDecoder(Jt600Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Jt600ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Jt600ProtocolDecoder.java
new file mode 100644
index 000000000..1351706e2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Jt600ProtocolDecoder.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BcdUtil;
+import org.traccar.helper.BitBuffer;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class Jt600ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Jt600ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static double convertCoordinate(int raw) {
+ int degrees = raw / 1000000;
+ double minutes = (raw % 1000000) / 10000.0;
+ return degrees + minutes / 60;
+ }
+
+ private void decodeStatus(Position position, ByteBuf buf) {
+
+ int value = buf.readUnsignedByte();
+
+ position.set(Position.KEY_IGNITION, BitUtil.check(value, 0));
+ position.set(Position.KEY_DOOR, BitUtil.check(value, 6));
+
+ value = buf.readUnsignedByte();
+
+ position.set(Position.KEY_CHARGE, BitUtil.check(value, 0));
+ position.set(Position.KEY_BLOCKED, BitUtil.check(value, 1));
+
+ if (BitUtil.check(value, 2)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ }
+ if (BitUtil.check(value, 3) || BitUtil.check(value, 4)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GPS_ANTENNA_CUT);
+ }
+ if (BitUtil.check(value, 4)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ }
+
+ value = buf.readUnsignedByte();
+
+ if (BitUtil.check(value, 2)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_FATIGUE_DRIVING);
+ }
+ if (BitUtil.check(value, 3)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_TOW);
+ }
+
+ buf.readUnsignedByte(); // reserved
+
+ }
+
+ private List<Position> decodeBinary(ByteBuf buf, Channel channel, SocketAddress remoteAddress) {
+
+ List<Position> positions = new LinkedList<>();
+
+ buf.readByte(); // header
+
+ boolean longFormat = buf.getUnsignedByte(buf.readerIndex()) == 0x75;
+
+ String id = String.valueOf(Long.parseLong(ByteBufUtil.hexDump(buf.readSlice(5))));
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int protocolVersion = 0;
+ if (longFormat) {
+ protocolVersion = buf.readUnsignedByte();
+ }
+
+ int version = BitUtil.from(buf.readUnsignedByte(), 4);
+ buf.readUnsignedShort(); // length
+
+ while (buf.readableBytes() > 1) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDay(BcdUtil.readInteger(buf, 2))
+ .setMonth(BcdUtil.readInteger(buf, 2))
+ .setYear(BcdUtil.readInteger(buf, 2))
+ .setHour(BcdUtil.readInteger(buf, 2))
+ .setMinute(BcdUtil.readInteger(buf, 2))
+ .setSecond(BcdUtil.readInteger(buf, 2));
+ position.setTime(dateBuilder.getDate());
+
+ double latitude = convertCoordinate(BcdUtil.readInteger(buf, 8));
+ double longitude = convertCoordinate(BcdUtil.readInteger(buf, 9));
+
+ byte flags = buf.readByte();
+ position.setValid((flags & 0x1) == 0x1);
+ if ((flags & 0x2) == 0) {
+ latitude = -latitude;
+ }
+ position.setLatitude(latitude);
+ if ((flags & 0x4) == 0) {
+ longitude = -longitude;
+ }
+ position.setLongitude(longitude);
+
+ position.setSpeed(BcdUtil.readInteger(buf, 2));
+ position.setCourse(buf.readUnsignedByte() * 2.0);
+
+ if (longFormat) {
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt() * 1000);
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ buf.readUnsignedInt(); // vehicle id combined
+
+ int status = buf.readUnsignedShort();
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 1) ? Position.ALARM_GEOFENCE_ENTER : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 2) ? Position.ALARM_GEOFENCE_EXIT : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 3) ? Position.ALARM_POWER_CUT : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 4) ? Position.ALARM_VIBRATION : null);
+ position.set(Position.KEY_BLOCKED, BitUtil.check(status, 7));
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 8 + 3) ? Position.ALARM_LOW_BATTERY : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 8 + 6) ? Position.ALARM_FAULT : null);
+ position.set(Position.KEY_STATUS, status);
+
+ int battery = buf.readUnsignedByte();
+ if (battery == 0xff) {
+ position.set(Position.KEY_CHARGE, true);
+ } else {
+ position.set(Position.KEY_BATTERY_LEVEL, battery);
+ }
+
+ CellTower cellTower = CellTower.fromCidLac(buf.readUnsignedShort(), buf.readUnsignedShort());
+ cellTower.setSignalStrength((int) buf.readUnsignedByte());
+ position.setNetwork(new Network(cellTower));
+
+ if (protocolVersion == 0x17) {
+ buf.readUnsignedByte(); // geofence id
+ buf.skipBytes(3); // reserved
+ }
+
+ } else if (version == 1) {
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_POWER, buf.readUnsignedByte());
+
+ buf.readByte(); // other flags and sensors
+
+ position.setAltitude(buf.readUnsignedShort());
+
+ int cid = buf.readUnsignedShort();
+ int lac = buf.readUnsignedShort();
+ int rssi = buf.readUnsignedByte();
+
+ if (cid != 0 && lac != 0) {
+ CellTower cellTower = CellTower.fromCidLac(cid, lac);
+ cellTower.setSignalStrength(rssi);
+ position.setNetwork(new Network(cellTower));
+ } else {
+ position.set(Position.KEY_RSSI, rssi);
+ }
+
+ } else if (version == 2) {
+
+ int fuel = buf.readUnsignedByte() << 8;
+
+ decodeStatus(position, buf);
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt() * 1000);
+
+ fuel += buf.readUnsignedByte();
+ position.set(Position.KEY_FUEL_LEVEL, fuel);
+
+ } else if (version == 3) {
+
+ BitBuffer bitBuffer = new BitBuffer(buf);
+
+ position.set("fuel1", bitBuffer.readUnsigned(12));
+ position.set("fuel2", bitBuffer.readUnsigned(12));
+ position.set("fuel3", bitBuffer.readUnsigned(12));
+ position.set(Position.KEY_ODOMETER, bitBuffer.readUnsigned(20) * 1000);
+
+ int status = bitBuffer.readUnsigned(24);
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 0));
+ position.set(Position.KEY_STATUS, status);
+
+ }
+
+ positions.add(position);
+
+ }
+
+ buf.readUnsignedByte(); // index
+
+ return positions;
+ }
+
+ private static final Pattern PATTERN_W01 = new PatternBuilder()
+ .text("(")
+ .number("(d+),") // id
+ .text("W01,") // type
+ .number("(ddd)(dd.dddd),") // longitude
+ .expression("([EW]),")
+ .number("(dd)(dd.dddd),") // latitude
+ .expression("([NS]),")
+ .expression("([AV]),") // validity
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // power
+ .number("(d+),") // gps signal
+ .number("(d+),") // gsm signal
+ .number("(d+),") // alert type
+ .any()
+ .compile();
+
+ private Position decodeW01(String sentence, Channel channel, SocketAddress remoteAddress) {
+
+ Parser parser = new Parser(PATTERN_W01, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setLongitude(parser.nextCoordinate());
+ position.setLatitude(parser.nextCoordinate());
+ position.setValid(parser.next().equals("A"));
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_POWER, parser.nextDouble(0));
+ position.set(Position.KEY_GPS, parser.nextInt(0));
+ position.set(Position.KEY_RSSI, parser.nextInt(0));
+ position.set("alertType", parser.nextInt(0));
+
+ return position;
+ }
+
+ private static final Pattern PATTERN_U01 = new PatternBuilder()
+ .text("(")
+ .number("(d+),") // id
+ .number("(Udd),") // type
+ .number("d+,").optional() // alarm
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([TF]),") // validity
+ .number("(d+.d+),([NS]),") // latitude
+ .number("(d+.d+),([EW]),") // longitude
+ .number("(d+.?d*),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // satellites
+ .number("(d+)%,") // battery
+ .expression("([01]+),") // status
+ .number("(d+),") // cid
+ .number("(d+),") // lac
+ .number("(d+),") // gsm signal
+ .number("(d+),") // odometer
+ .number("(d+)") // serial number
+ .number(",(xx)").optional() // checksum
+ .any()
+ .compile();
+
+ private Position decodeU01(String sentence, Channel channel, SocketAddress remoteAddress) {
+
+ Parser parser = new Parser(PATTERN_U01, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ String type = parser.next();
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setValid(parser.next().equals("T"));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+
+ position.setSpeed(UnitsConverter.knotsFromMph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt(0));
+ position.set(Position.KEY_STATUS, parser.nextBinInt(0));
+
+ CellTower cellTower = CellTower.fromCidLac(parser.nextInt(0), parser.nextInt(0));
+ cellTower.setSignalStrength(parser.nextInt(0));
+ position.setNetwork(new Network(cellTower));
+
+ position.set(Position.KEY_ODOMETER, parser.nextLong(0) * 1000);
+ position.set(Position.KEY_INDEX, parser.nextInt(0));
+
+ if (channel != null) {
+ if (type.equals("U01") || type.equals("U02") || type.equals("U03")) {
+ channel.writeAndFlush(new NetworkMessage("(S39)", remoteAddress));
+ } else if (type.equals("U06")) {
+ channel.writeAndFlush(new NetworkMessage("(S20)", remoteAddress));
+ }
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+ char first = (char) buf.getByte(0);
+
+ if (first == '$') {
+ return decodeBinary(buf, channel, remoteAddress);
+ } else if (first == '(') {
+ String sentence = buf.toString(StandardCharsets.US_ASCII);
+ if (sentence.contains("W01")) {
+ return decodeW01(sentence, channel, remoteAddress);
+ } else {
+ return decodeU01(sentence, channel, remoteAddress);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Jt600ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Jt600ProtocolEncoder.java
new file mode 100644
index 000000000..fe5c63c32
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Jt600ProtocolEncoder.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import java.util.TimeZone;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class Jt600ProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_ENGINE_STOP:
+ return "(S07,0)";
+ case Command.TYPE_ENGINE_RESUME:
+ return "(S07,1)";
+ case Command.TYPE_SET_TIMEZONE:
+ int offset = TimeZone.getTimeZone(command.getString(Command.KEY_TIMEZONE)).getRawOffset() / 60000;
+ return "(S09,1," + offset + ")";
+ case Command.TYPE_REBOOT_DEVICE:
+ return "(S17)";
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/KenjiProtocol.java b/src/main/java/org/traccar/protocol/KenjiProtocol.java
new file mode 100644
index 000000000..90c0c511c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/KenjiProtocol.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 Carlos Alvarez (carlos.alvarez.rozas@gmail.com)
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class KenjiProtocol extends BaseProtocol {
+
+ public KenjiProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new KenjiProtocolDecoder(KenjiProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/KenjiProtocolDecoder.java b/src/main/java/org/traccar/protocol/KenjiProtocolDecoder.java
new file mode 100644
index 000000000..63812242a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/KenjiProtocolDecoder.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class KenjiProtocolDecoder extends BaseProtocolDecoder {
+
+ public KenjiProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text(">")
+ .number("C(d{6}),") // device id
+ .number("M(x{6}),") // alarm
+ .number("O(x{4}),") // output
+ .number("I(x{4}),") // input
+ .number("D(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AV]),") // valid
+ .number("([NS])(dd)(dd.d+),") // latitude
+ .number("([EW])(ddd)(dd.d+),") // longitude
+ .number("T(d+.d+),") // speed
+ .number("H(d+.d+),") // course
+ .number("Y(dd)(dd)(dd),") // date (ddmmyy)
+ .number("G(d+)") // satellites
+ .any()
+ .compile();
+
+ private String decodeAlarm(int value) {
+ if (BitUtil.check(value, 2)) {
+ return Position.ALARM_SOS;
+ }
+ if (BitUtil.check(value, 4)) {
+ return Position.ALARM_LOW_BATTERY;
+ }
+ if (BitUtil.check(value, 6)) {
+ return Position.ALARM_MOVEMENT;
+ }
+ if (BitUtil.check(value, 1) || BitUtil.check(value, 10) || BitUtil.check(value, 11)) {
+ return Position.ALARM_VIBRATION;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.nextHexInt(0)));
+ position.set(Position.KEY_OUTPUT, parser.nextHexInt(0));
+ position.set(Position.KEY_INPUT, parser.nextHexInt(0));
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/KhdProtocol.java b/src/main/java/org/traccar/protocol/KhdProtocol.java
new file mode 100644
index 000000000..cec7158ed
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/KhdProtocol.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class KhdProtocol extends BaseProtocol {
+
+ public KhdProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(512, 3, 2));
+ pipeline.addLast(new KhdProtocolEncoder());
+ pipeline.addLast(new KhdProtocolDecoder(KhdProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/KhdProtocolDecoder.java b/src/main/java/org/traccar/protocol/KhdProtocolDecoder.java
new file mode 100644
index 000000000..0dd5b085a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/KhdProtocolDecoder.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BcdUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class KhdProtocolDecoder extends BaseProtocolDecoder {
+
+ public KhdProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private String readSerialNumber(ByteBuf buf) {
+ int b1 = buf.readUnsignedByte();
+ int b2 = buf.readUnsignedByte() - 0x80;
+ int b3 = buf.readUnsignedByte() - 0x80;
+ int b4 = buf.readUnsignedByte();
+ return String.format("%02d%02d%02d%02d", b1, b2, b3, b4);
+ }
+
+ public static final int MSG_LOGIN = 0xB1;
+ public static final int MSG_CONFIRMATION = 0x21;
+ public static final int MSG_ON_DEMAND = 0x81;
+ public static final int MSG_POSITION_UPLOAD = 0x80;
+ public static final int MSG_POSITION_REUPLOAD = 0x8E;
+ public static final int MSG_ALARM = 0x82;
+ public static final int MSG_ADMIN_NUMBER = 0x83;
+ public static final int MSG_SEND_TEXT = 0x84;
+ public static final int MSG_REPLY = 0x85;
+ public static final int MSG_SMS_ALARM_SWITCH = 0x86;
+ public static final int MSG_PERIPHERAL = 0xA3;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ int type = buf.readUnsignedByte();
+ buf.readUnsignedShort(); // size
+
+ if (type == MSG_LOGIN || type == MSG_ADMIN_NUMBER || type == MSG_SEND_TEXT
+ || type == MSG_SMS_ALARM_SWITCH || type == MSG_POSITION_REUPLOAD) {
+
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(0x29);
+ response.writeByte(0x29); // header
+ response.writeByte(MSG_CONFIRMATION);
+ response.writeShort(5); // size
+ response.writeByte(buf.getByte(buf.writerIndex() - 2));
+ response.writeByte(type);
+ response.writeByte(buf.writerIndex() > 9 ? buf.getByte(9) : 0); // 10th byte
+ response.writeByte(Checksum.xor(response.nioBuffer()));
+ response.writeByte(0x0D); // ending
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+
+ }
+
+ if (type == MSG_ON_DEMAND || type == MSG_POSITION_UPLOAD || type == MSG_POSITION_REUPLOAD
+ || type == MSG_ALARM || type == MSG_REPLY || type == MSG_PERIPHERAL) {
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, readSerialNumber(buf));
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setYear(BcdUtil.readInteger(buf, 2))
+ .setMonth(BcdUtil.readInteger(buf, 2))
+ .setDay(BcdUtil.readInteger(buf, 2))
+ .setHour(BcdUtil.readInteger(buf, 2))
+ .setMinute(BcdUtil.readInteger(buf, 2))
+ .setSecond(BcdUtil.readInteger(buf, 2));
+ position.setTime(dateBuilder.getDate());
+
+ position.setLatitude(BcdUtil.readCoordinate(buf));
+ position.setLongitude(BcdUtil.readCoordinate(buf));
+ position.setSpeed(UnitsConverter.knotsFromKph(BcdUtil.readInteger(buf, 4)));
+ position.setCourse(BcdUtil.readInteger(buf, 4));
+ position.setValid((buf.readUnsignedByte() & 0x80) != 0);
+
+ if (type != MSG_ALARM) {
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedMedium());
+ position.set(Position.KEY_STATUS, buf.readUnsignedInt());
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte());
+ position.set(Position.KEY_VDOP, buf.readUnsignedByte());
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ buf.skipBytes(5); // other location data
+
+ if (type == MSG_PERIPHERAL) {
+
+ buf.readUnsignedShort(); // data length
+
+ int dataType = buf.readUnsignedByte();
+
+ buf.readUnsignedByte(); // content length
+
+ switch (dataType) {
+ case 0x01:
+ position.set(Position.KEY_FUEL_LEVEL,
+ buf.readUnsignedByte() * 100 + buf.readUnsignedByte());
+ break;
+ case 0x02:
+ position.set(Position.PREFIX_TEMP + 1,
+ buf.readUnsignedByte() * 100 + buf.readUnsignedByte());
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ }
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/KhdProtocolEncoder.java b/src/main/java/org/traccar/protocol/KhdProtocolEncoder.java
new file mode 100644
index 000000000..c66129283
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/KhdProtocolEncoder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+public class KhdProtocolEncoder extends BaseProtocolEncoder {
+
+ public static final int MSG_CUT_OIL = 0x39;
+ public static final int MSG_RESUME_OIL = 0x38;
+
+ private ByteBuf encodeCommand(int command, String uniqueId) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeByte(0x29);
+ buf.writeByte(0x29);
+
+ buf.writeByte(command);
+ buf.writeShort(6); // size
+
+ uniqueId = "00000000".concat(uniqueId);
+ uniqueId = uniqueId.substring(uniqueId.length() - 8);
+ buf.writeByte(Integer.parseInt(uniqueId.substring(0, 2)));
+ buf.writeByte(Integer.parseInt(uniqueId.substring(2, 4)) + 0x80);
+ buf.writeByte(Integer.parseInt(uniqueId.substring(4, 6)) + 0x80);
+ buf.writeByte(Integer.parseInt(uniqueId.substring(6, 8)));
+
+ buf.writeByte(Checksum.xor(buf.nioBuffer()));
+ buf.writeByte(0x0D); // ending
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ String uniqueId = getUniqueId(command.getDeviceId());
+
+ switch (command.getType()) {
+ case Command.TYPE_ENGINE_STOP:
+ return encodeCommand(MSG_CUT_OIL, uniqueId);
+ case Command.TYPE_ENGINE_RESUME:
+ return encodeCommand(MSG_RESUME_OIL, uniqueId);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/L100FrameDecoder.java b/src/main/java/org/traccar/protocol/L100FrameDecoder.java
new file mode 100644
index 000000000..158461895
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/L100FrameDecoder.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+import java.nio.charset.StandardCharsets;
+
+public class L100FrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ if (buf.getCharSequence(buf.readerIndex(), 4, StandardCharsets.US_ASCII).toString().equals("ATL,")) {
+ return decodeNew(buf);
+ } else {
+ return decodeOld(buf);
+ }
+ }
+
+ private Object decodeOld(ByteBuf buf) {
+
+ int header = buf.getByte(buf.readerIndex());
+ boolean obd = header == 'L' || header == 'H';
+
+ int index;
+ if (obd) {
+ index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '*');
+ } else {
+ index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0x02);
+ if (index < 0) {
+ index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0x04);
+ if (index < 0) {
+ return null;
+ }
+ }
+ }
+
+ index += 2; // checksum
+
+ if (buf.writerIndex() >= index) {
+ if (!obd) {
+ buf.skipBytes(2); // header
+ }
+ ByteBuf frame = buf.readRetainedSlice(index - buf.readerIndex() - 2);
+ buf.skipBytes(2); // footer
+ return frame;
+ }
+
+ return null;
+ }
+
+ private Object decodeNew(ByteBuf buf) {
+
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '@');
+ if (index < 0) {
+ return null;
+ }
+
+ if (buf.writerIndex() >= index + 1) {
+ ByteBuf frame = buf.readRetainedSlice(index - buf.readerIndex());
+ buf.skipBytes(1); // delimiter
+ return frame;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/L100Protocol.java b/src/main/java/org/traccar/protocol/L100Protocol.java
new file mode 100644
index 000000000..942029307
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/L100Protocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class L100Protocol extends BaseProtocol {
+
+ public L100Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new L100FrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new L100ProtocolDecoder(L100Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/L100ProtocolDecoder.java b/src/main/java/org/traccar/protocol/L100ProtocolDecoder.java
new file mode 100644
index 000000000..9868de435
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/L100ProtocolDecoder.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.ObdDecoder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class L100ProtocolDecoder extends BaseProtocolDecoder {
+
+ public L100ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("ATL")
+ .expression(",[^,]+,").optional()
+ .number("(d{15}),") // imei
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd)") // time (hhmmss.sss)
+ .number(".(ddd)").optional()
+ .expression(",([AV]),") // validity
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .any()
+ .text("#")
+ .number("([01]+),") // io status
+ .number("(d+.?d*|N.C),") // adc
+ .expression("[^,]*,") // reserved
+ .expression("[^,]*,") // reserved
+ .number("(d+.?d*),") // odometer
+ .number("(d+.?d*),") // temperature
+ .number("(d+.?d*),") // battery
+ .number("(d+),") // rssi
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(x+),") // lac
+ .number("(x+)") // cid
+ .any()
+ .text("ATL")
+ .compile();
+
+ private static final Pattern PATTERN_OBD_LOCATION = new PatternBuilder()
+ .expression("[LH],") // archive
+ .text("ATL,")
+ .number("(d{15}),") // imei
+ .number("(d+),") // type
+ .number("(d+),") // index
+ .groupBegin()
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .expression("([AV]),") // validity
+ .number("(d+.d+);([NS]),") // latitude
+ .number("(d+.d+);([EW]),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+.d+),") // odometer
+ .number("(d+.d+),") // battery
+ .number("(d+),") // rssi
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(d+),") // lac
+ .number("(x+),") // cid
+ .number("#(d)(d)(d)(d),") // status
+ .number("(d),") // overspeed
+ .text("ATL,")
+ .groupEnd("?")
+ .compile();
+
+ private static final Pattern PATTERN_OBD_DATA = new PatternBuilder()
+ .expression("[LH],") // archive
+ .text("ATLOBD,")
+ .number("(d{15}),") // imei
+ .number("d+,") // type
+ .number("d+,") // index
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .expression("[^,]+,") // obd protocol
+ .expression("(.+)") // data
+ .compile();
+
+ private static final Pattern PATTERN_NEW = new PatternBuilder()
+ .groupBegin()
+ .text("ATL,")
+ .expression("[LH],") // archive
+ .number("(d{15}),") // imei
+ .groupEnd("?")
+ .expression("([NPT]),") // alarm
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(d+.d+),([NS]),") // latitude
+ .number("(d+.d+),([EW]),") // longitude
+ .number("(d+.?d*),") // speed
+ .expression("(?:GPS|GSM|INV),")
+ .number("(d+),") // battery
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(d+),") // lac
+ .number("(d+)") // cid
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("L") || sentence.startsWith("H")) {
+ if (sentence.substring(2, 8).equals("ATLOBD")) {
+ return decodeObdData(channel, remoteAddress, sentence);
+ } else {
+ return decodeObdLocation(channel, remoteAddress, sentence);
+ }
+ } else if (!sentence.contains("$GPRMC")) {
+ return decodeNew(channel, remoteAddress, sentence);
+ } else {
+ return decodeNormal(channel, remoteAddress, sentence);
+ }
+ }
+
+ private Object decodeNormal(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt(), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_STATUS, parser.next());
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.KEY_ODOMETER, parser.nextDouble());
+ position.set(Position.PREFIX_TEMP + 1, parser.nextDouble());
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+
+ int rssi = parser.nextInt();
+ if (rssi > 0) {
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(), parser.nextInt(), parser.nextHexInt(), parser.nextHexInt(), rssi)));
+ }
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(String.valueOf((char) 0x01), remoteAddress));
+ }
+
+ return position;
+ }
+
+ private Object decodeObdLocation(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_OBD_LOCATION, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String imei = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type = parser.nextInt();
+ int index = parser.nextInt();
+
+ if (type == 1) {
+ if (channel != null) {
+ String response = "@" + imei + ",00," + index + ",";
+ response += "*" + (char) Checksum.xor(response);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setSpeed(parser.nextInt());
+ position.setCourse(parser.nextInt());
+
+ position.set(Position.KEY_ODOMETER, parser.nextDouble() * 1000);
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+
+ int rssi = parser.nextInt();
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(), parser.nextInt(), parser.nextInt(), parser.nextHexInt(), rssi)));
+
+ position.set(Position.KEY_IGNITION, parser.nextInt() == 1);
+ parser.next(); // reserved
+
+ switch (parser.nextInt()) {
+ case 0:
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ break;
+ case 2:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ break;
+ case 1:
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ break;
+ default:
+ break;
+ }
+
+ position.set(Position.KEY_CHARGE, parser.nextInt() == 1);
+
+ if (parser.nextInt() == 1) {
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ }
+
+ return position;
+ }
+
+ private Object decodeObdData(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_OBD_DATA, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ for (String entry : parser.next().split(",")) {
+ String[] values = entry.split(":");
+ if (values.length == 2 && values[1].charAt(0) != 'X') {
+ position.add(ObdDecoder.decodeData(
+ Integer.parseInt(values[0].substring(2), 16), Integer.parseInt(values[1], 16), true));
+ }
+ }
+
+ return position;
+ }
+
+ private Object decodeNew(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_NEW, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String imei = parser.next();
+ DeviceSession deviceSession;
+ if (imei != null) {
+ deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ } else {
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ }
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ switch (parser.next()) {
+ case "P":
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ break;
+ case "T":
+ position.set(Position.KEY_ALARM, Position.ALARM_TAMPERING);
+ break;
+ default:
+ break;
+ }
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setSpeed(parser.nextDouble());
+
+ position.set(Position.KEY_BATTERY, parser.nextInt() * 0.001);
+
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(), parser.nextInt(), parser.nextInt(), parser.nextHexInt())));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/LaipacProtocol.java b/src/main/java/org/traccar/protocol/LaipacProtocol.java
new file mode 100644
index 000000000..923b08a16
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/LaipacProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class LaipacProtocol extends BaseProtocol {
+
+ public LaipacProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new LaipacProtocolDecoder(LaipacProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/LaipacProtocolDecoder.java b/src/main/java/org/traccar/protocol/LaipacProtocolDecoder.java
new file mode 100644
index 000000000..2f3cbb1b9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/LaipacProtocolDecoder.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class LaipacProtocolDecoder extends BaseProtocolDecoder {
+
+ public LaipacProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$AVRMC,")
+ .expression("([^,]+),") // identifier
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AVRPavrp]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .number("([EW]),")
+ .number("(d+.d+),") // speed
+ .number("(d+.d+),") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .expression("([abZXTSMHFE86430]),") // event code
+ .expression("([\\d.]+),") // battery voltage
+ .number("(d+),") // current mileage
+ .number("(d),") // gps status
+ .number("(d+),") // adc1
+ .number("(d+)") // adc2
+ .number(",(xxxx)") // lac
+ .number("(xxxx),") // cid
+ .number("(ddd)") // mcc
+ .number("(ddd)") // mnc
+ .optional(4)
+ .text("*")
+ .number("(xx)") // checksum
+ .compile();
+
+ private String decodeAlarm(String event) {
+ switch (event) {
+ case "Z":
+ return Position.ALARM_LOW_BATTERY;
+ case "X":
+ return Position.ALARM_GEOFENCE_ENTER;
+ case "T":
+ return Position.ALARM_TAMPERING;
+ case "H":
+ return Position.ALARM_POWER_OFF;
+ case "8":
+ return Position.ALARM_SHOCK;
+ case "7":
+ case "4":
+ return Position.ALARM_GEOFENCE_EXIT;
+ case "6":
+ return Position.ALARM_OVERSPEED;
+ case "3":
+ return Position.ALARM_SOS;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("$ECHK") && channel != null) {
+ channel.writeAndFlush(new NetworkMessage(sentence + "\r\n", remoteAddress)); // heartbeat
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ String status = parser.next();
+ String upperCaseStatus = status.toUpperCase();
+ position.setValid(upperCaseStatus.equals("A") || upperCaseStatus.equals("R") || upperCaseStatus.equals("P"));
+ position.set(Position.KEY_STATUS, status);
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ String event = parser.next();
+ position.set(Position.KEY_ALARM, decodeAlarm(event));
+ position.set(Position.KEY_EVENT, event);
+ position.set(Position.KEY_BATTERY, Double.parseDouble(parser.next().replaceAll("\\.", "")) * 0.001);
+ position.set(Position.KEY_ODOMETER, parser.nextDouble());
+ position.set(Position.KEY_GPS, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble() * 0.001);
+ position.set(Position.PREFIX_ADC + 2, parser.nextDouble() * 0.001);
+
+ Integer lac = parser.nextHexInt();
+ Integer cid = parser.nextHexInt();
+ Integer mcc = parser.nextInt();
+ Integer mnc = parser.nextInt();
+ if (lac != null && cid != null && mcc != null && mnc != null) {
+ position.setNetwork(new Network(CellTower.from(mcc, mnc, lac, cid)));
+ }
+
+ String checksum = parser.next();
+
+ if (channel != null) {
+ if (event.equals("3")) {
+ channel.writeAndFlush(new NetworkMessage("$AVCFG,00000000,d*31\r\n", remoteAddress));
+ } else if (event.equals("X") || event.equals("4")) {
+ channel.writeAndFlush(new NetworkMessage("$AVCFG,00000000,x*2D\r\n", remoteAddress));
+ } else if (event.equals("Z")) {
+ channel.writeAndFlush(new NetworkMessage("$AVCFG,00000000,z*2F\r\n", remoteAddress));
+ } else if (Character.isLowerCase(status.charAt(0))) {
+ String response = "$EAVACK," + event + "," + checksum;
+ response += Checksum.nmea(response) + "\r\n";
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/M2cProtocol.java b/src/main/java/org/traccar/protocol/M2cProtocol.java
new file mode 100644
index 000000000..9de8526c3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/M2cProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class M2cProtocol extends BaseProtocol {
+
+ public M2cProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(32 * 1024, ']'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new M2cProtocolDecoder(M2cProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/M2cProtocolDecoder.java b/src/main/java/org/traccar/protocol/M2cProtocolDecoder.java
new file mode 100644
index 000000000..1460bb176
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/M2cProtocolDecoder.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class M2cProtocolDecoder extends BaseProtocolDecoder {
+
+ public M2cProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("#M2C,")
+ .expression("[^,]+,") // model
+ .expression("[^,]+,") // firmware
+ .number("d+,") // protocol
+ .number("(d+),") // imei
+ .number("(d+),") // index
+ .expression("([LH]),") // archive
+ .number("d+,") // priority
+ .number("(d+),") // event
+ .number("(dd)(dd)(dd),") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+),") // altitude
+ .number("(d+),") // course
+ .number("(d+.d+),") // speed
+ .number("(d+),") // satellites
+ .number("(d+),") // odometer
+ .number("(d+),") // input
+ .number("(d+),") // output
+ .number("(d+),") // power
+ .number("(d+),") // battery
+ .number("(d+),") // adc 1
+ .number("(d+),") // adc 2
+ .number("(d+.?d*),") // temperature
+ .any()
+ .compile();
+
+ private Position decodePosition(Channel channel, SocketAddress remoteAddress, String line) {
+
+ Parser parser = new Parser(PATTERN, line);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_INDEX, parser.nextInt());
+
+ if (parser.next().equals("H")) {
+ position.set(Position.KEY_ARCHIVE, true);
+ }
+
+ position.set(Position.KEY_EVENT, parser.nextInt());
+
+ position.setValid(true);
+ position.setTime(parser.nextDateTime());
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setAltitude(parser.nextInt());
+ position.setCourse(parser.nextInt());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_ODOMETER, parser.nextLong());
+ position.set(Position.KEY_INPUT, parser.nextInt());
+ position.set(Position.KEY_OUTPUT, parser.nextInt());
+ position.set(Position.KEY_POWER, parser.nextInt() * 0.001);
+ position.set(Position.KEY_BATTERY, parser.nextInt() * 0.001);
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 2, parser.nextInt());
+ position.set(Position.PREFIX_TEMP + 1, parser.nextDouble());
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ sentence = sentence.substring(1); // remove start symbol
+
+ List<Position> positions = new LinkedList<>();
+ for (String line : sentence.split("\r\n")) {
+ if (!line.isEmpty()) {
+ Position position = decodePosition(channel, remoteAddress, line);
+ if (position != null) {
+ positions.add(position);
+ }
+ }
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/M2mProtocol.java b/src/main/java/org/traccar/protocol/M2mProtocol.java
new file mode 100644
index 000000000..dda328a59
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/M2mProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.FixedLengthFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class M2mProtocol extends BaseProtocol {
+
+ public M2mProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new FixedLengthFrameDecoder(23));
+ pipeline.addLast(new M2mProtocolDecoder(M2mProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/M2mProtocolDecoder.java b/src/main/java/org/traccar/protocol/M2mProtocolDecoder.java
new file mode 100644
index 000000000..21e4a2fd0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/M2mProtocolDecoder.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class M2mProtocolDecoder extends BaseProtocolDecoder {
+
+ public M2mProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private boolean firstPacket = true;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ // Remove offset
+ for (int i = 0; i < buf.readableBytes(); i++) {
+ int b = buf.getByte(i);
+ if (b != 0x0b) {
+ buf.setByte(i, b - 0x20);
+ }
+ }
+
+ if (firstPacket) {
+
+ firstPacket = false;
+
+ StringBuilder imei = new StringBuilder();
+ for (int i = 0; i < 8; i++) {
+ int b = buf.readByte();
+ if (i != 0) {
+ imei.append(b / 10);
+ }
+ imei.append(b % 10);
+ }
+
+ getDeviceSession(channel, remoteAddress, imei.toString());
+
+ } else {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDay(buf.readUnsignedByte() & 0x3f)
+ .setMonth(buf.readUnsignedByte() & 0x3f)
+ .setYear(buf.readUnsignedByte())
+ .setHour(buf.readUnsignedByte() & 0x3f)
+ .setMinute(buf.readUnsignedByte() & 0x7f)
+ .setSecond(buf.readUnsignedByte() & 0x7f);
+ position.setTime(dateBuilder.getDate());
+
+ int degrees = buf.readUnsignedByte();
+ double latitude = buf.readUnsignedByte();
+ latitude += buf.readUnsignedByte() / 100.0;
+ latitude += buf.readUnsignedByte() / 10000.0;
+ latitude /= 60;
+ latitude += degrees;
+
+ int b = buf.readUnsignedByte();
+
+ degrees = (b & 0x7f) * 100 + buf.readUnsignedByte();
+ double longitude = buf.readUnsignedByte();
+ longitude += buf.readUnsignedByte() / 100.0;
+ longitude += buf.readUnsignedByte() / 10000.0;
+ longitude /= 60;
+ longitude += degrees;
+
+ if ((b & 0x80) != 0) {
+ longitude = -longitude;
+ }
+ if ((b & 0x40) != 0) {
+ latitude = -latitude;
+ }
+
+ position.setValid(true);
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+ position.setSpeed(buf.readUnsignedByte());
+
+ int satellites = buf.readUnsignedByte();
+ if (satellites == 0) {
+ return null; // cell information
+ }
+ position.set(Position.KEY_SATELLITES, satellites);
+
+ // decode other data
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MaestroProtocol.java b/src/main/java/org/traccar/protocol/MaestroProtocol.java
new file mode 100644
index 000000000..87453ce7d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MaestroProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.FixedLengthFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class MaestroProtocol extends BaseProtocol {
+
+ public MaestroProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new FixedLengthFrameDecoder(160));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new MaestroProtocolDecoder(MaestroProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MaestroProtocolDecoder.java b/src/main/java/org/traccar/protocol/MaestroProtocolDecoder.java
new file mode 100644
index 000000000..37b097414
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MaestroProtocolDecoder.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class MaestroProtocolDecoder extends BaseProtocolDecoder {
+
+ public MaestroProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("@")
+ .number("(d+),") // imei
+ .number("d+,") // index
+ .expression("[^,]+,") // profile
+ .expression("([01]),") // validity
+ .number("(d+.d+),") // battery
+ .number("(d+),") // gsm
+ .expression("([01]),") // starter
+ .expression("([01]),") // ignition
+ .number("(dd)/(dd)/(dd),") // date (yy/mm/dd)
+ .number("(dd):(dd):(dd),") // time (hh:mm:ss)
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+.d+),") // latitude
+ .number("(d+.?d*),") // altitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(d+),") // satellites
+ .number("(d+.?d*),") // hdop
+ .number("(d+.?d*)") // odometer
+ .number(",(d+)").optional() // adc
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.nextInt(0) == 1);
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0));
+ position.set(Position.KEY_RSSI, parser.nextInt(0));
+ position.set(Position.KEY_CHARGE, parser.nextInt(0) == 1);
+ position.set(Position.KEY_IGNITION, parser.nextInt(0) == 1);
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromMph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_HDOP, parser.nextDouble(0));
+ position.set(Position.KEY_ODOMETER, parser.nextDouble(0) * 1609.34);
+
+ if (parser.hasNext()) {
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt(0));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ManPowerProtocol.java b/src/main/java/org/traccar/protocol/ManPowerProtocol.java
new file mode 100644
index 000000000..49d8b1e9f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ManPowerProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ManPowerProtocol extends BaseProtocol {
+
+ public ManPowerProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, ';'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new ManPowerProtocolDecoder(ManPowerProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ManPowerProtocolDecoder.java b/src/main/java/org/traccar/protocol/ManPowerProtocolDecoder.java
new file mode 100644
index 000000000..2c7b7eb40
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ManPowerProtocolDecoder.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class ManPowerProtocolDecoder extends BaseProtocolDecoder {
+
+ public ManPowerProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("simei:")
+ .number("(d+),") // imei
+ .expression("[^,]*,[^,]*,")
+ .expression("([^,]*),") // status
+ .number("d+,d+,d+.?d*,")
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.dddd),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.dddd),") // longitude
+ .expression("([EW])?,")
+ .number("(d+.?d*),") // speed
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_STATUS, parser.next());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MegastekFrameDecoder.java b/src/main/java/org/traccar/protocol/MegastekFrameDecoder.java
new file mode 100644
index 000000000..347fa24b1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MegastekFrameDecoder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+import org.traccar.helper.BufferUtil;
+
+import java.nio.charset.StandardCharsets;
+
+public class MegastekFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ if (Character.isDigit(buf.getByte(buf.readerIndex()))) {
+ int length = 4 + Integer.parseInt(buf.toString(buf.readerIndex(), 4, StandardCharsets.US_ASCII));
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+ } else {
+ while (buf.getByte(buf.readerIndex()) == '\r' || buf.getByte(buf.readerIndex()) == '\n') {
+ buf.skipBytes(1);
+ }
+ int delimiter = BufferUtil.indexOf("\r\n", buf);
+ if (delimiter == -1) {
+ delimiter = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '!');
+ }
+ if (delimiter != -1) {
+ ByteBuf result = buf.readRetainedSlice(delimiter - buf.readerIndex());
+ buf.skipBytes(1);
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MegastekProtocol.java b/src/main/java/org/traccar/protocol/MegastekProtocol.java
new file mode 100644
index 000000000..e9f5f9fde
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MegastekProtocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class MegastekProtocol extends BaseProtocol {
+
+ public MegastekProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new MegastekFrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new MegastekProtocolDecoder(MegastekProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MegastekProtocolDecoder.java b/src/main/java/org/traccar/protocol/MegastekProtocolDecoder.java
new file mode 100644
index 000000000..d81cc0eda
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MegastekProtocolDecoder.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class MegastekProtocolDecoder extends BaseProtocolDecoder {
+
+ public MegastekProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_GPRMC = new PatternBuilder()
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).(ddd),") // time (hhmmss.sss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),([NS]),") // latitude
+ .number("(d+)(dd.d+),([EW]),") // longitude
+ .number("(d+.d+)?,") // speed
+ .number("(d+.d+)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any() // checksum
+ .compile();
+
+ private static final Pattern PATTERN_SIMPLE = new PatternBuilder()
+ .expression("[FL],") // flag
+ .expression("([^,]*),") // alarm
+ .number("imei:(d+),") // imei
+ .number("(d+/?d*)?,") // satellites
+ .number("(d+.d+)?,") // altitude
+ .number("Battery=(d+)%,,?") // battery
+ .number("(d)?,") // charger
+ .number("(d+)?,") // mcc
+ .number("(d+)?,") // mnc
+ .number("(xxxx),") // lac
+ .number("(xxxx);") // cid
+ .any() // checksum
+ .compile();
+
+ private static final Pattern PATTERN_ALTERNATIVE = new PatternBuilder()
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(xxxx),") // lac
+ .number("(xxxx),") // cid
+ .number("(d+),") // gsm signal
+ .number("(d+),") // battery
+ .number("(d+),") // flags
+ .number("(d+),") // inputs
+ .number("(?:(d+),)?") // outputs
+ .number("(d.?d*),") // adc 1
+ .groupBegin()
+ .number("(d.dd),") // adc 2
+ .number("(d.dd),") // adc 3
+ .groupEnd("?")
+ .expression("([^;]+);") // alarm
+ .any() // checksum
+ .compile();
+
+ private boolean parseLocation(String location, Position position) {
+
+ Parser parser = new Parser(PATTERN_GPRMC, location);
+ if (!parser.matches()) {
+ return false;
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return true;
+ }
+
+ private Position decodeOld(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ // Detect type
+ boolean simple = sentence.charAt(3) == ',' || sentence.charAt(6) == ',';
+
+ // Split message
+ String id;
+ String location;
+ String status;
+ if (simple) {
+
+ int beginIndex = sentence.indexOf(',') + 1;
+ int endIndex = sentence.indexOf(',', beginIndex);
+ id = sentence.substring(beginIndex, endIndex);
+
+ beginIndex = endIndex + 1;
+ endIndex = sentence.indexOf('*', beginIndex);
+ if (endIndex != -1) {
+ endIndex += 3;
+ } else {
+ endIndex = sentence.length();
+ }
+ location = sentence.substring(beginIndex, endIndex);
+
+ beginIndex = endIndex + 1;
+ if (beginIndex > sentence.length()) {
+ beginIndex = endIndex;
+ }
+ status = sentence.substring(beginIndex);
+
+ } else {
+
+ int beginIndex = 3;
+ int endIndex = beginIndex + 16;
+ id = sentence.substring(beginIndex, endIndex).trim();
+
+ beginIndex = endIndex + 2;
+ endIndex = sentence.indexOf('*', beginIndex) + 3;
+ location = sentence.substring(beginIndex, endIndex);
+
+ beginIndex = endIndex + 1;
+ status = sentence.substring(beginIndex);
+
+ }
+
+ Position position = new Position(getProtocolName());
+ if (!parseLocation(location, position)) {
+ return null;
+ }
+
+ if (simple) {
+
+ Parser parser = new Parser(PATTERN_SIMPLE, status);
+ if (parser.matches()) {
+
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.next()));
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next(), id);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ String sat = parser.next();
+ if (sat.contains("/")) {
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(sat.split("/")[0]));
+ position.set(Position.KEY_SATELLITES_VISIBLE, Integer.parseInt(sat.split("/")[1]));
+ } else {
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(sat));
+ }
+
+ position.setAltitude(parser.nextDouble(0));
+
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextDouble(0));
+
+ String charger = parser.next();
+ if (charger != null) {
+ position.set(Position.KEY_CHARGE, Integer.parseInt(charger) == 1);
+ }
+
+ if (parser.hasNext(4)) {
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(0), parser.nextInt(0), parser.nextHexInt(0), parser.nextHexInt(0))));
+ }
+
+ } else {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ }
+
+ } else {
+
+ Parser parser = new Parser(PATTERN_ALTERNATIVE, status);
+ if (parser.matches()) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setNetwork(new Network(CellTower.from(parser.nextInt(0), parser.nextInt(0),
+ parser.nextHexInt(0), parser.nextHexInt(0), parser.nextInt(0))));
+
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextDouble());
+
+ position.set(Position.KEY_FLAGS, parser.next());
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+ position.set(Position.PREFIX_ADC + 3, parser.next());
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.next()));
+
+ }
+ }
+
+ return position;
+ }
+
+ private static final Pattern PATTERN_NEW = new PatternBuilder()
+ .number("dddd").optional()
+ .text("$MGV")
+ .number("ddd,")
+ .number("(d+),") // imei
+ .expression("[^,]*,") // name
+ .expression("([RS]),")
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),([NS]),") // latitude
+ .number("(d+)(dd.d+),([EW]),") // longitude
+ .number("dd,")
+ .number("(dd),") // satellites
+ .number("dd,")
+ .number("(d+.d+),") // hdop
+ .number("(d+.d+)?,") // speed
+ .number("(d+.d+)?,") // course
+ .number("(-?d+.d+),") // altitude
+ .number("(d+.d+)?,") // odometer
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(xxxx)?,") // lac
+ .number("(x+)?,") // cid
+ .number("(d+)?,") // gsm
+ .groupBegin()
+ .number("([01]{4})?,") // input
+ .number("([01]{4})?,") // output
+ .number("(d+)?,") // adc1
+ .number("(d+)?,") // adc2
+ .number("(d+)?,") // adc3
+ .or()
+ .number("(d+),") // input
+ .number("(d+),") // output
+ .number("(d+),") // adc1
+ .number("(d+),") // adc2
+ .number("(d+),") // adc3
+ .groupEnd()
+ .groupBegin()
+ .number("(-?d+.?d*)") // temperature 1
+ .or().text(" ")
+ .groupEnd("?").text(",")
+ .groupBegin()
+ .number("(-?d+.?d*)") // temperature 2
+ .or().text(" ")
+ .groupEnd("?").text(",")
+ .number("(d+)?,") // rfid
+ .expression("[^,]*,")
+ .number("(d+)?,") // battery
+ .expression("([^,]*)") // alert
+ .any()
+ .compile();
+
+ private Position decodeNew(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_NEW, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.next().equals("S")) {
+ position.set(Position.KEY_ARCHIVE, true);
+ }
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_HDOP, parser.nextDouble(0));
+
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ODOMETER, parser.nextDouble(0) * 1000);
+ }
+
+ int mcc = parser.nextInt();
+ int mnc = parser.nextInt();
+ Integer lac = parser.nextHexInt();
+ Integer cid = parser.nextHexInt();
+ Integer rssi = parser.nextInt();
+ if (lac != null && cid != null) {
+ CellTower tower = CellTower.from(mcc, mnc, lac, cid);
+ if (rssi != null) {
+ tower.setSignalStrength(rssi);
+ }
+ position.setNetwork(new Network(tower));
+ }
+
+ if (parser.hasNext(5)) {
+ position.set(Position.KEY_INPUT, parser.nextBinInt(0));
+ position.set(Position.KEY_OUTPUT, parser.nextBinInt(0));
+ for (int i = 1; i <= 3; i++) {
+ position.set(Position.PREFIX_ADC + i, parser.nextInt(0));
+ }
+ }
+
+ if (parser.hasNext(5)) {
+ position.set(Position.KEY_HEART_RATE, parser.nextInt());
+ position.set(Position.KEY_STEPS, parser.nextInt());
+ position.set("activityTime", parser.nextInt());
+ position.set("lightSleepTime", parser.nextInt());
+ position.set("deepSleepTime", parser.nextInt());
+ }
+
+ for (int i = 1; i <= 2; i++) {
+ String adc = parser.next();
+ if (adc != null) {
+ position.set(Position.PREFIX_TEMP + i, Double.parseDouble(adc));
+ }
+ }
+
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ String battery = parser.next();
+ if (battery != null) {
+ position.set(Position.KEY_BATTERY, Integer.parseInt(battery));
+ }
+
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.next()));
+
+ return position;
+ }
+
+ private String decodeAlarm(String value) {
+ value = value.toLowerCase();
+ if (value.startsWith("geo")) {
+ if (value.endsWith("in")) {
+ return Position.ALARM_GEOFENCE_ENTER;
+ } else if (value.endsWith("out")) {
+ return Position.ALARM_GEOFENCE_EXIT;
+ }
+ }
+ switch (value) {
+ case "poweron":
+ return Position.ALARM_POWER_ON;
+ case "poweroff":
+ return Position.ALARM_POWER_ON;
+ case "sos":
+ case "help":
+ return Position.ALARM_SOS;
+ case "over speed":
+ case "overspeed":
+ return Position.ALARM_OVERSPEED;
+ case "lowspeed":
+ return Position.ALARM_LOW_SPEED;
+ case "low battery":
+ case "lowbattery":
+ return Position.ALARM_LOW_BATTERY;
+ case "vib":
+ return Position.ALARM_VIBRATION;
+ case "move in":
+ return Position.ALARM_GEOFENCE_ENTER;
+ case "move out":
+ return Position.ALARM_GEOFENCE_EXIT;
+ case "error":
+ return Position.ALARM_FAULT;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.contains("$MG")) {
+ return decodeNew(channel, remoteAddress, sentence);
+ } else {
+ return decodeOld(channel, remoteAddress, sentence);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MeiligaoFrameDecoder.java b/src/main/java/org/traccar/protocol/MeiligaoFrameDecoder.java
new file mode 100644
index 000000000..52f9ae26d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MeiligaoFrameDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class MeiligaoFrameDecoder extends BaseFrameDecoder {
+
+ private static final int MESSAGE_HEADER = 4;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ // Strip not '$' (0x24) bytes from the beginning
+ while (buf.isReadable() && buf.getUnsignedByte(buf.readerIndex()) != 0x24) {
+ buf.readByte();
+ }
+
+ // Check length and return buffer
+ if (buf.readableBytes() >= MESSAGE_HEADER) {
+ int length = buf.getUnsignedShort(buf.readerIndex() + 2);
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MeiligaoProtocol.java b/src/main/java/org/traccar/protocol/MeiligaoProtocol.java
new file mode 100644
index 000000000..c307c7318
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MeiligaoProtocol.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class MeiligaoProtocol extends BaseProtocol {
+
+ public MeiligaoProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_POSITION_PERIODIC,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_ALARM_GEOFENCE,
+ Command.TYPE_SET_TIMEZONE,
+ Command.TYPE_REQUEST_PHOTO,
+ Command.TYPE_REBOOT_DEVICE);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new MeiligaoFrameDecoder());
+ pipeline.addLast(new MeiligaoProtocolEncoder());
+ pipeline.addLast(new MeiligaoProtocolDecoder(MeiligaoProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new MeiligaoProtocolEncoder());
+ pipeline.addLast(new MeiligaoProtocolDecoder(MeiligaoProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MeiligaoProtocolDecoder.java b/src/main/java/org/traccar/protocol/MeiligaoProtocolDecoder.java
new file mode 100644
index 000000000..cbfc3660a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MeiligaoProtocolDecoder.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class MeiligaoProtocolDecoder extends BaseProtocolDecoder {
+
+ private Map<Byte, ByteBuf> photos = new HashMap<>();
+
+ public MeiligaoProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .expression("[^\\|]*")
+ .groupBegin()
+ .number("|(d+.d+)?") // hdop
+ .number("|(-?d+.?d*)?") // altitude
+ .number("|(xxxx)?") // state
+ .groupBegin()
+ .number("|(xxxx),(xxxx)") // adc
+ .number(",(xxxx)").optional()
+ .number(",(xxxx)").optional()
+ .number(",(xxxx)").optional()
+ .number(",(xxxx)").optional()
+ .number(",(xxxx)").optional()
+ .number(",(xxxx)").optional()
+ .groupBegin()
+ .number("|x{16,20}") // cell
+ .number("|(xx)") // rssi
+ .number("|(x{8})") // odometer
+ .groupBegin()
+ .number("|(xx)") // satellites
+ .text("|")
+ .expression("(.*)") // driver
+ .groupEnd("?")
+ .or()
+ .number("|(d{1,9})") // odometer
+ .groupBegin()
+ .number("|(x{5,})") // rfid
+ .groupEnd("?")
+ .groupEnd("?")
+ .groupEnd("?")
+ .groupEnd("?")
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_RFID = new PatternBuilder()
+ .number("|(dd)(dd)(dd),") // time (hhmmss)
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW])")
+ .compile();
+
+ private static final Pattern PATTERN_OBD = new PatternBuilder()
+ .number("(d+.d+),") // battery
+ .number("(d+),") // rpm
+ .number("(d+),") // speed
+ .number("(d+.d+),") // throttle
+ .number("(d+.d+),") // engine load
+ .number("(-?d+),") // coolant temp
+ .number("(d+.d+),") // instantaneous fuel
+ .number("(d+.d+),") // average fuel
+ .number("(d+.d+),") // driving range
+ .number("(d+.?d*),") // odometer
+ .number("(d+.d+),") // single fuel consumption
+ .number("(d+.d+),") // total fuel consumption
+ .number("(d+),") // error code count
+ .number("(d+),") // hard acceleration count
+ .number("(d+)") // hard brake count
+ .compile();
+
+ private static final Pattern PATTERN_OBDA = new PatternBuilder()
+ .number("(d+),") // total ignition
+ .number("(d+.d+),") // total driving time
+ .number("(d+.d+),") // total idling time
+ .number("(d+),") // average hot start time
+ .number("(d+),") // average speed
+ .number("(d+),") // history highest speed
+ .number("(d+),") // history highest rpm
+ .number("(d+),") // total hard acceleration
+ .number("(d+)") // total hard brake
+ .compile();
+
+ public static final int MSG_HEARTBEAT = 0x0001;
+ public static final int MSG_SERVER = 0x0002;
+ public static final int MSG_LOGIN = 0x5000;
+ public static final int MSG_LOGIN_RESPONSE = 0x4000;
+ public static final int MSG_POSITION = 0x9955;
+ public static final int MSG_POSITION_LOGGED = 0x9016;
+ public static final int MSG_ALARM = 0x9999;
+ public static final int MSG_RFID = 0x9966;
+ public static final int MSG_RETRANSMISSION = 0x6688;
+
+ public static final int MSG_OBD_RT = 0x9901;
+ public static final int MSG_OBD_RTA = 0x9902;
+
+ public static final int MSG_TRACK_ON_DEMAND = 0x4101;
+ public static final int MSG_TRACK_BY_INTERVAL = 0x4102;
+ public static final int MSG_MOVEMENT_ALARM = 0x4106;
+ public static final int MSG_OUTPUT_CONTROL = 0x4115;
+ public static final int MSG_TIME_ZONE = 0x4132;
+ public static final int MSG_TAKE_PHOTO = 0x4151;
+ public static final int MSG_UPLOAD_PHOTO = 0x0800;
+ public static final int MSG_UPLOAD_PHOTO_RESPONSE = 0x8801;
+ public static final int MSG_DATA_PHOTO = 0x9988;
+ public static final int MSG_POSITION_IMAGE = 0x9977;
+ public static final int MSG_UPLOAD_COMPLETE = 0x0f80;
+ public static final int MSG_REBOOT_GPS = 0x4902;
+
+ private DeviceSession identify(ByteBuf buf, Channel channel, SocketAddress remoteAddress) {
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0; i < 7; i++) {
+ int b = buf.readUnsignedByte();
+
+ // First digit
+ int d1 = (b & 0xf0) >> 4;
+ if (d1 == 0xf) {
+ break;
+ }
+ builder.append(d1);
+
+ // Second digit
+ int d2 = b & 0x0f;
+ if (d2 == 0xf) {
+ break;
+ }
+ builder.append(d2);
+ }
+
+ String id = builder.toString();
+
+ if (id.length() == 14) {
+ return getDeviceSession(channel, remoteAddress, id, id + Checksum.luhn(Long.parseLong(id)));
+ } else {
+ return getDeviceSession(channel, remoteAddress, id);
+ }
+ }
+
+ private static void sendResponse(
+ Channel channel, SocketAddress remoteAddress, ByteBuf id, int type, ByteBuf msg) {
+
+ if (channel != null) {
+ ByteBuf buf = Unpooled.buffer(
+ 2 + 2 + id.readableBytes() + 2 + msg.readableBytes() + 2 + 2);
+
+ buf.writeByte('@');
+ buf.writeByte('@');
+ buf.writeShort(buf.capacity());
+ buf.writeBytes(id);
+ buf.writeShort(type);
+ buf.writeBytes(msg);
+ msg.release();
+ buf.writeShort(Checksum.crc16(Checksum.CRC16_CCITT_FALSE, buf.nioBuffer()));
+ buf.writeByte('\r');
+ buf.writeByte('\n');
+
+ channel.writeAndFlush(new NetworkMessage(buf, remoteAddress));
+ }
+ }
+
+ private String decodeAlarm(short value) {
+ switch (value) {
+ case 0x01:
+ return Position.ALARM_SOS;
+ case 0x10:
+ return Position.ALARM_LOW_BATTERY;
+ case 0x11:
+ return Position.ALARM_OVERSPEED;
+ case 0x12:
+ return Position.ALARM_MOVEMENT;
+ case 0x13:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 0x14:
+ return Position.ALARM_ACCIDENT;
+ case 0x50:
+ return Position.ALARM_POWER_OFF;
+ case 0x53:
+ return Position.ALARM_GPS_ANTENNA_CUT;
+ case 0x72:
+ return Position.ALARM_BRAKING;
+ case 0x73:
+ return Position.ALARM_ACCELERATION;
+ default:
+ return null;
+ }
+ }
+
+ private Position decodeRegular(Position position, String sentence) {
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ if (parser.hasNext()) {
+ position.setSpeed(parser.nextDouble(0));
+ }
+
+ if (parser.hasNext()) {
+ position.setCourse(parser.nextDouble(0));
+ }
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ if (parser.hasNext()) {
+ position.setAltitude(parser.nextDouble(0));
+ }
+
+ if (parser.hasNext()) {
+ int status = parser.nextHexInt();
+ for (int i = 1; i <= 5; i++) {
+ position.set(Position.PREFIX_OUT + i, BitUtil.check(status, i - 1));
+ }
+ for (int i = 1; i <= 5; i++) {
+ position.set(Position.PREFIX_IN + i, BitUtil.check(status, i - 1 + 8));
+ }
+ }
+
+ for (int i = 1; i <= 8; i++) {
+ position.set(Position.PREFIX_ADC + i, parser.nextHexInt());
+ }
+
+ position.set(Position.KEY_RSSI, parser.nextHexInt());
+ position.set(Position.KEY_ODOMETER, parser.nextHexLong());
+ position.set(Position.KEY_SATELLITES, parser.nextHexInt());
+ position.set("driverLicense", parser.next());
+ position.set(Position.KEY_ODOMETER, parser.nextLong());
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ return position;
+ }
+
+ private Position decodeRfid(Position position, String sentence) {
+ Parser parser = new Parser(PATTERN_RFID, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ return position;
+ }
+
+ private Position decodeObd(Position position, String sentence) {
+ Parser parser = new Parser(PATTERN_OBD, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.KEY_RPM, parser.nextInt());
+ position.set(Position.KEY_OBD_SPEED, parser.nextInt());
+ position.set(Position.KEY_THROTTLE, parser.nextDouble());
+ position.set(Position.KEY_ENGINE_LOAD, parser.nextDouble());
+ position.set(Position.KEY_COOLANT_TEMP, parser.nextInt());
+ position.set(Position.KEY_FUEL_CONSUMPTION, parser.nextDouble());
+ position.set("averageFuelConsumption", parser.nextDouble());
+ position.set("drivingRange", parser.nextDouble());
+ position.set(Position.KEY_ODOMETER, parser.nextDouble());
+ position.set("singleFuelConsumption", parser.nextDouble());
+ position.set(Position.KEY_FUEL_USED, parser.nextDouble());
+ position.set(Position.KEY_DTCS, parser.nextInt());
+ position.set("hardAccelerationCount", parser.nextInt());
+ position.set("hardBrakingCount", parser.nextInt());
+
+ return position;
+ }
+
+ private Position decodeObdA(Position position, String sentence) {
+ Parser parser = new Parser(PATTERN_OBDA, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ getLastLocation(position, null);
+
+ position.set("totalIgnitionNo", parser.nextInt(0));
+ position.set("totalDrivingTime", parser.nextDouble(0));
+ position.set("totalIdlingTime", parser.nextDouble(0));
+ position.set("averageHotStartTime", parser.nextInt(0));
+ position.set("averageSpeed", parser.nextInt(0));
+ position.set("historyHighestSpeed", parser.nextInt(0));
+ position.set("historyHighestRpm", parser.nextInt(0));
+ position.set("totalHarshAccerleration", parser.nextInt(0));
+ position.set("totalHarshBrake", parser.nextInt(0));
+
+ return position;
+ }
+
+ private List<Position> decodeRetransmission(ByteBuf buf, DeviceSession deviceSession) {
+ List<Position> positions = new LinkedList<>();
+
+ int count = buf.readUnsignedByte();
+ for (int i = 0; i < count; i++) {
+
+ buf.readUnsignedByte(); // alarm
+
+ int endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '\\');
+ if (endIndex < 0) {
+ endIndex = buf.writerIndex() - 4;
+ }
+
+ String sentence = buf.readSlice(endIndex - buf.readerIndex()).toString(StandardCharsets.US_ASCII);
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position = decodeRegular(position, sentence);
+
+ if (position != null) {
+ positions.add(position);
+ }
+
+ if (buf.readableBytes() > 4) {
+ buf.readUnsignedByte(); // delimiter
+ }
+
+ }
+
+ return positions;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+ buf.skipBytes(2); // header
+ buf.readShort(); // length
+ ByteBuf id = buf.readSlice(7);
+ int command = buf.readUnsignedShort();
+
+ if (command == MSG_LOGIN) {
+ ByteBuf response = Unpooled.wrappedBuffer(new byte[]{0x01});
+ sendResponse(channel, remoteAddress, id, MSG_LOGIN_RESPONSE, response);
+ return null;
+ } else if (command == MSG_HEARTBEAT) {
+ ByteBuf response = Unpooled.wrappedBuffer(new byte[]{0x01});
+ sendResponse(channel, remoteAddress, id, MSG_HEARTBEAT, response);
+ return null;
+ } else if (command == MSG_SERVER) {
+ ByteBuf response = Unpooled.copiedBuffer(getServer(channel, ':'), StandardCharsets.US_ASCII);
+ sendResponse(channel, remoteAddress, id, MSG_SERVER, response);
+ return null;
+ } else if (command == MSG_UPLOAD_PHOTO) {
+ byte imageIndex = buf.readByte();
+ photos.put(imageIndex, Unpooled.buffer());
+ ByteBuf response = Unpooled.copiedBuffer(new byte[]{imageIndex});
+ sendResponse(channel, remoteAddress, id, MSG_UPLOAD_PHOTO_RESPONSE, response);
+ return null;
+ } else if (command == MSG_UPLOAD_COMPLETE) {
+ byte imageIndex = buf.readByte();
+ ByteBuf response = Unpooled.copiedBuffer(new byte[]{imageIndex, 0, 0});
+ sendResponse(channel, remoteAddress, id, MSG_RETRANSMISSION, response);
+ return null;
+ }
+
+ DeviceSession deviceSession = identify(id, channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (command == MSG_DATA_PHOTO) {
+
+ byte imageIndex = buf.readByte();
+ buf.readUnsignedShort(); // image footage
+ buf.readUnsignedByte(); // total packets
+ buf.readUnsignedByte(); // packet index
+
+ photos.get(imageIndex).writeBytes(buf, buf.readableBytes() - 2 - 2);
+
+ return null;
+
+ } else if (command == MSG_RETRANSMISSION) {
+
+ return decodeRetransmission(buf, deviceSession);
+
+ } else {
+
+ Position position = new Position(getProtocolName());
+
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (command == MSG_ALARM) {
+ short alarmCode = buf.readUnsignedByte();
+ position.set(Position.KEY_ALARM, decodeAlarm(alarmCode));
+ if (alarmCode >= 0x02 && alarmCode <= 0x05) {
+ position.set(Position.PREFIX_IN + alarmCode, 1);
+ } else if (alarmCode >= 0x32 && alarmCode <= 0x35) {
+ position.set(Position.PREFIX_IN + (alarmCode - 0x30), 0);
+ }
+ } else if (command == MSG_POSITION_LOGGED) {
+ buf.skipBytes(6);
+ } else if (command == MSG_RFID) {
+ for (int i = 0; i < 15; i++) {
+ long rfid = buf.readUnsignedInt();
+ if (rfid != 0) {
+ String card = String.format("%010d", rfid);
+ position.set("card" + (i + 1), card);
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, card);
+ }
+ }
+ } else if (command == MSG_POSITION_IMAGE) {
+ byte imageIndex = buf.readByte();
+ buf.readUnsignedByte(); // image upload type
+ String uniqueId = Context.getIdentityManager().getById(deviceSession.getDeviceId()).getUniqueId();
+ ByteBuf photo = photos.remove(imageIndex);
+ try {
+ position.set(Position.KEY_IMAGE, Context.getMediaManager().writeFile(uniqueId, photo, "jpg"));
+ } finally {
+ photo.release();
+ }
+ }
+
+ String sentence = buf.toString(buf.readerIndex(), buf.readableBytes() - 4, StandardCharsets.US_ASCII);
+
+ switch (command) {
+ case MSG_POSITION:
+ case MSG_POSITION_LOGGED:
+ case MSG_ALARM:
+ case MSG_POSITION_IMAGE:
+ return decodeRegular(position, sentence);
+ case MSG_RFID:
+ return decodeRfid(position, sentence);
+ case MSG_OBD_RT:
+ return decodeObd(position, sentence);
+ case MSG_OBD_RTA:
+ return decodeObdA(position, sentence);
+ default:
+ return null;
+ }
+
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MeiligaoProtocolEncoder.java b/src/main/java/org/traccar/protocol/MeiligaoProtocolEncoder.java
new file mode 100644
index 000000000..57cbbe0fc
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MeiligaoProtocolEncoder.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DataConverter;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+import java.util.TimeZone;
+
+public class MeiligaoProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeContent(long deviceId, int type, ByteBuf content) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeByte('@');
+ buf.writeByte('@');
+
+ buf.writeShort(2 + 2 + 7 + 2 + content.readableBytes() + 2 + 2); // message length
+
+ buf.writeBytes(DataConverter.parseHex((getUniqueId(deviceId) + "FFFFFFFFFFFFFF").substring(0, 14)));
+
+ buf.writeShort(type);
+
+ buf.writeBytes(content);
+
+ buf.writeShort(Checksum.crc16(Checksum.CRC16_CCITT_FALSE, buf.nioBuffer()));
+
+ buf.writeByte('\r');
+ buf.writeByte('\n');
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ ByteBuf content = Unpooled.buffer();
+
+ switch (command.getType()) {
+ case Command.TYPE_POSITION_SINGLE:
+ return encodeContent(command.getDeviceId(), MeiligaoProtocolDecoder.MSG_TRACK_ON_DEMAND, content);
+ case Command.TYPE_POSITION_PERIODIC:
+ content.writeShort(command.getInteger(Command.KEY_FREQUENCY) / 10);
+ return encodeContent(command.getDeviceId(), MeiligaoProtocolDecoder.MSG_TRACK_BY_INTERVAL, content);
+ case Command.TYPE_ENGINE_STOP:
+ content.writeByte(0x01);
+ return encodeContent(command.getDeviceId(), MeiligaoProtocolDecoder.MSG_OUTPUT_CONTROL, content);
+ case Command.TYPE_ENGINE_RESUME:
+ content.writeByte(0x00);
+ return encodeContent(command.getDeviceId(), MeiligaoProtocolDecoder.MSG_OUTPUT_CONTROL, content);
+ case Command.TYPE_ALARM_GEOFENCE:
+ content.writeShort(command.getInteger(Command.KEY_RADIUS));
+ return encodeContent(command.getDeviceId(), MeiligaoProtocolDecoder.MSG_MOVEMENT_ALARM, content);
+ case Command.TYPE_SET_TIMEZONE:
+ int offset = TimeZone.getTimeZone(command.getString(Command.KEY_TIMEZONE)).getRawOffset() / 60000;
+ content.writeBytes(String.valueOf(offset).getBytes(StandardCharsets.US_ASCII));
+ return encodeContent(command.getDeviceId(), MeiligaoProtocolDecoder.MSG_TIME_ZONE, content);
+ case Command.TYPE_REQUEST_PHOTO:
+ return encodeContent(command.getDeviceId(), MeiligaoProtocolDecoder.MSG_TAKE_PHOTO, content);
+ case Command.TYPE_REBOOT_DEVICE:
+ return encodeContent(command.getDeviceId(), MeiligaoProtocolDecoder.MSG_REBOOT_GPS, content);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MeitrackFrameDecoder.java b/src/main/java/org/traccar/protocol/MeitrackFrameDecoder.java
new file mode 100644
index 000000000..d122bca0c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MeitrackFrameDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+import java.nio.charset.StandardCharsets;
+
+public class MeitrackFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ',');
+ if (index != -1) {
+ int length = index - buf.readerIndex() + Integer.parseInt(
+ buf.toString(buf.readerIndex() + 3, index - buf.readerIndex() - 3, StandardCharsets.US_ASCII));
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MeitrackProtocol.java b/src/main/java/org/traccar/protocol/MeitrackProtocol.java
new file mode 100644
index 000000000..c887cd3a0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MeitrackProtocol.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class MeitrackProtocol extends BaseProtocol {
+
+ public MeitrackProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_ALARM_ARM,
+ Command.TYPE_ALARM_DISARM,
+ Command.TYPE_REQUEST_PHOTO,
+ Command.TYPE_SEND_SMS);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new MeitrackFrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new MeitrackProtocolEncoder());
+ pipeline.addLast(new MeitrackProtocolDecoder(MeitrackProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new MeitrackProtocolEncoder());
+ pipeline.addLast(new MeitrackProtocolDecoder(MeitrackProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java
new file mode 100644
index 000000000..55260ef0c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MeitrackProtocolDecoder.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class MeitrackProtocolDecoder extends BaseProtocolDecoder {
+
+ private ByteBuf photo;
+
+ public MeitrackProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$$").expression(".") // flag
+ .number("d+,") // length
+ .number("(d+),") // imei
+ .number("xxx,") // command
+ .number("d+,").optional()
+ .number("(d+),") // event
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("([AV]),") // validity
+ .number("(d+),") // satellites
+ .number("(d+),") // rssi
+ .number("(d+.?d*),") // speed
+ .number("(d+),") // course
+ .number("(d+.?d*),") // hdop
+ .number("(-?d+),") // altitude
+ .number("(d+),") // odometer
+ .number("(d+),") // runtime
+ .number("(d+)|") // mcc
+ .number("(d+)|") // mnc
+ .number("(x+)|") // lac
+ .number("(x+),") // cid
+ .number("(x+),") // state
+ .number("(x+)?|") // adc1
+ .number("(x+)?|") // adc2
+ .number("(x+)?|") // adc3
+ .number("(x+)|") // battery
+ .number("(x+)?,") // power
+ .groupBegin()
+ .expression("([^,]+)?,").optional() // event specific
+ .expression("[^,]*,") // reserved
+ .number("(d+)?,") // protocol
+ .number("(x{4})?") // fuel
+ .groupBegin()
+ .number(",(x{6}(?:|x{6})*)?") // temperature
+ .groupBegin()
+ .number(",(d+)") // data count
+ .expression(",([^*]*)") // data
+ .groupEnd("?")
+ .groupEnd("?")
+ .or()
+ .any()
+ .groupEnd()
+ .text("*")
+ .number("xx")
+ .text("\r\n").optional()
+ .compile();
+
+ private String decodeAlarm(int event) {
+ switch (event) {
+ case 1:
+ return Position.ALARM_SOS;
+ case 17:
+ return Position.ALARM_LOW_BATTERY;
+ case 18:
+ return Position.ALARM_LOW_POWER;
+ case 19:
+ return Position.ALARM_OVERSPEED;
+ case 20:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 21:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 22:
+ return Position.ALARM_POWER_RESTORED;
+ case 23:
+ return Position.ALARM_POWER_CUT;
+ case 36:
+ return Position.ALARM_TOW;
+ case 44:
+ return Position.ALARM_JAMMING;
+ case 78:
+ return Position.ALARM_ACCIDENT;
+ case 90:
+ case 91:
+ return Position.ALARM_CORNERING;
+ case 129:
+ return Position.ALARM_BRAKING;
+ case 130:
+ return Position.ALARM_ACCELERATION;
+ case 135:
+ return Position.ALARM_FATIGUE_DRIVING;
+ default:
+ return null;
+ }
+ }
+
+ private Position decodeRegular(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ Parser parser = new Parser(PATTERN, buf.toString(StandardCharsets.US_ASCII));
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int event = parser.nextInt(0);
+ position.set(Position.KEY_EVENT, event);
+ position.set(Position.KEY_ALARM, decodeAlarm(event));
+
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(parser.next().equals("A"));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ int rssi = parser.nextInt(0);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ position.setAltitude(parser.nextDouble(0));
+
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+ position.set("runtime", parser.next());
+
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(0), parser.nextInt(0), parser.nextHexInt(0), parser.nextHexInt(0), rssi)));
+
+ position.set(Position.KEY_STATUS, parser.next());
+
+ for (int i = 1; i <= 3; i++) {
+ if (parser.hasNext()) {
+ position.set(Position.PREFIX_ADC + i, parser.nextHexInt(0));
+ }
+ }
+
+ String deviceModel = Context.getIdentityManager().getById(deviceSession.getDeviceId()).getModel();
+ if (deviceModel == null) {
+ deviceModel = "";
+ }
+ switch (deviceModel.toUpperCase()) {
+ case "MVT340":
+ case "MVT380":
+ position.set(Position.KEY_BATTERY, parser.nextHexInt(0) * 3.0 * 2.0 / 1024.0);
+ position.set(Position.KEY_POWER, parser.nextHexInt(0) * 3.0 * 16.0 / 1024.0);
+ break;
+ case "MT90":
+ position.set(Position.KEY_BATTERY, parser.nextHexInt(0) * 3.3 * 2.0 / 4096.0);
+ position.set(Position.KEY_POWER, parser.nextHexInt(0));
+ break;
+ case "T1":
+ case "T3":
+ case "MVT100":
+ case "MVT600":
+ case "MVT800":
+ case "TC68":
+ case "TC68S":
+ position.set(Position.KEY_BATTERY, parser.nextHexInt(0) * 3.3 * 2.0 / 4096.0);
+ position.set(Position.KEY_POWER, parser.nextHexInt(0) * 3.3 * 16.0 / 4096.0);
+ break;
+ case "T311":
+ case "T322X":
+ case "T333":
+ case "T355":
+ position.set(Position.KEY_BATTERY, parser.nextHexInt(0) / 100.0);
+ position.set(Position.KEY_POWER, parser.nextHexInt(0) / 100.0);
+ break;
+ default:
+ position.set(Position.KEY_BATTERY, parser.nextHexInt(0));
+ position.set(Position.KEY_POWER, parser.nextHexInt(0));
+ break;
+ }
+
+ String eventData = parser.next();
+ if (eventData != null && !eventData.isEmpty()) {
+ switch (event) {
+ case 37:
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, eventData);
+ break;
+ default:
+ position.set("eventData", eventData);
+ break;
+ }
+ }
+
+ int protocol = parser.nextInt(0);
+
+ if (parser.hasNext()) {
+ String fuel = parser.next();
+ position.set(Position.KEY_FUEL_LEVEL,
+ Integer.parseInt(fuel.substring(0, 2), 16) + Integer.parseInt(fuel.substring(2), 16) * 0.01);
+ }
+
+ if (parser.hasNext()) {
+ for (String temp : parser.next().split("\\|")) {
+ int index = Integer.parseInt(temp.substring(0, 2), 16);
+ if (protocol >= 3) {
+ double value = (short) Integer.parseInt(temp.substring(2), 16);
+ position.set(Position.PREFIX_TEMP + index, value * 0.01);
+ } else {
+ double value = Byte.parseByte(temp.substring(2, 4), 16);
+ value += (value < 0 ? -0.01 : 0.01) * Integer.parseInt(temp.substring(4), 16);
+ position.set(Position.PREFIX_TEMP + index, value);
+ }
+ }
+ }
+
+ if (parser.hasNext(2)) {
+ parser.nextInt(); // count
+ decodeDataFields(position, parser.next().split(","));
+ }
+
+ return position;
+ }
+
+ private void decodeDataFields(Position position, String[] values) {
+
+ if (values.length > 1 && !values[1].isEmpty()) {
+ position.set("tempData", values[1]);
+ }
+
+ if (values.length > 5 && !values[5].isEmpty()) {
+ String[] data = values[5].split("\\|");
+ boolean started = data[0].charAt(1) == '0';
+ position.set("taximeterOn", started);
+ position.set("taximeterStart", data[1]);
+ if (data.length > 2) {
+ position.set("taximeterEnd", data[2]);
+ position.set("taximeterDistance", Integer.parseInt(data[3]));
+ position.set("taximeterFare", Integer.parseInt(data[4]));
+ position.set("taximeterTrip", data[5]);
+ position.set("taximeterWait", data[6]);
+ }
+ }
+
+ }
+
+ private List<Position> decodeBinaryC(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+ List<Position> positions = new LinkedList<>();
+
+ String flag = buf.toString(2, 1, StandardCharsets.US_ASCII);
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ',');
+
+ String imei = buf.toString(index + 1, 15, StandardCharsets.US_ASCII);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ buf.skipBytes(index + 1 + 15 + 1 + 3 + 1 + 2 + 2 + 4);
+
+ while (buf.readableBytes() >= 0x34) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+
+ position.setLatitude(buf.readIntLE() * 0.000001);
+ position.setLongitude(buf.readIntLE() * 0.000001);
+
+ position.setTime(new Date((946684800 + buf.readUnsignedIntLE()) * 1000)); // 946684800 = 2000-01-01
+
+ position.setValid(buf.readUnsignedByte() == 1);
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ int rssi = buf.readUnsignedByte();
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShortLE()));
+ position.setCourse(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_HDOP, buf.readUnsignedShortLE() * 0.1);
+
+ position.setAltitude(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ position.set("runtime", buf.readUnsignedIntLE());
+
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShortLE(), buf.readUnsignedShortLE(),
+ buf.readUnsignedShortLE(), buf.readUnsignedShortLE(),
+ rssi)));
+
+ position.set(Position.KEY_STATUS, buf.readUnsignedShortLE());
+
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShortLE());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.01);
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE());
+
+ buf.readUnsignedIntLE(); // geo-fence
+
+ positions.add(position);
+ }
+
+ if (channel != null) {
+ StringBuilder command = new StringBuilder("@@");
+ command.append(flag).append(27 + positions.size() / 10).append(",");
+ command.append(imei).append(",CCC,").append(positions.size()).append("*");
+ int checksum = 0;
+ for (int i = 0; i < command.length(); i += 1) {
+ checksum += command.charAt(i);
+ }
+ command.append(String.format("%02x", checksum & 0xff).toUpperCase());
+ command.append("\r\n");
+ channel.writeAndFlush(new NetworkMessage(command.toString(), remoteAddress)); // delete processed data
+ }
+
+ return positions;
+ }
+
+ private List<Position> decodeBinaryE(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+ List<Position> positions = new LinkedList<>();
+
+ buf.readerIndex(buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ',') + 1);
+ String imei = buf.readSlice(15).toString(StandardCharsets.US_ASCII);
+ buf.skipBytes(1 + 3 + 1);
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ buf.readUnsignedIntLE(); // remaining cache
+ int count = buf.readUnsignedShortLE();
+
+ for (int i = 0; i < count; i++) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedShortLE(); // length
+ buf.readUnsignedShortLE(); // index
+
+ int paramCount = buf.readUnsignedByte();
+ for (int j = 0; j < paramCount; j++) {
+ int id = buf.readUnsignedByte();
+ switch (id) {
+ case 0x01:
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+ break;
+ case 0x05:
+ position.setValid(buf.readUnsignedByte() > 0);
+ break;
+ case 0x06:
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ break;
+ case 0x07:
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ break;
+ default:
+ buf.readUnsignedByte();
+ break;
+ }
+ }
+
+ paramCount = buf.readUnsignedByte();
+ for (int j = 0; j < paramCount; j++) {
+ int id = buf.readUnsignedByte();
+ switch (id) {
+ case 0x08:
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShortLE()));
+ break;
+ case 0x09:
+ position.setCourse(buf.readUnsignedShortLE());
+ break;
+ case 0x0B:
+ position.setAltitude(buf.readShortLE());
+ break;
+ case 0x19:
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.01);
+ break;
+ case 0x1A:
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() * 0.01);
+ break;
+ default:
+ buf.readUnsignedShortLE();
+ break;
+ }
+ }
+
+ paramCount = buf.readUnsignedByte();
+ for (int j = 0; j < paramCount; j++) {
+ int id = buf.readUnsignedByte();
+ switch (id) {
+ case 0x02:
+ position.setLatitude(buf.readIntLE() * 0.000001);
+ break;
+ case 0x03:
+ position.setLongitude(buf.readIntLE() * 0.000001);
+ break;
+ case 0x04:
+ position.setTime(new Date((946684800 + buf.readUnsignedIntLE()) * 1000)); // 2000-01-01
+ break;
+ case 0x0D:
+ position.set("runtime", buf.readUnsignedIntLE());
+ break;
+ default:
+ buf.readUnsignedIntLE();
+ break;
+ }
+ }
+
+ paramCount = buf.readUnsignedByte();
+ for (int j = 0; j < paramCount; j++) {
+ buf.readUnsignedByte(); // id
+ buf.skipBytes(buf.readUnsignedByte()); // value
+ }
+
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+ private void requestPhotoPacket(Channel channel, SocketAddress socketAddress, String imei, String file, int index) {
+ if (channel != null) {
+ String content = "D00," + file + "," + index;
+ int length = 1 + imei.length() + 1 + content.length() + 5;
+ String response = String.format("@@O%02d,%s,%s*", length, imei, content);
+ response += Checksum.sum(response) + "\r\n";
+ channel.writeAndFlush(new NetworkMessage(response, socketAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ',');
+ String imei = buf.toString(index + 1, 15, StandardCharsets.US_ASCII);
+ index = buf.indexOf(index + 1, buf.writerIndex(), (byte) ',');
+ String type = buf.toString(index + 1, 3, StandardCharsets.US_ASCII);
+
+ switch (type) {
+ case "D00":
+ if (photo == null) {
+ photo = Unpooled.buffer();
+ }
+
+ index = index + 1 + type.length() + 1;
+ int endIndex = buf.indexOf(index, buf.writerIndex(), (byte) ',');
+ String file = buf.toString(index, endIndex - index, StandardCharsets.US_ASCII);
+ index = endIndex + 1;
+ endIndex = buf.indexOf(index, buf.writerIndex(), (byte) ',');
+ int total = Integer.parseInt(buf.toString(index, endIndex - index, StandardCharsets.US_ASCII));
+ index = endIndex + 1;
+ endIndex = buf.indexOf(index, buf.writerIndex(), (byte) ',');
+ int current = Integer.parseInt(buf.toString(index, endIndex - index, StandardCharsets.US_ASCII));
+
+ buf.readerIndex(endIndex + 1);
+ photo.writeBytes(buf.readSlice(buf.readableBytes() - 1 - 2 - 2));
+
+ if (current == total - 1) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(getDeviceSession(channel, remoteAddress, imei).getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_IMAGE, Context.getMediaManager().writeFile(imei, photo, "jpg"));
+ photo.release();
+ photo = null;
+
+ return position;
+ } else {
+ if ((current + 1) % 8 == 0) {
+ requestPhotoPacket(channel, remoteAddress, imei, file, current + 1);
+ }
+ return null;
+ }
+ case "D03":
+ photo = Unpooled.buffer();
+ requestPhotoPacket(channel, remoteAddress, imei, "camera_picture.jpg", 0);
+ return null;
+ case "CCC":
+ return decodeBinaryC(channel, remoteAddress, buf);
+ case "CCE":
+ return decodeBinaryE(channel, remoteAddress, buf);
+ default:
+ return decodeRegular(channel, remoteAddress, buf);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MeitrackProtocolEncoder.java b/src/main/java/org/traccar/protocol/MeitrackProtocolEncoder.java
new file mode 100644
index 000000000..abb6ec9d4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MeitrackProtocolEncoder.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.Context;
+import org.traccar.StringProtocolEncoder;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+import java.util.Map;
+
+public class MeitrackProtocolEncoder extends StringProtocolEncoder {
+
+ private Object formatCommand(Command command, char dataId, String content) {
+ String uniqueId = getUniqueId(command.getDeviceId());
+ int length = 1 + uniqueId.length() + 1 + content.length() + 5;
+ String result = String.format("@@%c%02d,%s,%s*", dataId, length, uniqueId, content);
+ result += Checksum.sum(result) + "\r\n";
+ return result;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ Map<String, Object> attributes = command.getAttributes();
+
+ boolean alternative = Context.getIdentityManager().lookupAttributeBoolean(
+ command.getDeviceId(), "meitrack.alternative", false, true);
+
+ switch (command.getType()) {
+ case Command.TYPE_POSITION_SINGLE:
+ return formatCommand(command, 'Q', "A10");
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, 'M', "C01,0,12222");
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, 'M', "C01,0,02222");
+ case Command.TYPE_ALARM_ARM:
+ return formatCommand(command, 'M', alternative ? "B21,1" : "C01,0,22122");
+ case Command.TYPE_ALARM_DISARM:
+ return formatCommand(command, 'M', alternative ? "B21,0" : "C01,0,22022");
+ case Command.TYPE_REQUEST_PHOTO:
+ int index = command.getInteger(Command.KEY_INDEX);
+ return formatCommand(command, 'D', "D03," + (index > 0 ? index : 1) + ",camera_picture.jpg");
+ case Command.TYPE_SEND_SMS:
+ return formatCommand(command, 'f', "C02,0,"
+ + attributes.get(Command.KEY_PHONE) + "," + attributes.get(Command.KEY_MESSAGE));
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MilesmateProtocol.java b/src/main/java/org/traccar/protocol/MilesmateProtocol.java
new file mode 100644
index 000000000..822711603
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MilesmateProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class MilesmateProtocol extends BaseProtocol {
+
+ public MilesmateProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new MilesmateProtocolDecoder(MilesmateProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MilesmateProtocolDecoder.java b/src/main/java/org/traccar/protocol/MilesmateProtocolDecoder.java
new file mode 100644
index 000000000..901ceb8f7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MilesmateProtocolDecoder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class MilesmateProtocolDecoder extends BaseProtocolDecoder {
+
+ public MilesmateProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("ApiString={")
+ .number("A:(d+),") // imei
+ .number("B:(d+.d+),") // battery
+ .number("C:(d+.d+),") // adc
+ .number("D:(dd)(dd)(dd),") // time (hhmmss)
+ .number("E:(dd)(dd.d+)([NS]),") // latitude
+ .number("F:(ddd)(dd.d+)([EW]),") // longitude
+ .number("G:(d+.d+),") // speed
+ .number("H:(dd)(dd)(dd),") // date (ddmmyy)
+ .expression("I:[GL],") // location source
+ .number("J:(d{8}),") // flags
+ .number("K:(d{7})") // flags
+ .expression("([AV]),") // validity
+ .number("L:d{4},") // pin
+ .number("M:(d+.d+)") // course
+ .text("}")
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("+##Received OK\n", remoteAddress));
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ String flags = parser.next();
+ position.set(Position.KEY_IGNITION, flags.charAt(0) == '1');
+ position.set(Position.KEY_ALARM, flags.charAt(1) == '1' ? Position.ALARM_SOS : null);
+ position.set(Position.KEY_CHARGE, flags.charAt(5) == '1');
+ position.set(Position.KEY_ALARM, flags.charAt(7) == '1' ? Position.ALARM_OVERSPEED : null);
+
+ flags = parser.next();
+ position.set(Position.KEY_BLOCKED, flags.charAt(0) == '1');
+ position.set(Position.KEY_ALARM, flags.charAt(1) == '1' ? Position.ALARM_TOW : null);
+
+ position.setValid(parser.next().equals("A"));
+
+ position.setCourse(parser.nextDouble());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MiniFinderProtocol.java b/src/main/java/org/traccar/protocol/MiniFinderProtocol.java
new file mode 100644
index 000000000..d4a154053
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MiniFinderProtocol.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class MiniFinderProtocol extends BaseProtocol {
+
+ public MiniFinderProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_SET_TIMEZONE,
+ Command.TYPE_VOICE_MONITORING,
+ Command.TYPE_ALARM_SPEED,
+ Command.TYPE_ALARM_GEOFENCE,
+ Command.TYPE_ALARM_VIBRATION,
+ Command.TYPE_SET_AGPS,
+ Command.TYPE_ALARM_FALL,
+ Command.TYPE_MODE_POWER_SAVING,
+ Command.TYPE_MODE_DEEP_SLEEP,
+ Command.TYPE_SOS_NUMBER,
+ Command.TYPE_SET_INDICATOR);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, ';'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new MiniFinderProtocolEncoder());
+ pipeline.addLast(new MiniFinderProtocolDecoder(MiniFinderProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MiniFinderProtocolDecoder.java b/src/main/java/org/traccar/protocol/MiniFinderProtocolDecoder.java
new file mode 100644
index 000000000..2b7a960c4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MiniFinderProtocolDecoder.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class MiniFinderProtocolDecoder extends BaseProtocolDecoder {
+
+ public MiniFinderProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_FIX = new PatternBuilder()
+ .number("(d+)/(d+)/(d+),") // date (dd/mm/yy)
+ .number("(d+):(d+):(d+),") // time (hh:mm:ss)
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .compile();
+
+ private static final Pattern PATTERN_STATE = new PatternBuilder()
+ .number("(d+.?d*),") // speed (km/h)
+ .number("(d+.?d*),") // course
+ .number("(x+),") // flags
+ .number("(-?d+.d+),") // altitude (meters)
+ .number("(d+),") // battery (percentage)
+ .compile();
+
+ private static final Pattern PATTERN_A = new PatternBuilder()
+ .text("!A,")
+ .expression(PATTERN_FIX.pattern())
+ .any() // unknown 3 fields
+ .compile();
+
+ private static final Pattern PATTERN_C = new PatternBuilder()
+ .text("!C,")
+ .expression(PATTERN_FIX.pattern())
+ .expression(PATTERN_STATE.pattern())
+ .any() // unknown 3 fields
+ .compile();
+
+ private static final Pattern PATTERN_BD = new PatternBuilder()
+ .expression("![BD],") // B - buffered, D - live
+ .expression(PATTERN_FIX.pattern())
+ .expression(PATTERN_STATE.pattern())
+ .number("(d+),") // satellites in use
+ .number("(d+),") // satellites in view
+ .number("(d+.?d*)") // hdop
+ .compile();
+
+ private void decodeFix(Position position, Parser parser) {
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ }
+
+ private void decodeFlags(Position position, int flags) {
+
+ position.setValid(BitUtil.to(flags, 2) > 0);
+ if (BitUtil.check(flags, 1)) {
+ position.set(Position.KEY_APPROXIMATE, true);
+ }
+
+ if (BitUtil.check(flags, 2)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_FAULT);
+ }
+ if (BitUtil.check(flags, 6)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ }
+ if (BitUtil.check(flags, 7)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ }
+ if (BitUtil.check(flags, 8)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_FALL_DOWN);
+ }
+ if (BitUtil.check(flags, 9) || BitUtil.check(flags, 10) || BitUtil.check(flags, 11)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE);
+ }
+ if (BitUtil.check(flags, 12)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ }
+ if (BitUtil.check(flags, 15) || BitUtil.check(flags, 14)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_MOVEMENT);
+ }
+
+ position.set(Position.KEY_RSSI, BitUtil.between(flags, 16, 21));
+ position.set(Position.KEY_CHARGE, BitUtil.check(flags, 22));
+ }
+
+ private void decodeState(Position position, Parser parser) {
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+
+ position.setCourse(parser.nextDouble(0));
+ if (position.getCourse() > 360) {
+ position.setCourse(0);
+ }
+
+ decodeFlags(position, parser.nextHexInt(0));
+
+ position.setAltitude(parser.nextDouble(0));
+
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt(0));
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("!1,")) {
+ int index = sentence.indexOf(',', 3);
+ if (index < 0) {
+ index = sentence.length();
+ }
+ getDeviceSession(channel, remoteAddress, sentence.substring(3, index));
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null || !sentence.matches("![3A-D],.*")) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ String type = sentence.substring(1, 2);
+ position.set(Position.KEY_TYPE, type);
+
+ if (type.equals("3")) {
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_RESULT, sentence.substring(3));
+
+ return position;
+
+ } else if (type.equals("B") || type.equals("D")) {
+
+ Parser parser = new Parser(PATTERN_BD, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ decodeFix(position, parser);
+ decodeState(position, parser);
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_SATELLITES_VISIBLE, parser.nextInt(0));
+ position.set(Position.KEY_HDOP, parser.nextDouble(0));
+
+ return position;
+
+ } else if (type.equals("C")) {
+
+ Parser parser = new Parser(PATTERN_C, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ decodeFix(position, parser);
+ decodeState(position, parser);
+
+ return position;
+
+ } else if (type.equals("A")) {
+
+ Parser parser = new Parser(PATTERN_A, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ decodeFix(position, parser);
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MiniFinderProtocolEncoder.java b/src/main/java/org/traccar/protocol/MiniFinderProtocolEncoder.java
new file mode 100644
index 000000000..7a3d5b226
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MiniFinderProtocolEncoder.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import java.util.TimeZone;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class MiniFinderProtocolEncoder extends StringProtocolEncoder implements StringProtocolEncoder.ValueFormatter {
+
+ @Override
+ public String formatValue(String key, Object value) {
+ switch (key) {
+ case Command.KEY_ENABLE:
+ return (Boolean) value ? "1" : "0";
+ case Command.KEY_TIMEZONE:
+ return String.format("%+03d", TimeZone.getTimeZone((String) value).getRawOffset() / 3600000);
+ case Command.KEY_INDEX:
+ switch (((Number) value).intValue()) {
+ case 0:
+ return "A";
+ case 1:
+ return "B";
+ case 2:
+ return "C";
+ default:
+ return null;
+ }
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ initDevicePassword(command, "123456");
+
+ switch (command.getType()) {
+ case Command.TYPE_SET_TIMEZONE:
+ return formatCommand(command, "{%s}L{%s}", this, Command.KEY_DEVICE_PASSWORD, Command.KEY_TIMEZONE);
+ case Command.TYPE_VOICE_MONITORING:
+ return formatCommand(command, "{%s}P{%s}", this, Command.KEY_DEVICE_PASSWORD, Command.KEY_ENABLE);
+ case Command.TYPE_ALARM_SPEED:
+ return formatCommand(command, "{%s}J1{%s}", Command.KEY_DEVICE_PASSWORD, Command.KEY_DATA);
+ case Command.TYPE_ALARM_GEOFENCE:
+ return formatCommand(command, "{%s}R1{%s}", Command.KEY_DEVICE_PASSWORD, Command.KEY_RADIUS);
+ case Command.TYPE_ALARM_VIBRATION:
+ return formatCommand(command, "{%s}W1,{%s}", Command.KEY_DEVICE_PASSWORD, Command.KEY_DATA);
+ case Command.TYPE_SET_AGPS:
+ return formatCommand(command, "{%s}AGPS{%s}", this, Command.KEY_DEVICE_PASSWORD, Command.KEY_ENABLE);
+ case Command.TYPE_ALARM_FALL:
+ return formatCommand(command, "{%s}F{%s}", this, Command.KEY_DEVICE_PASSWORD, Command.KEY_ENABLE);
+ case Command.TYPE_MODE_POWER_SAVING:
+ return formatCommand(command, "{%s}SP{%s}", this, Command.KEY_DEVICE_PASSWORD, Command.KEY_ENABLE);
+ case Command.TYPE_MODE_DEEP_SLEEP:
+ return formatCommand(command, "{%s}DS{%s}", this, Command.KEY_DEVICE_PASSWORD, Command.KEY_ENABLE);
+ case Command.TYPE_SOS_NUMBER:
+ return formatCommand(command, "{%s}{%s}1,{%s}", this,
+ Command.KEY_DEVICE_PASSWORD, Command.KEY_INDEX, Command.KEY_PHONE);
+ case Command.TYPE_SET_INDICATOR:
+ return formatCommand(command, "{%s}LED{%s}", Command.KEY_DEVICE_PASSWORD, Command.KEY_DATA);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Mta6Protocol.java b/src/main/java/org/traccar/protocol/Mta6Protocol.java
new file mode 100644
index 000000000..632a7df80
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Mta6Protocol.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.Context;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Mta6Protocol extends BaseProtocol {
+
+ public Mta6Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(65535));
+ pipeline.addLast(new Mta6ProtocolDecoder(
+ Mta6Protocol.this, !Context.getConfig().getBoolean(getName() + ".can")));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java
new file mode 100644
index 000000000..88419b871
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Mta6ProtocolDecoder.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class Mta6ProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Mta6ProtocolDecoder.class);
+
+ private final boolean simple;
+
+ public Mta6ProtocolDecoder(Protocol protocol, boolean simple) {
+ super(protocol);
+ this.simple = simple;
+ }
+
+ private void sendContinue(Channel channel) {
+ FullHttpResponse response = new DefaultFullHttpResponse(
+ HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+
+ private void sendResponse(Channel channel, short packetId, short packetCount) {
+ ByteBuf begin = Unpooled.copiedBuffer("#ACK#", StandardCharsets.US_ASCII);
+ ByteBuf end = Unpooled.buffer(3);
+ end.writeByte(packetId);
+ end.writeByte(packetCount);
+ end.writeByte(0);
+
+ FullHttpResponse response = new DefaultFullHttpResponse(
+ HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(begin, end));
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+
+ private static class FloatReader {
+
+ private int previousFloat;
+
+ public float readFloat(ByteBuf buf) {
+ switch (buf.getUnsignedByte(buf.readerIndex()) >> 6) {
+ case 0:
+ previousFloat = buf.readInt() << 2;
+ break;
+ case 1:
+ previousFloat = (previousFloat & 0xffffff00) + ((buf.readUnsignedByte() & 0x3f) << 2);
+ break;
+ case 2:
+ previousFloat = (previousFloat & 0xffff0000) + ((buf.readUnsignedShort() & 0x3fff) << 2);
+ break;
+ case 3:
+ previousFloat = (previousFloat & 0xff000000) + ((buf.readUnsignedMedium() & 0x3fffff) << 2);
+ break;
+ default:
+ LOGGER.warn("MTA6 float decoding error", new IllegalArgumentException());
+ break;
+ }
+ return Float.intBitsToFloat(previousFloat);
+ }
+
+ }
+
+ private static class TimeReader extends FloatReader {
+
+ private long weekNumber;
+
+ public Date readTime(ByteBuf buf) {
+ long weekTime = (long) (readFloat(buf) * 1000);
+ if (weekNumber == 0) {
+ weekNumber = buf.readUnsignedShort();
+ }
+
+ DateBuilder dateBuilder = new DateBuilder().setDate(1980, 1, 6);
+ dateBuilder.addMillis(weekNumber * 7 * 24 * 60 * 60 * 1000 + weekTime);
+
+ return dateBuilder.getDate();
+ }
+
+ }
+
+ private List<Position> parseFormatA(DeviceSession deviceSession, ByteBuf buf) {
+ List<Position> positions = new LinkedList<>();
+
+ FloatReader latitudeReader = new FloatReader();
+ FloatReader longitudeReader = new FloatReader();
+ TimeReader timeReader = new TimeReader();
+
+ try {
+ while (buf.isReadable()) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ short flags = buf.readUnsignedByte();
+
+ short event = buf.readUnsignedByte();
+ if (BitUtil.check(event, 7)) {
+ if (BitUtil.check(event, 6)) {
+ buf.skipBytes(8);
+ } else {
+ while (BitUtil.check(event, 7)) {
+ event = buf.readUnsignedByte();
+ }
+ }
+ }
+
+ position.setLatitude(latitudeReader.readFloat(buf) / Math.PI * 180);
+ position.setLongitude(longitudeReader.readFloat(buf) / Math.PI * 180);
+ position.setTime(timeReader.readTime(buf));
+
+ if (BitUtil.check(flags, 0)) {
+ buf.readUnsignedByte(); // status
+ }
+
+ if (BitUtil.check(flags, 1)) {
+ position.setAltitude(buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(flags, 2)) {
+ position.setSpeed(buf.readUnsignedShort() & 0x03ff);
+ position.setCourse(buf.readUnsignedByte());
+ }
+
+ if (BitUtil.check(flags, 3)) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedShort() * 1000);
+ }
+
+ if (BitUtil.check(flags, 4)) {
+ position.set(Position.KEY_FUEL_CONSUMPTION + "Accumulator1", buf.readUnsignedInt());
+ position.set(Position.KEY_FUEL_CONSUMPTION + "Accumulator2", buf.readUnsignedInt());
+ position.set("hours1", buf.readUnsignedShort());
+ position.set("hours2", buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(flags, 5)) {
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort() & 0x03ff);
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShort() & 0x03ff);
+ position.set(Position.PREFIX_ADC + 3, buf.readUnsignedShort() & 0x03ff);
+ position.set(Position.PREFIX_ADC + 4, buf.readUnsignedShort() & 0x03ff);
+ }
+
+ if (BitUtil.check(flags, 6)) {
+ position.set(Position.PREFIX_TEMP + 1, buf.readByte());
+ buf.getUnsignedByte(buf.readerIndex()); // control (>> 4)
+ position.set(Position.KEY_INPUT, buf.readUnsignedShort() & 0x0fff);
+ buf.readUnsignedShort(); // old sensor state (& 0x0fff)
+ }
+
+ if (BitUtil.check(flags, 7)) {
+ position.set(Position.KEY_BATTERY, buf.getUnsignedByte(buf.readerIndex()) >> 2);
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() & 0x03ff);
+ position.set(Position.KEY_DEVICE_TEMP, buf.readByte());
+
+ position.set(Position.KEY_RSSI, (buf.getUnsignedByte(buf.readerIndex()) >> 4) & 0x07);
+
+ int satellites = buf.readUnsignedByte() & 0x0f;
+ position.setValid(satellites >= 3);
+ position.set(Position.KEY_SATELLITES, satellites);
+ }
+ positions.add(position);
+ }
+ } catch (IndexOutOfBoundsException error) {
+ LOGGER.warn("MTA6 parsing error", error);
+ }
+
+ return positions;
+ }
+
+ private Position parseFormatA1(DeviceSession deviceSession, ByteBuf buf) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ short flags = buf.readUnsignedByte();
+
+ // Skip events
+ short event = buf.readUnsignedByte();
+ if (BitUtil.check(event, 7)) {
+ if (BitUtil.check(event, 6)) {
+ buf.skipBytes(8);
+ } else {
+ while (BitUtil.check(event, 7)) {
+ event = buf.readUnsignedByte();
+ }
+ }
+ }
+
+ position.setLatitude(new FloatReader().readFloat(buf) / Math.PI * 180);
+ position.setLongitude(new FloatReader().readFloat(buf) / Math.PI * 180);
+ position.setTime(new TimeReader().readTime(buf));
+
+ position.set(Position.KEY_STATUS, buf.readUnsignedByte());
+
+ if (BitUtil.check(flags, 0)) {
+ position.setAltitude(buf.readUnsignedShort());
+ position.setSpeed(buf.readUnsignedByte());
+ position.setCourse(buf.readByte());
+ position.set(Position.KEY_ODOMETER, new FloatReader().readFloat(buf));
+ }
+
+ if (BitUtil.check(flags, 1)) {
+ position.set(Position.KEY_FUEL_CONSUMPTION, new FloatReader().readFloat(buf));
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromHours(new FloatReader().readFloat(buf)));
+ position.set("tank", buf.readUnsignedByte() * 0.4);
+ }
+
+ if (BitUtil.check(flags, 2)) {
+ position.set("engine", buf.readUnsignedShort() * 0.125);
+ position.set("pedals", buf.readUnsignedByte());
+ position.set(Position.PREFIX_TEMP + 1, buf.readUnsignedByte() - 40);
+ position.set(Position.KEY_ODOMETER_SERVICE, buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(flags, 3)) {
+ position.set(Position.KEY_FUEL_LEVEL, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 3, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 4, buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(flags, 4)) {
+ position.set(Position.PREFIX_TEMP + 1, buf.readByte());
+ buf.getUnsignedByte(buf.readerIndex()); // control (>> 4)
+ position.set(Position.KEY_INPUT, buf.readUnsignedShort() & 0x0fff);
+ buf.readUnsignedShort(); // old sensor state (& 0x0fff)
+ }
+
+ if (BitUtil.check(flags, 5)) {
+ position.set(Position.KEY_BATTERY, buf.getUnsignedByte(buf.readerIndex()) >> 2);
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() & 0x03ff);
+ position.set(Position.KEY_DEVICE_TEMP, buf.readByte());
+
+ position.set(Position.KEY_RSSI, buf.getUnsignedByte(buf.readerIndex()) >> 5);
+
+ int satellites = buf.readUnsignedByte() & 0x1f;
+ position.setValid(satellites >= 3);
+ position.set(Position.KEY_SATELLITES, satellites);
+ }
+
+ // other data
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ ByteBuf buf = request.content();
+
+ buf.skipBytes("id=".length());
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '&');
+ String uniqueId = buf.toString(buf.readerIndex(), index - buf.readerIndex(), StandardCharsets.US_ASCII);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, uniqueId);
+ if (deviceSession == null) {
+ return null;
+ }
+ buf.skipBytes(uniqueId.length());
+ buf.skipBytes("&bin=".length());
+
+ short packetId = buf.readUnsignedByte();
+ short offset = buf.readUnsignedByte(); // dataOffset
+ short packetCount = buf.readUnsignedByte();
+ buf.readUnsignedByte(); // reserved
+ buf.readUnsignedByte(); // timezone
+ buf.skipBytes(offset - 5);
+
+ if (channel != null) {
+ sendContinue(channel);
+ sendResponse(channel, packetId, packetCount);
+ }
+
+ if (packetId == 0x31 || packetId == 0x32 || packetId == 0x36) {
+ if (simple) {
+ return parseFormatA1(deviceSession, buf);
+ } else {
+ return parseFormatA(deviceSession, buf);
+ }
+ } // else if (0x34 0x38 0x4F 0x59)
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MtxProtocol.java b/src/main/java/org/traccar/protocol/MtxProtocol.java
new file mode 100644
index 000000000..44372ce83
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MtxProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class MtxProtocol extends BaseProtocol {
+
+ public MtxProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new MtxProtocolDecoder(MtxProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MtxProtocolDecoder.java b/src/main/java/org/traccar/protocol/MtxProtocolDecoder.java
new file mode 100644
index 000000000..d1207bedf
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MtxProtocolDecoder.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class MtxProtocolDecoder extends BaseProtocolDecoder {
+
+ public MtxProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("#MTX,")
+ .number("(d+),") // imei
+ .number("(dddd)(dd)(dd),") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(d+.?d*),") // speed
+ .number("(d+),") // course
+ .number("(d+.?d*),") // odometer
+ .groupBegin()
+ .number("d+")
+ .or()
+ .text("X")
+ .groupEnd()
+ .text(",")
+ .expression("(?:[01]|X),")
+ .expression("([01]+),") // input
+ .expression("([01]+),") // output
+ .number("(d+),") // adc1
+ .number("(d+)") // adc2
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("#ACK", remoteAddress));
+ }
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(true);
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_ODOMETER, parser.nextDouble(0) * 1000);
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MxtFrameDecoder.java b/src/main/java/org/traccar/protocol/MxtFrameDecoder.java
new file mode 100644
index 000000000..d70e92da1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MxtFrameDecoder.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class MxtFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 2) {
+ return null;
+ }
+
+ int index = buf.indexOf(buf.readerIndex() + 1, buf.writerIndex(), (byte) 0x04);
+ if (index != -1) {
+ ByteBuf result = Unpooled.buffer(index + 1 - buf.readerIndex());
+
+ while (buf.readerIndex() <= index) {
+ int b = buf.readUnsignedByte();
+ if (b == 0x10) {
+ result.writeByte(buf.readUnsignedByte() - 0x20);
+ } else {
+ result.writeByte(b);
+ }
+ }
+
+ return result;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MxtProtocol.java b/src/main/java/org/traccar/protocol/MxtProtocol.java
new file mode 100644
index 000000000..dbe43fe45
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MxtProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class MxtProtocol extends BaseProtocol {
+
+ public MxtProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new MxtFrameDecoder());
+ pipeline.addLast(new MxtProtocolDecoder(MxtProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/MxtProtocolDecoder.java b/src/main/java/org/traccar/protocol/MxtProtocolDecoder.java
new file mode 100644
index 000000000..7bde85f87
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/MxtProtocolDecoder.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class MxtProtocolDecoder extends BaseProtocolDecoder {
+
+ public MxtProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_ACK = 0x02;
+ public static final int MSG_NACK = 0x03;
+ public static final int MSG_POSITION = 0x31;
+
+ private static void sendResponse(Channel channel, int device, long id, int crc) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(device);
+ response.writeByte(MSG_ACK);
+ response.writeIntLE((int) id);
+ response.writeShortLE(crc);
+ response.writeShortLE(Checksum.crc16(
+ Checksum.CRC16_XMODEM, response.nioBuffer()));
+
+ ByteBuf encoded = Unpooled.buffer();
+ encoded.writeByte(0x01); // header
+ while (response.isReadable()) {
+ int b = response.readByte();
+ if (b == 0x01 || b == 0x04 || b == 0x10 || b == 0x11 || b == 0x13) {
+ encoded.writeByte(0x10);
+ b += 0x20;
+ }
+ encoded.writeByte(b);
+ }
+ response.release();
+ encoded.writeByte(0x04); // ending
+ channel.writeAndFlush(new NetworkMessage(encoded, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedByte(); // start
+ int device = buf.readUnsignedByte(); // device descriptor
+ int type = buf.readUnsignedByte();
+
+ long id = buf.readUnsignedIntLE();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(id));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (type == MSG_POSITION) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedByte(); // protocol
+ int infoGroups = buf.readUnsignedByte();
+
+ position.set(Position.KEY_INDEX, buf.readUnsignedShortLE());
+
+ DateBuilder dateBuilder = new DateBuilder().setDate(2000, 1, 1);
+
+ long date = buf.readUnsignedIntLE();
+
+ long days = BitUtil.from(date, 6 + 6 + 5);
+ long hours = BitUtil.between(date, 6 + 6, 6 + 6 + 5);
+ long minutes = BitUtil.between(date, 6, 6 + 6);
+ long seconds = BitUtil.to(date, 6);
+
+ dateBuilder.addMillis((((days * 24 + hours) * 60 + minutes) * 60 + seconds) * 1000);
+
+ position.setTime(dateBuilder.getDate());
+
+ position.setValid(true);
+ position.setLatitude(buf.readIntLE() / 1000000.0);
+ position.setLongitude(buf.readIntLE() / 1000000.0);
+
+ long flags = buf.readUnsignedIntLE();
+ position.set(Position.KEY_IGNITION, BitUtil.check(flags, 0));
+ if (BitUtil.check(flags, 1)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ position.set(Position.KEY_INPUT, BitUtil.between(flags, 2, 7));
+ position.set(Position.KEY_OUTPUT, BitUtil.between(flags, 7, 10));
+ position.setCourse(BitUtil.between(flags, 10, 13) * 45);
+ // position.setValid(BitUtil.check(flags, 15));
+ position.set(Position.KEY_CHARGE, BitUtil.check(flags, 20));
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+
+ buf.readUnsignedByte(); // input mask
+
+ if (BitUtil.check(infoGroups, 0)) {
+ buf.skipBytes(8); // waypoints
+ }
+
+ if (BitUtil.check(infoGroups, 1)) {
+ buf.skipBytes(8); // wireless accessory
+ }
+
+ if (BitUtil.check(infoGroups, 2)) {
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte());
+ position.setAccuracy(buf.readUnsignedByte());
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ buf.readUnsignedShortLE(); // time since boot
+ position.set(Position.KEY_POWER, buf.readUnsignedByte());
+ position.set(Position.PREFIX_TEMP + 1, buf.readByte());
+ }
+
+ if (BitUtil.check(infoGroups, 3)) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ }
+
+ if (BitUtil.check(infoGroups, 4)) {
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromMinutes(buf.readUnsignedIntLE()));
+ }
+
+ if (BitUtil.check(infoGroups, 5)) {
+ buf.readUnsignedIntLE(); // reason
+ }
+
+ if (BitUtil.check(infoGroups, 6)) {
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() * 0.001);
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE());
+ }
+
+ if (BitUtil.check(infoGroups, 7)) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, String.valueOf(buf.readUnsignedIntLE()));
+ }
+
+ buf.readerIndex(buf.writerIndex() - 3);
+ sendResponse(channel, device, id, buf.readUnsignedShortLE());
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NavigilFrameDecoder.java b/src/main/java/org/traccar/protocol/NavigilFrameDecoder.java
new file mode 100644
index 000000000..e8b6bea52
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NavigilFrameDecoder.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class NavigilFrameDecoder extends BaseFrameDecoder {
+
+ private static final int MESSAGE_HEADER = 20;
+ private static final long PREAMBLE = 0x2477F5F6;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ // Check minimum length
+ if (buf.readableBytes() < MESSAGE_HEADER) {
+ return null;
+ }
+
+ // Check for preamble
+ boolean hasPreamble = false;
+ if (buf.getUnsignedIntLE(buf.readerIndex()) == PREAMBLE) {
+ hasPreamble = true;
+ }
+
+ // Check length and return buffer
+ int length = buf.getUnsignedShortLE(buf.readerIndex() + 6);
+ if (buf.readableBytes() >= length) {
+ if (hasPreamble) {
+ buf.readUnsignedIntLE();
+ length -= 4;
+ }
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NavigilProtocol.java b/src/main/java/org/traccar/protocol/NavigilProtocol.java
new file mode 100644
index 000000000..2c946c39f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NavigilProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class NavigilProtocol extends BaseProtocol {
+
+ public NavigilProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new NavigilFrameDecoder());
+ pipeline.addLast(new NavigilProtocolDecoder(NavigilProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NavigilProtocolDecoder.java b/src/main/java/org/traccar/protocol/NavigilProtocolDecoder.java
new file mode 100644
index 000000000..db5521201
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NavigilProtocolDecoder.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+
+public class NavigilProtocolDecoder extends BaseProtocolDecoder {
+
+ public NavigilProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final int LEAP_SECONDS_DELTA = 25;
+
+ public static final int MSG_ERROR = 2;
+ public static final int MSG_INDICATION = 4;
+ public static final int MSG_CONN_OPEN = 5;
+ public static final int MSG_CONN_CLOSE = 6;
+ public static final int MSG_SYSTEM_REPORT = 7;
+ public static final int MSG_UNIT_REPORT = 8;
+ public static final int MSG_GEOFENCE_ALARM = 10;
+ public static final int MSG_INPUT_ALARM = 11;
+ public static final int MSG_TG2_REPORT = 12;
+ public static final int MSG_POSITION_REPORT = 13;
+ public static final int MSG_POSITION_REPORT_2 = 15;
+ public static final int MSG_SNAPSHOT4 = 17;
+ public static final int MSG_TRACKING_DATA = 18;
+ public static final int MSG_MOTION_ALARM = 19;
+ public static final int MSG_ACKNOWLEDGEMENT = 255;
+
+ private static Date convertTimestamp(long timestamp) {
+ return new Date((timestamp - LEAP_SECONDS_DELTA) * 1000);
+ }
+
+ private int senderSequenceNumber = 1;
+
+ private void sendAcknowledgment(Channel channel, int sequenceNumber) {
+ ByteBuf data = Unpooled.buffer(4);
+ data.writeShortLE(sequenceNumber);
+ data.writeShortLE(0); // OK
+
+ ByteBuf header = Unpooled.buffer(20);
+ header.writeByte(1); header.writeByte(0);
+ header.writeShortLE(senderSequenceNumber++);
+ header.writeShortLE(MSG_ACKNOWLEDGEMENT);
+ header.writeShortLE(header.capacity() + data.capacity());
+ header.writeShortLE(0);
+ header.writeShortLE(Checksum.crc16(Checksum.CRC16_CCITT_FALSE, data.nioBuffer()));
+ header.writeIntLE(0);
+ header.writeIntLE((int) (System.currentTimeMillis() / 1000) + LEAP_SECONDS_DELTA);
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(Unpooled.wrappedBuffer(header, data), channel.remoteAddress()));
+ }
+ }
+
+ private Position parseUnitReport(
+ DeviceSession deviceSession, ByteBuf buf, int sequenceNumber) {
+ Position position = new Position(getProtocolName());
+
+ position.setValid(true);
+ position.set(Position.KEY_INDEX, sequenceNumber);
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedShortLE(); // report trigger
+ position.set(Position.KEY_FLAGS, buf.readUnsignedShortLE());
+
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+ position.setAltitude(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedShortLE());
+ position.set(Position.KEY_SATELLITES_VISIBLE, buf.readUnsignedShortLE());
+ position.set("gpsAntennaState", buf.readUnsignedShortLE());
+
+ position.setSpeed(buf.readUnsignedShortLE() * 0.194384);
+ position.setCourse(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ position.set(Position.KEY_DISTANCE, buf.readUnsignedIntLE());
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.001);
+
+ position.set(Position.KEY_CHARGE, buf.readUnsignedShortLE());
+
+ position.setTime(convertTimestamp(buf.readUnsignedIntLE()));
+
+ return position;
+ }
+
+ private Position parseTg2Report(
+ DeviceSession deviceSession, ByteBuf buf, int sequenceNumber) {
+ Position position = new Position(getProtocolName());
+
+ position.setValid(true);
+ position.set(Position.KEY_INDEX, sequenceNumber);
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedShortLE(); // report trigger
+ buf.readUnsignedByte(); // reserved
+ buf.readUnsignedByte(); // assisted GPS age
+
+ position.setTime(convertTimestamp(buf.readUnsignedIntLE()));
+
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+ position.setAltitude(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_SATELLITES_VISIBLE, buf.readUnsignedByte());
+
+ position.setSpeed(buf.readUnsignedShortLE() * 0.194384);
+ position.setCourse(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+ position.set("maximumSpeed", buf.readUnsignedShortLE());
+ position.set("minimumSpeed", buf.readUnsignedShortLE());
+
+ position.set(Position.PREFIX_IO + 1, buf.readUnsignedShortLE()); // VSAUT1 voltage
+ position.set(Position.PREFIX_IO + 2, buf.readUnsignedShortLE()); // VSAUT2 voltage
+ position.set(Position.PREFIX_IO + 3, buf.readUnsignedShortLE()); // solar voltage
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.001);
+
+ return position;
+ }
+
+ private Position parsePositionReport(
+ DeviceSession deviceSession, ByteBuf buf, int sequenceNumber, long timestamp) {
+ Position position = new Position(getProtocolName());
+
+ position.set(Position.KEY_INDEX, sequenceNumber);
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setTime(convertTimestamp(timestamp));
+
+ position.setLatitude(buf.readMediumLE() * 0.00002);
+ position.setLongitude(buf.readMediumLE() * 0.00002);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.setCourse(buf.readUnsignedByte() * 2);
+
+ short flags = buf.readUnsignedByte();
+ position.setValid((flags & 0x80) == 0x80 && (flags & 0x40) == 0x40);
+
+ buf.readUnsignedByte(); // reserved
+
+ return position;
+ }
+
+ private Position parsePositionReport2(
+ DeviceSession deviceSession, ByteBuf buf, int sequenceNumber, long timestamp) {
+ Position position = new Position(getProtocolName());
+
+ position.set(Position.KEY_INDEX, sequenceNumber);
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setTime(convertTimestamp(timestamp));
+
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+
+ buf.readUnsignedByte(); // report trigger
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+
+ short flags = buf.readUnsignedByte();
+ position.setValid((flags & 0x80) == 0x80 && (flags & 0x40) == 0x40);
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+
+ return position;
+ }
+
+ private Position parseSnapshot4(
+ DeviceSession deviceSession, ByteBuf buf, int sequenceNumber) {
+ Position position = new Position(getProtocolName());
+
+ position.set(Position.KEY_INDEX, sequenceNumber);
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedByte(); // report trigger
+ buf.readUnsignedByte(); // position fix source
+ buf.readUnsignedByte(); // GNSS fix quality
+ buf.readUnsignedByte(); // GNSS assistance age
+
+ long flags = buf.readUnsignedIntLE();
+ position.setValid((flags & 0x0400) == 0x0400);
+
+ position.setTime(convertTimestamp(buf.readUnsignedIntLE()));
+
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+ position.setAltitude(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_SATELLITES_VISIBLE, buf.readUnsignedByte());
+
+ position.setSpeed(buf.readUnsignedShortLE() * 0.194384);
+ position.setCourse(buf.readUnsignedShortLE() * 0.1);
+
+ position.set("maximumSpeed", buf.readUnsignedByte());
+ position.set("minimumSpeed", buf.readUnsignedByte());
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+
+ position.set(Position.PREFIX_IO + 1, buf.readUnsignedByte()); // supply voltage 1
+ position.set(Position.PREFIX_IO + 2, buf.readUnsignedByte()); // supply voltage 2
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.001);
+
+ return position;
+ }
+
+ private Position parseTrackingData(
+ DeviceSession deviceSession, ByteBuf buf, int sequenceNumber, long timestamp) {
+ Position position = new Position(getProtocolName());
+
+ position.set(Position.KEY_INDEX, sequenceNumber);
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setTime(convertTimestamp(timestamp));
+
+ buf.readUnsignedByte(); // tracking mode
+
+ short flags = buf.readUnsignedByte();
+ position.setValid((flags & 0x01) == 0x01);
+
+ buf.readUnsignedShortLE(); // duration
+
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.setCourse(buf.readUnsignedByte() * 2.0);
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.001);
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedByte(); // protocol version
+ buf.readUnsignedByte(); // version id
+ int sequenceNumber = buf.readUnsignedShortLE();
+ int messageId = buf.readUnsignedShortLE();
+ buf.readUnsignedShortLE(); // length
+ int flags = buf.readUnsignedShortLE();
+ buf.readUnsignedShortLE(); // checksum
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(buf.readUnsignedIntLE()));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ long timestamp = buf.readUnsignedIntLE();
+
+ if ((flags & 0x1) == 0x0) {
+ sendAcknowledgment(channel, sequenceNumber);
+ }
+
+ switch (messageId) {
+ case MSG_UNIT_REPORT:
+ return parseUnitReport(deviceSession, buf, sequenceNumber);
+ case MSG_TG2_REPORT:
+ return parseTg2Report(deviceSession, buf, sequenceNumber);
+ case MSG_POSITION_REPORT:
+ return parsePositionReport(deviceSession, buf, sequenceNumber, timestamp);
+ case MSG_POSITION_REPORT_2:
+ return parsePositionReport2(deviceSession, buf, sequenceNumber, timestamp);
+ case MSG_SNAPSHOT4:
+ return parseSnapshot4(deviceSession, buf, sequenceNumber);
+ case MSG_TRACKING_DATA:
+ return parseTrackingData(deviceSession, buf, sequenceNumber, timestamp);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NavisFrameDecoder.java b/src/main/java/org/traccar/protocol/NavisFrameDecoder.java
new file mode 100644
index 000000000..8a0bb0b9a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NavisFrameDecoder.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import java.nio.charset.StandardCharsets;
+import org.traccar.BaseFrameDecoder;
+import org.traccar.BasePipelineFactory;
+
+public class NavisFrameDecoder extends BaseFrameDecoder {
+
+ private static final int NTCB_HEADER_LENGTH = 16;
+ private static final int NTCB_LENGTH_OFFSET = 12;
+ private static final int FLEX_HEADER_LENGTH = 2;
+
+ private int flexDataSize;
+
+ public void setFlexDataSize(int flexDataSize) {
+ this.flexDataSize = flexDataSize;
+ }
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.getByte(buf.readerIndex()) == 0x7F) {
+ return buf.readRetainedSlice(1); // keep alive
+ }
+
+ if (ctx != null && flexDataSize == 0) {
+ NavisProtocolDecoder protocolDecoder =
+ BasePipelineFactory.getHandler(ctx.pipeline(), NavisProtocolDecoder.class);
+ if (protocolDecoder != null) {
+ flexDataSize = protocolDecoder.getFlexDataSize();
+ }
+ }
+
+ if (flexDataSize > 0) {
+
+ if (buf.readableBytes() > FLEX_HEADER_LENGTH) {
+ int length = 0;
+ String type = buf.toString(buf.readerIndex(), 2, StandardCharsets.US_ASCII);
+ switch (type) {
+ // FLEX 1.0
+ case "~A":
+ length = flexDataSize * buf.getByte(buf.readerIndex() + FLEX_HEADER_LENGTH) + 1 + 1;
+ break;
+ case "~T":
+ length = flexDataSize + 4 + 1;
+ break;
+ case "~C":
+ length = flexDataSize + 1;
+ break;
+ // FLEX 2.0 (Extra packages)
+ case "~E":
+ length++;
+ for (int i = 0; i < buf.getByte(buf.readerIndex() + FLEX_HEADER_LENGTH); i++) {
+ if (buf.readableBytes() > FLEX_HEADER_LENGTH + length + 1) {
+ length += buf.getUnsignedShort(length + FLEX_HEADER_LENGTH) + 2;
+ } else {
+ return null;
+ }
+ }
+ length++;
+ break;
+ case "~X":
+ length = buf.getUnsignedShortLE(buf.readerIndex() + FLEX_HEADER_LENGTH) + 4 + 1;
+ break;
+ default:
+ return null;
+ }
+
+ if (buf.readableBytes() >= FLEX_HEADER_LENGTH + length) {
+ return buf.readRetainedSlice(buf.readableBytes());
+ }
+ }
+
+ } else {
+
+ if (buf.readableBytes() < NTCB_HEADER_LENGTH) {
+ return null;
+ }
+
+ int length = NTCB_HEADER_LENGTH + buf.getUnsignedShortLE(buf.readerIndex() + NTCB_LENGTH_OFFSET);
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NavisProtocol.java b/src/main/java/org/traccar/protocol/NavisProtocol.java
new file mode 100644
index 000000000..d5af6838d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NavisProtocol.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class NavisProtocol extends BaseProtocol {
+
+ public NavisProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new NavisFrameDecoder());
+ pipeline.addLast(new NavisProtocolDecoder(NavisProtocol.this));
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/traccar/protocol/NavisProtocolDecoder.java b/src/main/java/org/traccar/protocol/NavisProtocolDecoder.java
new file mode 100644
index 000000000..7ba474ae0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NavisProtocolDecoder.java
@@ -0,0 +1,683 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.Checksum.Algorithm;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Date;
+
+public class NavisProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final int[] FLEX_FIELDS_SIZES = {
+ 4, 2, 4, 1, 1, 1, 1, 1, 4, 4, 4, 4, 4, 2, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 2, 2,
+ 4, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4, 2, 1, 4, 2, 2, 2, 2, 2, 1, 1, 1, 2, 4, 2, 1,
+ /* FLEX 2.0 */
+ 8, 2, 1, 16, 4, 2, 4, 37, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6, 12, 24, 48, 1, 1, 1, 1, 4, 4,
+ 1, 4, 2, 6, 2, 6, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1
+ };
+
+ private String prefix;
+ private long deviceUniqueId, serverId;
+ private int flexDataSize;
+ private int flexBitFieldSize;
+ private final byte[] flexBitField = new byte[16];
+
+ public NavisProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int F10 = 0x01;
+ public static final int F20 = 0x02;
+ public static final int F30 = 0x03;
+ public static final int F40 = 0x04;
+ public static final int F50 = 0x05;
+ public static final int F51 = 0x15;
+ public static final int F52 = 0x25;
+ public static final int F60 = 0x06;
+
+ public int getFlexDataSize() {
+ return flexDataSize;
+ }
+
+ private static boolean isFormat(int type, int... types) {
+ for (int i : types) {
+ if (type == i) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Position parseNtcbPosition(DeviceSession deviceSession, ByteBuf buf) {
+ Position position = new Position(getProtocolName());
+
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int format;
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0) {
+ format = buf.readUnsignedShortLE();
+ } else {
+ format = buf.readUnsignedByte();
+ }
+ position.set("format", format);
+
+ position.set(Position.KEY_INDEX, buf.readUnsignedIntLE());
+ position.set(Position.KEY_EVENT, buf.readUnsignedShortLE());
+
+ buf.skipBytes(6); // event time
+
+ short armedStatus = buf.readUnsignedByte();
+ if (isFormat(format, F10, F20, F30, F40, F50, F51, F52)) {
+ position.set(Position.KEY_ARMED, BitUtil.to(armedStatus, 7));
+ if (BitUtil.check(armedStatus, 7)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ } else if (isFormat(format, F60)) {
+ position.set(Position.KEY_ARMED, BitUtil.check(armedStatus, 0));
+ if (BitUtil.check(armedStatus, 1)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ }
+
+ position.set(Position.KEY_STATUS, buf.readUnsignedByte());
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+
+ if (isFormat(format, F10, F20, F30)) {
+ int output = buf.readUnsignedShortLE();
+ position.set(Position.KEY_OUTPUT, output);
+ for (int i = 0; i < 16; i++) {
+ position.set(Position.PREFIX_OUT + (i + 1), BitUtil.check(output, i));
+ }
+ } else if (isFormat(format, F50, F51, F52)) {
+ short extField = buf.readUnsignedByte();
+ position.set(Position.KEY_OUTPUT, BitUtil.to(extField, 2));
+ position.set(Position.PREFIX_OUT + 1, BitUtil.check(extField, 0));
+ position.set(Position.PREFIX_OUT + 2, BitUtil.check(extField, 1));
+ position.set(Position.KEY_SATELLITES, BitUtil.from(extField, 2));
+ } else if (isFormat(format, F40, F60)) {
+ short output = buf.readUnsignedByte();
+ position.set(Position.KEY_OUTPUT, BitUtil.to(output, 4));
+ for (int i = 0; i < 4; i++) {
+ position.set(Position.PREFIX_OUT + (i + 1), BitUtil.check(output, i));
+ }
+ }
+
+ if (isFormat(format, F10, F20, F30, F40)) {
+ int input = buf.readUnsignedShortLE();
+ position.set(Position.KEY_INPUT, input);
+ if (!isFormat(format, F40)) {
+ for (int i = 0; i < 16; i++) {
+ position.set(Position.PREFIX_IN + (i + 1), BitUtil.check(input, i));
+ }
+ } else {
+ position.set(Position.PREFIX_IN + 1, BitUtil.check(input, 0));
+ position.set(Position.PREFIX_IN + 2, BitUtil.check(input, 1));
+ position.set(Position.PREFIX_IN + 3, BitUtil.check(input, 2));
+ position.set(Position.PREFIX_IN + 4, BitUtil.check(input, 3));
+ position.set(Position.PREFIX_IN + 5, BitUtil.between(input, 4, 7));
+ position.set(Position.PREFIX_IN + 6, BitUtil.between(input, 7, 10));
+ position.set(Position.PREFIX_IN + 7, BitUtil.between(input, 10, 12));
+ position.set(Position.PREFIX_IN + 8, BitUtil.between(input, 12, 14));
+ }
+ } else if (isFormat(format, F50, F51, F52, F60)) {
+ short input = buf.readUnsignedByte();
+ position.set(Position.KEY_INPUT, input);
+ for (int i = 0; i < 8; i++) {
+ position.set(Position.PREFIX_IN + (i + 1), BitUtil.check(input, i));
+ }
+ }
+
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() * 0.001);
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.001);
+
+ if (isFormat(format, F10, F20, F30)) {
+ position.set(Position.PREFIX_TEMP + 1, buf.readShortLE());
+ }
+
+ if (isFormat(format, F10, F20, F50, F51, F52, F60)) {
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShortLE());
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShortLE());
+ }
+ if (isFormat(format, F60)) {
+ position.set(Position.PREFIX_ADC + 3, buf.readUnsignedShortLE());
+ }
+
+ // Impulse counters
+ if (isFormat(format, F20, F50, F51, F52, F60)) {
+ buf.readUnsignedIntLE();
+ buf.readUnsignedIntLE();
+ }
+
+ if (isFormat(format, F60)) {
+ // Fuel
+ buf.readUnsignedShortLE();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+
+ position.set(Position.PREFIX_TEMP + 1, buf.readByte());
+ position.set(Position.PREFIX_TEMP + 2, buf.readByte());
+ position.set(Position.PREFIX_TEMP + 3, buf.readByte());
+ position.set(Position.PREFIX_TEMP + 4, buf.readByte());
+ position.set(Position.KEY_AXLE_WEIGHT, buf.readIntLE());
+ position.set(Position.KEY_RPM, buf.readUnsignedShortLE());
+ }
+
+ if (isFormat(format, F20, F50, F51, F52, F60)) {
+ int navSensorState = buf.readUnsignedByte();
+ position.setValid(BitUtil.check(navSensorState, 1));
+ if (isFormat(format, F60)) {
+ position.set(Position.KEY_SATELLITES, BitUtil.from(navSensorState, 2));
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setDateReverse(buf.readUnsignedByte(), buf.readUnsignedByte() + 1, buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ if (isFormat(format, F60)) {
+ position.setLatitude(buf.readIntLE() / 600000.0);
+ position.setLongitude(buf.readIntLE() / 600000.0);
+ position.setAltitude(buf.readIntLE() * 0.1);
+ } else {
+ position.setLatitude(buf.readFloatLE() / Math.PI * 180);
+ position.setLongitude(buf.readFloatLE() / Math.PI * 180);
+ }
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readFloatLE()));
+ position.setCourse(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_ODOMETER, buf.readFloatLE() * 1000);
+ position.set(Position.KEY_DISTANCE, buf.readFloatLE() * 1000);
+
+ // Segment times
+ buf.readUnsignedShortLE();
+ buf.readUnsignedShortLE();
+ }
+
+ // Other
+ if (isFormat(format, F51, F52)) {
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ buf.readUnsignedShortLE();
+ buf.readByte();
+ buf.readUnsignedShortLE();
+ }
+
+ // Four temperature sensors
+ if (isFormat(format, F40, F52)) {
+ position.set(Position.PREFIX_TEMP + 1, buf.readByte());
+ position.set(Position.PREFIX_TEMP + 2, buf.readByte());
+ position.set(Position.PREFIX_TEMP + 3, buf.readByte());
+ position.set(Position.PREFIX_TEMP + 4, buf.readByte());
+ }
+
+ return position;
+ }
+
+ private Object processNtcbSingle(DeviceSession deviceSession, Channel channel, ByteBuf buf) {
+ Position position = parseNtcbPosition(deviceSession, buf);
+
+ ByteBuf response = Unpooled.buffer(7);
+ response.writeCharSequence("*<T", StandardCharsets.US_ASCII);
+ response.writeIntLE((int) position.getLong(Position.KEY_INDEX));
+ sendNtcbReply(channel, response);
+
+ return position.getFixTime() != null ? position : null;
+ }
+
+ private Object processNtcbArray(DeviceSession deviceSession, Channel channel, ByteBuf buf) {
+ List<Position> positions = new LinkedList<>();
+ int count = buf.readUnsignedByte();
+
+ for (int i = 0; i < count; i++) {
+ Position position = parseNtcbPosition(deviceSession, buf);
+ if (position.getFixTime() != null) {
+ positions.add(position);
+ }
+ }
+
+ ByteBuf response = Unpooled.buffer(7);
+ response.writeCharSequence("*<A", StandardCharsets.US_ASCII);
+ response.writeByte(count);
+ sendNtcbReply(channel, response);
+
+ if (positions.isEmpty()) {
+ return null;
+ }
+
+ return positions;
+ }
+
+ private boolean checkFlexBitfield(int index) {
+ int byteIndex = Math.floorDiv(index, 8);
+ int bitIndex = Math.floorMod(index, 8);
+ return BitUtil.check(flexBitField[byteIndex], 7 - bitIndex);
+ }
+
+ private Position parseFlexPosition(DeviceSession deviceSession, ByteBuf buf) {
+
+ Position position = new Position(getProtocolName());
+
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int status = 0;
+ short input = 0;
+ short output = 0;
+
+ for (int i = 0; i < flexBitFieldSize; i++) {
+ if (!checkFlexBitfield(i)) {
+ continue;
+ }
+
+ switch (i) {
+ case 0:
+ position.set(Position.KEY_INDEX, buf.readUnsignedIntLE());
+ break;
+ case 1:
+ position.set(Position.KEY_EVENT, buf.readUnsignedShortLE());
+ break;
+ case 3:
+ short armedStatus = buf.readUnsignedByte();
+ position.set(Position.KEY_ARMED, BitUtil.check(armedStatus, 0));
+ if (BitUtil.check(armedStatus, 1)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ break;
+ case 4:
+ status = buf.readUnsignedByte();
+ position.set(Position.KEY_STATUS, status);
+ break;
+ case 5:
+ int status2 = buf.readUnsignedByte();
+ position.set(Position.KEY_STATUS, (short) (BitUtil.to(status, 8) | (status2 << 8)));
+ break;
+ case 6:
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ break;
+ case 7:
+ int navSensorState = buf.readUnsignedByte();
+ position.setValid(BitUtil.check(navSensorState, 1));
+ position.set(Position.KEY_SATELLITES, BitUtil.from(navSensorState, 2));
+ break;
+ case 8:
+ position.setTime(new DateBuilder(new Date(buf.readUnsignedIntLE() * 1000)).getDate());
+ break;
+ case 9:
+ position.setLatitude(buf.readIntLE() / 600000.0);
+ break;
+ case 10:
+ position.setLongitude(buf.readIntLE() / 600000.0);
+ break;
+ case 11:
+ position.setAltitude(buf.readIntLE() * 0.1);
+ break;
+ case 12:
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readFloatLE()));
+ break;
+ case 13:
+ position.setCourse(buf.readUnsignedShortLE());
+ break;
+ case 14:
+ position.set(Position.KEY_ODOMETER, buf.readFloatLE() * 1000);
+ break;
+ case 15:
+ position.set(Position.KEY_DISTANCE, buf.readFloatLE() * 1000);
+ break;
+ case 18:
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() * 0.001);
+ break;
+ case 19:
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE() * 0.001);
+ break;
+ case 20:
+ case 21:
+ case 22:
+ case 23:
+ case 24:
+ case 25:
+ case 26:
+ case 27:
+ position.set(Position.PREFIX_ADC + (i - 19), buf.readUnsignedShortLE());
+ break;
+ case 28:
+ input = buf.readUnsignedByte();
+ position.set(Position.KEY_INPUT, input);
+ for (int k = 0; k < 8; k++) {
+ position.set(Position.PREFIX_IN + (k + 1), BitUtil.check(input, k));
+ }
+ break;
+ case 29:
+ short input2 = buf.readUnsignedByte();
+ position.set(Position.KEY_INPUT, (short) (BitUtil.to(input, 8) | (input2 << 8)));
+ for (int k = 0; k < 8; k++) {
+ position.set(Position.PREFIX_IN + (k + 9), BitUtil.check(input2, k));
+ }
+ break;
+ case 30:
+ output = buf.readUnsignedByte();
+ position.set(Position.KEY_OUTPUT, output);
+ for (int k = 0; k < 8; k++) {
+ position.set(Position.PREFIX_OUT + (k + 1), BitUtil.check(output, k));
+ }
+ break;
+ case 31:
+ short output2 = buf.readUnsignedByte();
+ position.set(Position.KEY_OUTPUT, (short) (BitUtil.to(output, 8) | (output2 << 8)));
+ for (int k = 0; k < 8; k++) {
+ position.set(Position.PREFIX_OUT + (k + 9), BitUtil.check(output2, k));
+ }
+ break;
+ case 36:
+ position.set(Position.KEY_HOURS, buf.readUnsignedIntLE() * 1000);
+ break;
+ case 44:
+ case 45:
+ case 46:
+ case 47:
+ case 48:
+ case 49:
+ case 50:
+ case 51:
+ position.set(Position.PREFIX_TEMP + (i - 43), buf.readByte());
+ break;
+ case 68:
+ position.set("can-speed", buf.readUnsignedByte());
+ break;
+ // FLEX 2.0
+ case 69:
+ int satVisible = 0;
+ for (int k = 0; k < 8; k++) {
+ satVisible += buf.readUnsignedByte();
+ }
+ position.set(Position.KEY_SATELLITES_VISIBLE, satVisible);
+ break;
+ case 70:
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte() * 0.1);
+ position.set(Position.KEY_PDOP, buf.readUnsignedByte() * 0.1);
+ break;
+ default:
+ if (i < FLEX_FIELDS_SIZES.length) {
+ buf.skipBytes(FLEX_FIELDS_SIZES[i]);
+ }
+ break;
+ }
+ }
+
+ return position;
+ }
+
+ private Position parseFlex20Position(DeviceSession deviceSession, ByteBuf buf) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int length = buf.readUnsignedShort();
+ if (length <= buf.readableBytes() && buf.readUnsignedByte() == 0x0A) {
+
+ buf.readUnsignedByte(); // length of static part
+
+ position.set(Position.KEY_INDEX, buf.readUnsignedIntLE());
+
+ position.set(Position.KEY_EVENT, buf.readUnsignedShortLE());
+ buf.readUnsignedInt(); // event time
+
+ int navSensorState = buf.readUnsignedByte();
+ position.setValid(BitUtil.check(navSensorState, 1));
+ position.set(Position.KEY_SATELLITES, BitUtil.from(navSensorState, 2));
+
+ position.setTime(new DateBuilder(new Date(buf.readUnsignedIntLE() * 1000)).getDate());
+ position.setLatitude(buf.readIntLE() / 600000.0);
+ position.setLongitude(buf.readIntLE() / 600000.0);
+ position.setAltitude(buf.readIntLE() * 0.1);
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readFloatLE()));
+ position.setCourse(buf.readUnsignedShortLE());
+ position.set(Position.KEY_ODOMETER, buf.readFloatLE() * 1000);
+
+ buf.skipBytes(length - buf.readerIndex() - 1); // skip unused part
+ }
+
+ return position;
+ }
+
+ private interface FlexPositionParser {
+ Position parsePosition(DeviceSession deviceSession, ByteBuf buf);
+ }
+
+ private Object processFlexSingle(
+ FlexPositionParser parser, String flexHeader, DeviceSession deviceSession, Channel channel, ByteBuf buf) {
+
+ if (!flexHeader.equals("~C")) {
+ buf.readUnsignedInt(); // event index
+ }
+
+ Position position = parser.parsePosition(deviceSession, buf);
+
+ ByteBuf response = Unpooled.buffer();
+ response.writeCharSequence(flexHeader, StandardCharsets.US_ASCII);
+ response.writeIntLE((int) position.getLong(Position.KEY_INDEX));
+ sendFlexReply(channel, response);
+
+ return position.getFixTime() != null ? position : null;
+ }
+
+ private Object processFlexArray(
+ FlexPositionParser parser, String flexHeader, DeviceSession deviceSession, Channel channel, ByteBuf buf) {
+
+ List<Position> positions = new LinkedList<>();
+ int count = buf.readUnsignedByte();
+
+ for (int i = 0; i < count; i++) {
+ Position position = parser.parsePosition(deviceSession, buf);
+ if (position.getFixTime() != null) {
+ positions.add(position);
+ }
+ }
+
+ ByteBuf response = Unpooled.buffer();
+ response.writeCharSequence(flexHeader, StandardCharsets.US_ASCII);
+ response.writeByte(count);
+ sendFlexReply(channel, response);
+
+ return !positions.isEmpty() ? positions : null;
+ }
+
+ private Object processFlexNegotiation(Channel channel, ByteBuf buf) {
+ if ((byte) buf.readUnsignedByte() != (byte) 0xB0) {
+ return null;
+ }
+
+ short flexProtocolVersion = buf.readUnsignedByte();
+ short flexStructVersion = buf.readUnsignedByte();
+ if ((flexProtocolVersion == 0x0A || flexProtocolVersion == 0x14)
+ && (flexStructVersion == 0x0A || flexStructVersion == 0x14)) {
+
+ flexBitFieldSize = buf.readUnsignedByte();
+ if (flexBitFieldSize > 122) {
+ return null;
+ }
+
+ buf.readBytes(flexBitField, 0, (int) Math.ceil((double) flexBitFieldSize / 8));
+
+ flexDataSize = 0;
+ for (int i = 0; i < flexBitFieldSize; i++) {
+ if (checkFlexBitfield(i)) {
+ flexDataSize += FLEX_FIELDS_SIZES[i];
+ }
+ }
+ } else {
+ flexProtocolVersion = 0x14;
+ flexStructVersion = 0x14;
+ }
+
+ ByteBuf response = Unpooled.buffer(9);
+ response.writeCharSequence("*<FLEX", StandardCharsets.US_ASCII);
+ response.writeByte(0xB0);
+ response.writeByte(flexProtocolVersion);
+ response.writeByte(flexStructVersion);
+ sendNtcbReply(channel, response);
+
+ return null;
+ }
+
+ private Object processHandshake(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+ buf.readByte(); // colon
+ if (getDeviceSession(channel, remoteAddress, buf.toString(StandardCharsets.US_ASCII)) != null) {
+ sendNtcbReply(channel, Unpooled.copiedBuffer("*<S", StandardCharsets.US_ASCII));
+ }
+ return null;
+ }
+
+ private void sendNtcbReply(Channel channel, ByteBuf data) {
+ if (channel != null) {
+ ByteBuf header = Unpooled.buffer(16);
+ header.writeCharSequence(prefix, StandardCharsets.US_ASCII);
+ header.writeIntLE((int) deviceUniqueId);
+ header.writeIntLE((int) serverId);
+ header.writeShortLE(data.readableBytes());
+ header.writeByte(Checksum.xor(data.nioBuffer()));
+ header.writeByte(Checksum.xor(header.nioBuffer()));
+
+ channel.writeAndFlush(new NetworkMessage(Unpooled.wrappedBuffer(header, data), channel.remoteAddress()));
+ }
+ }
+
+ private void sendFlexReply(Channel channel, ByteBuf data) {
+ if (channel != null) {
+ ByteBuf cs = Unpooled.buffer(1);
+ cs.writeByte(Checksum.crc8(new Algorithm(8, 0x31, 0xFF, false, false, 0x00), data.nioBuffer()));
+
+ channel.writeAndFlush(new NetworkMessage(Unpooled.wrappedBuffer(data, cs), channel.remoteAddress()));
+ }
+ }
+
+ private Object decodeNtcb(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ prefix = buf.toString(buf.readerIndex(), 4, StandardCharsets.US_ASCII);
+ buf.skipBytes(prefix.length()); // prefix @NTC by default
+ serverId = buf.readUnsignedIntLE();
+ deviceUniqueId = buf.readUnsignedIntLE();
+ int length = buf.readUnsignedShortLE();
+ buf.skipBytes(2); // header and data XOR checksum
+
+ if (length == 0) {
+ return null; // keep alive message
+ }
+
+ String type = buf.toString(buf.readerIndex(), 3, StandardCharsets.US_ASCII);
+ buf.skipBytes(type.length());
+
+ if (type.equals("*>S")) {
+ return processHandshake(channel, remoteAddress, buf);
+ } else {
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession != null) {
+ switch (type) {
+ case "*>A":
+ return processNtcbArray(deviceSession, channel, buf);
+ case "*>T":
+ return processNtcbSingle(deviceSession, channel, buf);
+ case "*>F":
+ buf.skipBytes(3);
+ return processFlexNegotiation(channel, buf);
+ default:
+ break;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private Object decodeFlex(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ if (buf.getByte(buf.readerIndex()) == 0x7F) {
+ return null; // keep alive
+ }
+
+ String type = buf.toString(buf.readerIndex(), 2, StandardCharsets.US_ASCII);
+ buf.skipBytes(type.length());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession != null) {
+ switch (type) {
+ // FLEX 1.0
+ case "~A":
+ return processFlexArray(this::parseFlexPosition, type, deviceSession, channel, buf);
+ case "~T":
+ case "~C":
+ return processFlexSingle(this::parseFlexPosition, type, deviceSession, channel, buf);
+ // FLEX 2.0 (extra packages)
+ case "~E":
+ return processFlexArray(this::parseFlex20Position, type, deviceSession, channel, buf);
+ case "~X":
+ return processFlexSingle(this::parseFlex20Position, type, deviceSession, channel, buf);
+ default:
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (flexDataSize > 0) {
+ return decodeFlex(channel, remoteAddress, buf);
+ } else {
+ return decodeNtcb(channel, remoteAddress, buf);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NeosProtocol.java b/src/main/java/org/traccar/protocol/NeosProtocol.java
new file mode 100644
index 000000000..e545a9969
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NeosProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class NeosProtocol extends BaseProtocol {
+
+ public NeosProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new NeosProtocolDecoder(NeosProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NeosProtocolDecoder.java b/src/main/java/org/traccar/protocol/NeosProtocolDecoder.java
new file mode 100644
index 000000000..6b5596dba
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NeosProtocolDecoder.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class NeosProtocolDecoder extends BaseProtocolDecoder {
+
+ public NeosProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text(">")
+ .number("(d{8}),") // id
+ .number("d+,") // status
+ .number("([01]),") // valid
+ .number("(dd)(dd)(dd),") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([EW])")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([NS])")
+ .number("(d+)(dd.d+),") // latitude
+ .expression("[^,]*,") // response
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // rssi
+ .expression("[^,]*,") // event data
+ .number("(d+)-") // adc
+ .number("(d+),") // battery
+ .number("0,")
+ .number("d,")
+ .number("([01]{8})") // input
+ .text("*")
+ .number("xx!")
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("$OK!", remoteAddress));
+ }
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.nextInt() > 0);
+ position.setTime(parser.nextDateTime());
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setSpeed(parser.nextInt());
+ position.setCourse(parser.nextInt());
+
+ position.set(Position.KEY_RSSI, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt());
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+ position.set(Position.KEY_INPUT, parser.nextBinInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NoranProtocol.java b/src/main/java/org/traccar/protocol/NoranProtocol.java
new file mode 100644
index 000000000..9f3078d6d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NoranProtocol.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class NoranProtocol extends BaseProtocol {
+
+ public NoranProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_POSITION_PERIODIC,
+ Command.TYPE_POSITION_STOP,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME);
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new NoranProtocolEncoder());
+ pipeline.addLast(new NoranProtocolDecoder(NoranProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NoranProtocolDecoder.java b/src/main/java/org/traccar/protocol/NoranProtocolDecoder.java
new file mode 100644
index 000000000..53dae7fd6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NoranProtocolDecoder.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+
+public class NoranProtocolDecoder extends BaseProtocolDecoder {
+
+ public NoranProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_UPLOAD_POSITION = 0x0008;
+ public static final int MSG_UPLOAD_POSITION_NEW = 0x0032;
+ public static final int MSG_CONTROL = 0x0002;
+ public static final int MSG_CONTROL_RESPONSE = 0x8009;
+ public static final int MSG_ALARM = 0x0003;
+ public static final int MSG_SHAKE_HAND = 0x0000;
+ public static final int MSG_SHAKE_HAND_RESPONSE = 0x8000;
+ public static final int MSG_IMAGE_SIZE = 0x0200;
+ public static final int MSG_IMAGE_PACKET = 0x0201;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedShortLE(); // length
+ int type = buf.readUnsignedShortLE();
+
+ if (type == MSG_SHAKE_HAND && channel != null) {
+
+ ByteBuf response = Unpooled.buffer(13);
+ response.writeCharSequence("\r\n*KW", StandardCharsets.US_ASCII);
+ response.writeByte(0);
+ response.writeShortLE(response.capacity());
+ response.writeShortLE(MSG_SHAKE_HAND_RESPONSE);
+ response.writeByte(1); // status
+ response.writeCharSequence("\r\n", StandardCharsets.US_ASCII);
+
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+
+ } else if (type == MSG_UPLOAD_POSITION || type == MSG_UPLOAD_POSITION_NEW
+ || type == MSG_CONTROL_RESPONSE || type == MSG_ALARM) {
+
+ boolean newFormat = false;
+ if (type == MSG_UPLOAD_POSITION && buf.readableBytes() == 48
+ || type == MSG_ALARM && buf.readableBytes() == 48
+ || type == MSG_CONTROL_RESPONSE && buf.readableBytes() == 57) {
+ newFormat = true;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ if (type == MSG_CONTROL_RESPONSE) {
+ buf.readUnsignedIntLE(); // GIS ip
+ buf.readUnsignedIntLE(); // GIS port
+ }
+
+ position.setValid(BitUtil.check(buf.readUnsignedByte(), 0));
+
+ short alarm = buf.readUnsignedByte();
+ switch (alarm) {
+ case 1:
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ break;
+ case 2:
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ break;
+ case 3:
+ position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE_EXIT);
+ break;
+ case 9:
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_OFF);
+ break;
+ default:
+ break;
+ }
+
+ if (newFormat) {
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedIntLE()));
+ position.setCourse(buf.readFloatLE());
+ } else {
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.setCourse(buf.readUnsignedShortLE());
+ }
+ position.setLongitude(buf.readFloatLE());
+ position.setLatitude(buf.readFloatLE());
+
+ if (!newFormat) {
+ long timeValue = buf.readUnsignedIntLE();
+ DateBuilder dateBuilder = new DateBuilder()
+ .setYear((int) BitUtil.from(timeValue, 26))
+ .setMonth((int) BitUtil.between(timeValue, 22, 26))
+ .setDay((int) BitUtil.between(timeValue, 17, 22))
+ .setHour((int) BitUtil.between(timeValue, 12, 17))
+ .setMinute((int) BitUtil.between(timeValue, 6, 12))
+ .setSecond((int) BitUtil.to(timeValue, 6));
+ position.setTime(dateBuilder.getDate());
+ }
+
+ ByteBuf rawId;
+ if (newFormat) {
+ rawId = buf.readSlice(12);
+ } else {
+ rawId = buf.readSlice(11);
+ }
+ String id = rawId.toString(StandardCharsets.US_ASCII).replaceAll("[^\\p{Print}]", "");
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (newFormat) {
+ DateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
+ position.setTime(dateFormat.parse(buf.readSlice(17).toString(StandardCharsets.US_ASCII)));
+ buf.readByte();
+ }
+
+ if (!newFormat) {
+ position.set(Position.PREFIX_IO + 1, buf.readUnsignedByte());
+ position.set(Position.KEY_FUEL_LEVEL, buf.readUnsignedByte());
+ } else if (type == MSG_UPLOAD_POSITION_NEW) {
+ position.set(Position.PREFIX_TEMP + 1, buf.readShortLE());
+ position.set(Position.KEY_ODOMETER, buf.readFloatLE());
+ }
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NoranProtocolEncoder.java b/src/main/java/org/traccar/protocol/NoranProtocolEncoder.java
new file mode 100644
index 000000000..92826c8b2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NoranProtocolEncoder.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class NoranProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeContent(String content) {
+
+ ByteBuf buf = Unpooled.buffer(12 + 56);
+
+ buf.writeCharSequence("\r\n*KW", StandardCharsets.US_ASCII);
+ buf.writeByte(0);
+ buf.writeShortLE(buf.capacity());
+ buf.writeShortLE(NoranProtocolDecoder.MSG_CONTROL);
+ buf.writeInt(0); // gis ip
+ buf.writeShortLE(0); // gis port
+ buf.writeBytes(content.getBytes(StandardCharsets.US_ASCII));
+ buf.writerIndex(buf.writerIndex() + 50 - content.length());
+ buf.writeCharSequence("\r\n", StandardCharsets.US_ASCII);
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_POSITION_SINGLE:
+ return encodeContent("*KW,000,000,000000#");
+ case Command.TYPE_POSITION_PERIODIC:
+ int interval = command.getInteger(Command.KEY_FREQUENCY);
+ return encodeContent("*KW,000,002,000000," + interval + "#");
+ case Command.TYPE_POSITION_STOP:
+ return encodeContent("*KW,000,002,000000,0#");
+ case Command.TYPE_ENGINE_STOP:
+ return encodeContent("*KW,000,007,000000,0#");
+ case Command.TYPE_ENGINE_RESUME:
+ return encodeContent("*KW,000,007,000000,1#");
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NvsFrameDecoder.java b/src/main/java/org/traccar/protocol/NvsFrameDecoder.java
new file mode 100644
index 000000000..e93a58cf6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NvsFrameDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class NvsFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 4 + 2) {
+ return null;
+ }
+
+ int length;
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0) {
+ length = 2 + buf.getUnsignedShort(buf.readerIndex());
+ } else {
+ length = 4 + 2 + buf.getUnsignedShort(buf.readerIndex() + 4) + 2;
+ }
+
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NvsProtocol.java b/src/main/java/org/traccar/protocol/NvsProtocol.java
new file mode 100644
index 000000000..d319b22f3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NvsProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class NvsProtocol extends BaseProtocol {
+
+ public NvsProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new NvsFrameDecoder());
+ pipeline.addLast(new NvsProtocolDecoder(NvsProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NvsProtocolDecoder.java b/src/main/java/org/traccar/protocol/NvsProtocolDecoder.java
new file mode 100644
index 000000000..5d1159f7d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NvsProtocolDecoder.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class NvsProtocolDecoder extends BaseProtocolDecoder {
+
+ public NvsProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress, String response) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(
+ Unpooled.copiedBuffer(response, StandardCharsets.US_ASCII), remoteAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0) {
+
+ buf.readUnsignedShort(); // length
+
+ String imei = buf.toString(buf.readerIndex(), 15, StandardCharsets.US_ASCII);
+
+ if (getDeviceSession(channel, remoteAddress, imei) != null) {
+ sendResponse(channel, remoteAddress, "OK");
+ } else {
+ sendResponse(channel, remoteAddress, "NO01");
+ }
+
+ } else {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ buf.skipBytes(4); // marker
+ buf.readUnsignedShort(); // length
+ buf.readLong(); // imei
+ buf.readUnsignedByte(); // codec
+ int count = buf.readUnsignedByte();
+
+ for (int i = 0; i < count; i++) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+
+ position.set("reason", buf.readUnsignedByte());
+
+ position.setLongitude(buf.readInt() / 10000000.0);
+ position.setLatitude(buf.readInt() / 10000000.0);
+ position.setAltitude(buf.readShort());
+ position.setCourse(buf.readUnsignedShort());
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+ position.setValid(buf.readUnsignedByte() != 0);
+
+ buf.readUnsignedByte(); // used systems
+
+ buf.readUnsignedByte(); // cause element id
+
+ // Read 1 byte data
+ int cnt = buf.readUnsignedByte();
+ for (int j = 0; j < cnt; j++) {
+ position.set(Position.PREFIX_IO + buf.readUnsignedByte(), buf.readUnsignedByte());
+ }
+
+ // Read 2 byte data
+ cnt = buf.readUnsignedByte();
+ for (int j = 0; j < cnt; j++) {
+ position.set(Position.PREFIX_IO + buf.readUnsignedByte(), buf.readUnsignedShort());
+ }
+
+ // Read 4 byte data
+ cnt = buf.readUnsignedByte();
+ for (int j = 0; j < cnt; j++) {
+ position.set(Position.PREFIX_IO + buf.readUnsignedByte(), buf.readUnsignedInt());
+ }
+
+ // Read 8 byte data
+ cnt = buf.readUnsignedByte();
+ for (int j = 0; j < cnt; j++) {
+ position.set(Position.PREFIX_IO + buf.readUnsignedByte(), buf.readLong());
+ }
+
+ positions.add(position);
+ }
+
+ return positions;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NyitechProtocol.java b/src/main/java/org/traccar/protocol/NyitechProtocol.java
new file mode 100644
index 000000000..58974be5c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NyitechProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import java.nio.ByteOrder;
+
+public class NyitechProtocol extends BaseProtocol {
+
+ public NyitechProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 2, 2, -4, 0, true));
+ pipeline.addLast(new NyitechProtocolDecoder(NyitechProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/NyitechProtocolDecoder.java b/src/main/java/org/traccar/protocol/NyitechProtocolDecoder.java
new file mode 100644
index 000000000..e145205f7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/NyitechProtocolDecoder.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+
+public class NyitechProtocolDecoder extends BaseProtocolDecoder {
+
+ public NyitechProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final short MSG_LOGIN = 0x1001;
+ public static final short MSG_COMPREHENSIVE_LIVE = 0x2001;
+ public static final short MSG_COMPREHENSIVE_HISTORY = 0x2002;
+ public static final short MSG_ALARM = 0x2003;
+ public static final short MSG_FIXED = 0x2004;
+
+ private void decodeLocation(Position position, ByteBuf buf) {
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDateReverse(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ int flags = buf.readUnsignedByte();
+ position.setValid(BitUtil.to(flags, 2) > 0);
+
+ double lat = buf.readUnsignedIntLE() / 3600000.0;
+ double lon = buf.readUnsignedIntLE() / 3600000.0;
+
+ position.setLatitude(BitUtil.check(flags, 2) ? lat : -lat);
+ position.setLongitude(BitUtil.check(flags, 3) ? lon : -lon);
+
+ position.setSpeed(UnitsConverter.knotsFromCps(buf.readUnsignedShortLE()));
+ position.setCourse(buf.readUnsignedShortLE() * 0.1);
+ position.setAltitude(buf.readShortLE() * 0.1);
+ }
+
+ private String decodeAlarm(int type) {
+ switch (type) {
+ case 0x09:
+ return Position.ALARM_ACCELERATION;
+ case 0x0a:
+ return Position.ALARM_BRAKING;
+ case 0x0b:
+ return Position.ALARM_CORNERING;
+ case 0x0e:
+ return Position.ALARM_SOS;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ buf.readUnsignedShortLE(); // length
+
+ String id = buf.readCharSequence(12, StandardCharsets.US_ASCII).toString();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type = buf.readUnsignedShortLE();
+
+ if (type != MSG_LOGIN && type != MSG_COMPREHENSIVE_LIVE
+ && type != MSG_COMPREHENSIVE_HISTORY && type != MSG_ALARM && type != MSG_FIXED) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (type == MSG_COMPREHENSIVE_LIVE || type == MSG_COMPREHENSIVE_HISTORY) {
+ buf.skipBytes(6); // time
+ buf.skipBytes(3); // data
+ } else if (type == MSG_ALARM) {
+ buf.readUnsignedShortLE(); // random number
+ buf.readUnsignedByte(); // tag
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte()));
+ buf.readUnsignedShortLE(); // threshold
+ buf.readUnsignedShortLE(); // value
+ buf.skipBytes(6); // time
+ } else if (type == MSG_FIXED) {
+ buf.skipBytes(6); // time
+ }
+
+ decodeLocation(position, buf);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ObdDongleProtocol.java b/src/main/java/org/traccar/protocol/ObdDongleProtocol.java
new file mode 100644
index 000000000..10a55759b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ObdDongleProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ObdDongleProtocol extends BaseProtocol {
+
+ public ObdDongleProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1099, 20, 2, 3, 0));
+ pipeline.addLast(new ObdDongleProtocolDecoder(ObdDongleProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ObdDongleProtocolDecoder.java b/src/main/java/org/traccar/protocol/ObdDongleProtocolDecoder.java
new file mode 100644
index 000000000..1c9771ce9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ObdDongleProtocolDecoder.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class ObdDongleProtocolDecoder extends BaseProtocolDecoder {
+
+ public ObdDongleProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_TYPE_CONNECT = 0x01;
+ public static final int MSG_TYPE_CONNACK = 0x02;
+ public static final int MSG_TYPE_PUBLISH = 0x03;
+ public static final int MSG_TYPE_PUBACK = 0x04;
+ public static final int MSG_TYPE_PINGREQ = 0x0C;
+ public static final int MSG_TYPE_PINGRESP = 0x0D;
+ public static final int MSG_TYPE_DISCONNECT = 0x0E;
+
+ private static void sendResponse(Channel channel, int type, int index, String imei, ByteBuf content) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeShort(0x5555); // header
+ response.writeShort(index);
+ response.writeBytes(imei.getBytes(StandardCharsets.US_ASCII));
+ response.writeByte(type);
+ response.writeShort(content.readableBytes());
+ response.writeBytes(content);
+ content.release();
+ response.writeByte(0); // checksum
+ response.writeShort(0xAAAA);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ int index = buf.readUnsignedShort();
+
+ String imei = buf.readSlice(15).toString(StandardCharsets.US_ASCII);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type = buf.readUnsignedByte();
+
+ buf.readUnsignedShort(); // data length
+
+ if (type == MSG_TYPE_CONNECT) {
+
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(1);
+ response.writeShort(0);
+ response.writeInt(0);
+ sendResponse(channel, MSG_TYPE_CONNACK, index, imei, response);
+
+ } else if (type == MSG_TYPE_PUBLISH) {
+
+ int typeMajor = buf.readUnsignedByte();
+ int typeMinor = buf.readUnsignedByte();
+
+ buf.readUnsignedByte(); // event id
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+
+ int flags = buf.readUnsignedByte();
+
+ position.setValid(!BitUtil.check(flags, 6));
+
+ position.set(Position.KEY_SATELLITES, BitUtil.to(flags, 4));
+
+ double longitude = ((BitUtil.to(buf.readUnsignedShort(), 1) << 24) + buf.readUnsignedMedium()) * 0.00001;
+ position.setLongitude(BitUtil.check(flags, 5) ? longitude : -longitude);
+
+ double latitude = buf.readUnsignedMedium() * 0.00001;
+ position.setLatitude(BitUtil.check(flags, 4) ? latitude : -latitude);
+
+ int speedCourse = buf.readUnsignedMedium();
+ position.setSpeed(UnitsConverter.knotsFromMph(BitUtil.from(speedCourse, 10) * 0.1));
+ position.setCourse(BitUtil.to(speedCourse, 10));
+
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(typeMajor);
+ response.writeByte(typeMinor);
+ sendResponse(channel, MSG_TYPE_PUBACK, index, imei, response);
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OigoProtocol.java b/src/main/java/org/traccar/protocol/OigoProtocol.java
new file mode 100644
index 000000000..5056f68aa
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OigoProtocol.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class OigoProtocol extends BaseProtocol {
+
+ public OigoProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new OigoProtocolDecoder(OigoProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OigoProtocolDecoder.java b/src/main/java/org/traccar/protocol/OigoProtocolDecoder.java
new file mode 100644
index 000000000..b9cc71e8c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OigoProtocolDecoder.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+
+public class OigoProtocolDecoder extends BaseProtocolDecoder {
+
+ public OigoProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_AR_LOCATION = 0x00;
+ public static final int MSG_AR_REMOTE_START = 0x10;
+
+ public static final int MSG_ACKNOWLEDGEMENT = 0xE0;
+
+ private Position decodeArMessage(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ buf.skipBytes(1); // header
+ buf.readUnsignedShort(); // length
+
+ int type = buf.readUnsignedByte();
+
+ int tag = buf.readUnsignedByte();
+
+ DeviceSession deviceSession;
+ switch (BitUtil.to(tag, 3)) {
+ case 0:
+ String imei = ByteBufUtil.hexDump(buf.readSlice(8)).substring(1);
+ deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ break;
+ case 1:
+ buf.skipBytes(1);
+ String meid = buf.readSlice(14).toString(StandardCharsets.US_ASCII);
+ deviceSession = getDeviceSession(channel, remoteAddress, meid);
+ break;
+ default:
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ break;
+ }
+
+ if (deviceSession == null || type != MSG_AR_LOCATION) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+
+ int mask = buf.readInt();
+
+ if (BitUtil.check(mask, 0)) {
+ position.set(Position.KEY_INDEX, buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(mask, 1)) {
+ int date = buf.readUnsignedByte();
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(BitUtil.between(date, 4, 8) + 2010, BitUtil.to(date, 4), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+ }
+
+ if (BitUtil.check(mask, 2)) {
+ buf.skipBytes(5); // device time
+ }
+
+ if (BitUtil.check(mask, 3)) {
+ position.setLatitude(buf.readUnsignedInt() * 0.000001 - 90);
+ position.setLongitude(buf.readUnsignedInt() * 0.000001 - 180.0);
+ }
+
+ if (BitUtil.check(mask, 4)) {
+ int status = buf.readUnsignedByte();
+ position.setValid(BitUtil.between(status, 4, 8) != 0);
+ position.set(Position.KEY_SATELLITES, BitUtil.to(status, 4));
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte() * 0.1);
+ }
+
+ if (BitUtil.check(mask, 5)) {
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ }
+
+ if (BitUtil.check(mask, 6)) {
+ position.setCourse(buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(mask, 7)) {
+ position.setAltitude(buf.readShort());
+ }
+
+ if (BitUtil.check(mask, 8)) {
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ }
+
+ if (BitUtil.check(mask, 9)) {
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.001);
+ }
+
+ if (BitUtil.check(mask, 10)) {
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort() * 0.001);
+ }
+
+ if (BitUtil.check(mask, 11)) {
+ buf.skipBytes(2); // gpio
+ }
+
+ if (BitUtil.check(mask, 12)) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt() * 1000);
+ }
+
+ if (BitUtil.check(mask, 13)) {
+ buf.skipBytes(6); // software version
+ }
+
+ if (BitUtil.check(mask, 14)) {
+ buf.skipBytes(5); // hardware version
+ }
+
+ if (BitUtil.check(mask, 15)) {
+ buf.readUnsignedShort(); // device config
+ }
+
+ return position;
+ }
+
+ private double convertCoordinate(long value) {
+ boolean negative = value < 0;
+ value = Math.abs(value);
+ double minutes = (value % 100000) * 0.001;
+ value /= 100000;
+ double degrees = value + minutes / 60;
+ return negative ? -degrees : degrees;
+ }
+
+ private Position decodeMgMessage(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ buf.readUnsignedByte(); // tag
+ int flags = buf.getUnsignedByte(buf.readerIndex());
+
+ DeviceSession deviceSession;
+ if (BitUtil.check(flags, 6)) {
+ buf.readUnsignedByte(); // flags
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ } else {
+ String imei = ByteBufUtil.hexDump(buf.readSlice(8)).substring(1);
+ deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ }
+
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.skipBytes(8); // imsi
+
+ int date = buf.readUnsignedShort();
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(2010 + BitUtil.from(date, 12), BitUtil.between(date, 8, 12), BitUtil.to(date, 8))
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), 0);
+
+ position.setValid(true);
+ position.setLatitude(convertCoordinate(buf.readInt()));
+ position.setLongitude(convertCoordinate(buf.readInt()));
+
+ position.setAltitude(UnitsConverter.metersFromFeet(buf.readShort()));
+ position.setCourse(buf.readUnsignedShort());
+ position.setSpeed(UnitsConverter.knotsFromMph(buf.readUnsignedByte()));
+
+ position.set(Position.KEY_POWER, buf.readUnsignedByte() * 0.1);
+ position.set(Position.PREFIX_IO + 1, buf.readUnsignedByte());
+
+ dateBuilder.setSecond(buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+
+ int index = buf.readUnsignedByte();
+
+ position.set(Position.KEY_VERSION_FW, buf.readUnsignedByte());
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_ODOMETER, (long) (buf.readUnsignedInt() * 1609.34));
+
+ if (channel != null && BitUtil.check(flags, 7)) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(MSG_ACKNOWLEDGEMENT);
+ response.writeByte(index);
+ response.writeByte(0x00);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0x7e) {
+ return decodeArMessage(channel, remoteAddress, buf);
+ } else {
+ return decodeMgMessage(channel, remoteAddress, buf);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OkoProtocol.java b/src/main/java/org/traccar/protocol/OkoProtocol.java
new file mode 100644
index 000000000..9571ccc48
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OkoProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class OkoProtocol extends BaseProtocol {
+
+ public OkoProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '}'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new OkoProtocolDecoder(OkoProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OkoProtocolDecoder.java b/src/main/java/org/traccar/protocol/OkoProtocolDecoder.java
new file mode 100644
index 000000000..5adf61494
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OkoProtocolDecoder.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class OkoProtocolDecoder extends BaseProtocolDecoder {
+
+ public OkoProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("{")
+ .number("(d{15}),").optional() // imei
+ .number("(dd)(dd)(dd).d+,") // time
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(d+),") // satellites
+ .number("(d+.d+),") // adc
+ .number("(xx),") // event
+ .number("(d+.d+),") // power
+ .number("d,") // memory status
+ .number("(xx)") // io
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession;
+ if (parser.hasNext()) {
+ deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ } else {
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ }
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.KEY_EVENT, parser.next());
+ position.set(Position.KEY_POWER, parser.nextDouble());
+ position.set(Position.KEY_INPUT, parser.nextHexInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OpenGtsProtocol.java b/src/main/java/org/traccar/protocol/OpenGtsProtocol.java
new file mode 100644
index 000000000..5ef3260c6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OpenGtsProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class OpenGtsProtocol extends BaseProtocol {
+
+ public OpenGtsProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(16384));
+ pipeline.addLast(new OpenGtsProtocolDecoder(OpenGtsProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OpenGtsProtocolDecoder.java b/src/main/java/org/traccar/protocol/OpenGtsProtocolDecoder.java
new file mode 100644
index 000000000..b76cbfa85
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OpenGtsProtocolDecoder.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.QueryStringDecoder;
+import org.traccar.BaseHttpProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class OpenGtsProtocolDecoder extends BaseHttpProtocolDecoder {
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd)(?:.d+)?,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+),") // speed
+ .number("(d+.d+)?,") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .any()
+ .compile();
+
+ public OpenGtsProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
+ Map<String, List<String>> params = decoder.parameters();
+
+ Position position = new Position(getProtocolName());
+
+ for (Map.Entry<String, List<String>> entry : params.entrySet()) {
+ String value = entry.getValue().get(0);
+ switch (entry.getKey()) {
+ case "id":
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, value);
+ if (deviceSession == null) {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+ break;
+ case "gprmc":
+ Parser parser = new Parser(PATTERN, value);
+ if (!parser.matches()) {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble());
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+ break;
+ case "alt":
+ position.setAltitude(Double.parseDouble(value));
+ break;
+ case "batt":
+ position.set(Position.KEY_BATTERY_LEVEL, Double.parseDouble(value));
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (position.getDeviceId() != 0) {
+ sendResponse(channel, HttpResponseStatus.OK);
+ return position;
+ } else {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OrionFrameDecoder.java b/src/main/java/org/traccar/protocol/OrionFrameDecoder.java
new file mode 100644
index 000000000..948806609
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OrionFrameDecoder.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class OrionFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int length = 6;
+
+ if (buf.readableBytes() >= length) {
+
+ int type = buf.getUnsignedByte(buf.readerIndex() + 2) & 0x0f;
+
+ if (type == OrionProtocolDecoder.MSG_USERLOG && buf.readableBytes() >= length + 5) {
+
+ int index = buf.readerIndex() + 3;
+ int count = buf.getUnsignedByte(index) & 0x0f;
+ index += 5;
+ length += 5;
+
+ for (int i = 0; i < count; i++) {
+ if (buf.readableBytes() < length) {
+ return null;
+ }
+ int logLength = buf.getUnsignedByte(index + 1);
+ index += logLength;
+ length += logLength;
+ }
+
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ } else if (type == OrionProtocolDecoder.MSG_SYSLOG && buf.readableBytes() >= length + 12) {
+
+ length += buf.getUnsignedShortLE(buf.readerIndex() + 8);
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OrionProtocol.java b/src/main/java/org/traccar/protocol/OrionProtocol.java
new file mode 100644
index 000000000..8485ae638
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OrionProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class OrionProtocol extends BaseProtocol {
+
+ public OrionProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new OrionFrameDecoder());
+ pipeline.addLast(new OrionProtocolDecoder(OrionProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OrionProtocolDecoder.java b/src/main/java/org/traccar/protocol/OrionProtocolDecoder.java
new file mode 100644
index 000000000..af819989e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OrionProtocolDecoder.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.LinkedList;
+import java.util.List;
+
+public class OrionProtocolDecoder extends BaseProtocolDecoder {
+
+ public OrionProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_USERLOG = 0;
+ public static final int MSG_SYSLOG = 3;
+
+ private static void sendResponse(Channel channel, ByteBuf buf) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer(4);
+ response.writeByte('*');
+ response.writeShort(buf.getUnsignedShort(buf.writerIndex() - 2));
+ response.writeByte(buf.getUnsignedByte(buf.writerIndex() - 3));
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ private static double convertCoordinate(int raw) {
+ int degrees = raw / 1000000;
+ double minutes = (raw % 1000000) / 10000.0;
+ return degrees + minutes / 60;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ int type = buf.readUnsignedByte() & 0x0f;
+
+ if (type == MSG_USERLOG) {
+
+ int header = buf.readUnsignedByte();
+
+ if ((header & 0x40) != 0) {
+ sendResponse(channel, buf);
+ }
+
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, String.valueOf(buf.readUnsignedInt()));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ for (int i = 0; i < (header & 0x0f); i++) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+ buf.readUnsignedByte(); // length
+ position.set(Position.KEY_FLAGS, buf.readUnsignedShortLE());
+
+ position.setLatitude(convertCoordinate(buf.readIntLE()));
+ position.setLongitude(convertCoordinate(buf.readIntLE()));
+ position.setAltitude(buf.readShortLE() / 10.0);
+ position.setCourse(buf.readUnsignedShortLE());
+ position.setSpeed(buf.readUnsignedShortLE() * 0.0539957);
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ int satellites = buf.readUnsignedByte();
+ position.setValid(satellites >= 3);
+ position.set(Position.KEY_SATELLITES, satellites);
+
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OsmAndProtocol.java b/src/main/java/org/traccar/protocol/OsmAndProtocol.java
new file mode 100644
index 000000000..d3aa2fd6f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OsmAndProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class OsmAndProtocol extends BaseProtocol {
+
+ public OsmAndProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(16384));
+ pipeline.addLast(new OsmAndProtocolDecoder(OsmAndProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OsmAndProtocolDecoder.java b/src/main/java/org/traccar/protocol/OsmAndProtocolDecoder.java
new file mode 100644
index 000000000..3bc71de81
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OsmAndProtocolDecoder.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.QueryStringDecoder;
+import org.traccar.BaseHttpProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+import org.traccar.model.WifiAccessPoint;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public class OsmAndProtocolDecoder extends BaseHttpProtocolDecoder {
+
+ public OsmAndProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
+ Map<String, List<String>> params = decoder.parameters();
+ if (params.isEmpty()) {
+ decoder = new QueryStringDecoder(request.content().toString(StandardCharsets.US_ASCII), false);
+ params = decoder.parameters();
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setValid(true);
+
+ Network network = new Network();
+
+ for (Map.Entry<String, List<String>> entry : params.entrySet()) {
+ for (String value : entry.getValue()) {
+ switch (entry.getKey()) {
+ case "id":
+ case "deviceid":
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, value);
+ if (deviceSession == null) {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+ break;
+ case "valid":
+ position.setValid(Boolean.parseBoolean(value) || "1".equals(value));
+ break;
+ case "timestamp":
+ try {
+ long timestamp = Long.parseLong(value);
+ if (timestamp < Integer.MAX_VALUE) {
+ timestamp *= 1000;
+ }
+ position.setTime(new Date(timestamp));
+ } catch (NumberFormatException error) {
+ if (value.contains("T")) {
+ position.setTime(DateUtil.parseDate(value));
+ } else {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ position.setTime(dateFormat.parse(value));
+ }
+ }
+ break;
+ case "lat":
+ position.setLatitude(Double.parseDouble(value));
+ break;
+ case "lon":
+ position.setLongitude(Double.parseDouble(value));
+ break;
+ case "location":
+ String[] location = value.split(",");
+ position.setLatitude(Double.parseDouble(location[0]));
+ position.setLongitude(Double.parseDouble(location[1]));
+ break;
+ case "cell":
+ String[] cell = value.split(",");
+ if (cell.length > 4) {
+ network.addCellTower(CellTower.from(
+ Integer.parseInt(cell[0]), Integer.parseInt(cell[1]),
+ Integer.parseInt(cell[2]), Integer.parseInt(cell[3]), Integer.parseInt(cell[4])));
+ } else {
+ network.addCellTower(CellTower.from(
+ Integer.parseInt(cell[0]), Integer.parseInt(cell[1]),
+ Integer.parseInt(cell[2]), Integer.parseInt(cell[3])));
+ }
+ break;
+ case "wifi":
+ String[] wifi = value.split(",");
+ network.addWifiAccessPoint(WifiAccessPoint.from(
+ wifi[0].replace('-', ':'), Integer.parseInt(wifi[1])));
+ break;
+ case "speed":
+ position.setSpeed(convertSpeed(Double.parseDouble(value), "kn"));
+ break;
+ case "bearing":
+ case "heading":
+ position.setCourse(Double.parseDouble(value));
+ break;
+ case "altitude":
+ position.setAltitude(Double.parseDouble(value));
+ break;
+ case "accuracy":
+ position.setAccuracy(Double.parseDouble(value));
+ break;
+ case "hdop":
+ position.set(Position.KEY_HDOP, Double.parseDouble(value));
+ break;
+ case "batt":
+ position.set(Position.KEY_BATTERY_LEVEL, Double.parseDouble(value));
+ break;
+ case "driverUniqueId":
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, value);
+ break;
+ default:
+ try {
+ position.set(entry.getKey(), Double.parseDouble(value));
+ } catch (NumberFormatException e) {
+ switch (value) {
+ case "true":
+ position.set(entry.getKey(), true);
+ break;
+ case "false":
+ position.set(entry.getKey(), false);
+ break;
+ default:
+ position.set(entry.getKey(), value);
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if (position.getFixTime() == null) {
+ position.setTime(new Date());
+ }
+
+ if (network.getCellTowers() != null || network.getWifiAccessPoints() != null) {
+ position.setNetwork(network);
+ }
+
+ if (position.getLatitude() == 0 && position.getLongitude() == 0) {
+ getLastLocation(position, position.getDeviceTime());
+ }
+
+ if (position.getDeviceId() != 0) {
+ sendResponse(channel, HttpResponseStatus.OK);
+ return position;
+ } else {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OwnTracksProtocol.java b/src/main/java/org/traccar/protocol/OwnTracksProtocol.java
new file mode 100644
index 000000000..0086371d8
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OwnTracksProtocol.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Jan-Piet Mens (jpmens@gmail.com)
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class OwnTracksProtocol extends BaseProtocol {
+
+ public OwnTracksProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(16384));
+ pipeline.addLast(new OwnTracksProtocolDecoder(OwnTracksProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/OwnTracksProtocolDecoder.java b/src/main/java/org/traccar/protocol/OwnTracksProtocolDecoder.java
new file mode 100644
index 000000000..323d97fa3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/OwnTracksProtocolDecoder.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2017 Jan-Piet Mens (jpmens@gmail.com)
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.traccar.BaseHttpProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import java.io.StringReader;
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class OwnTracksProtocolDecoder extends BaseHttpProtocolDecoder {
+
+ public OwnTracksProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ JsonObject root = Json.createReader(
+ new StringReader(request.content().toString(StandardCharsets.US_ASCII))).readObject();
+
+ if (!root.containsKey("_type")) {
+ sendResponse(channel, HttpResponseStatus.OK);
+ return null;
+ }
+ if (!root.getString("_type").equals("location")
+ && !root.getString("_type").equals("lwt")) {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ String uniqueId;
+
+ if (root.containsKey("topic")) {
+ uniqueId = root.getString("topic");
+ if (root.containsKey("tid")) {
+ position.set("tid", root.getString("tid"));
+ }
+ } else {
+ uniqueId = root.getString("tid");
+ }
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, uniqueId);
+ if (deviceSession == null) {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+
+ if (root.getString("_type").equals("lwt")) {
+ sendResponse(channel, HttpResponseStatus.OK);
+ return null;
+ }
+
+ if (root.containsKey("t") && root.getString("t").equals("p")) {
+ sendResponse(channel, HttpResponseStatus.OK);
+ return null;
+ }
+
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(root.getJsonNumber("tst").longValue() * 1000));
+ if (root.containsKey("sent")) {
+ position.setDeviceTime(new Date(root.getJsonNumber("sent").longValue() * 1000));
+ }
+
+ position.setValid(true);
+
+ position.setLatitude(root.getJsonNumber("lat").doubleValue());
+ position.setLongitude(root.getJsonNumber("lon").doubleValue());
+
+ if (root.containsKey("vel")) {
+ position.setSpeed(UnitsConverter.knotsFromKph(root.getInt("vel")));
+ }
+ if (root.containsKey("alt")) {
+ position.setAltitude(root.getInt("alt"));
+ }
+ if (root.containsKey("cog")) {
+ position.setCourse(root.getInt("cog"));
+ }
+ if (root.containsKey("acc")) {
+ position.setAccuracy(root.getInt("acc"));
+ }
+ if (root.containsKey("t")) {
+ String trigger = root.getString("t");
+ position.set("t", trigger);
+ Integer reportType = -1;
+ if (root.containsKey("rty")) {
+ reportType = root.getInt("rty");
+ }
+ setEventOrAlarm(position, trigger, reportType);
+ }
+ if (root.containsKey("batt")) {
+ position.set(Position.KEY_BATTERY_LEVEL, root.getInt("batt"));
+ }
+ if (root.containsKey("uext")) {
+ position.set(Position.KEY_POWER, root.getJsonNumber("uext").doubleValue());
+ }
+ if (root.containsKey("ubatt")) {
+ position.set(Position.KEY_BATTERY, root.getJsonNumber("ubatt").doubleValue());
+ }
+ if (root.containsKey("vin")) {
+ position.set(Position.KEY_VIN, root.getString("vin"));
+ }
+ if (root.containsKey("name")) {
+ position.set(Position.KEY_VIN, root.getString("name"));
+ }
+ if (root.containsKey("rpm")) {
+ position.set(Position.KEY_RPM, root.getInt("rpm"));
+ }
+ if (root.containsKey("ign")) {
+ position.set(Position.KEY_IGNITION, root.getBoolean("ign"));
+ }
+ if (root.containsKey("motion")) {
+ position.set(Position.KEY_MOTION, root.getBoolean("motion"));
+ }
+ if (root.containsKey("odometer")) {
+ position.set(Position.KEY_ODOMETER, root.getJsonNumber("odometer").doubleValue() * 1000.0);
+ }
+ if (root.containsKey("hmc")) {
+ position.set(Position.KEY_HOURS, root.getJsonNumber("hmc").doubleValue() / 3600.0);
+ }
+
+ if (root.containsKey("anum")) {
+ Integer numberOfAnalogueInputs = root.getInt("anum");
+ for (Integer i = 0; i < numberOfAnalogueInputs; i++) {
+ String indexString = String.format("%02d", i);
+ if (root.containsKey("adda-" + indexString)) {
+ position.set(Position.PREFIX_ADC + (i + 1), root.getString("adda-" + indexString));
+ }
+ if (root.containsKey("temp_c-" + indexString)) {
+ position.set(Position.PREFIX_TEMP + (i + 1),
+ root.getJsonNumber("temp_c-" + indexString).doubleValue());
+ }
+ }
+ }
+
+ sendResponse(channel, HttpResponseStatus.OK);
+ return position;
+ }
+
+ private void setEventOrAlarm(Position position, String trigger, Integer reportType) {
+ switch (trigger) {
+ case "9":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ case "1":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_ON);
+ break;
+ case "i":
+ position.set(Position.KEY_IGNITION, true);
+ break;
+ case "I":
+ position.set(Position.KEY_IGNITION, false);
+ break;
+ case "E":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_RESTORED);
+ break;
+ case "e":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_CUT);
+ break;
+ case "!":
+ position.set(Position.KEY_ALARM, Position.ALARM_TOW);
+ break;
+ case "s":
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ break;
+ case "h":
+ switch (reportType) {
+ case 0:
+ case 3:
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ break;
+ case 1:
+ case 4:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ break;
+ case 2:
+ case 5:
+ default:
+ position.set(Position.KEY_ALARM, Position.ALARM_CORNERING);
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/protocol/PathAwayProtocol.java b/src/main/java/org/traccar/protocol/PathAwayProtocol.java
new file mode 100644
index 000000000..6b5d75c5e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PathAwayProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class PathAwayProtocol extends BaseProtocol {
+
+ public PathAwayProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(16384));
+ pipeline.addLast(new PathAwayProtocolDecoder(PathAwayProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/PathAwayProtocolDecoder.java b/src/main/java/org/traccar/protocol/PathAwayProtocolDecoder.java
new file mode 100644
index 000000000..02a15e34a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PathAwayProtocolDecoder.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.QueryStringDecoder;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class PathAwayProtocolDecoder extends BaseProtocolDecoder {
+
+ public PathAwayProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$PWS,")
+ .number("d+,") // version
+ .expression("[^,]*,") // name
+ .expression("[^,]*,") // icon
+ .expression("[^,]*,") // color
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+.?d*),") // altitude
+ .number("(-?d+.?d*),") // speed
+ .number("(-?d+.?d*),") // course
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
+
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, decoder.parameters().get("UserName").get(0));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, decoder.parameters().get("LOC").get(0));
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setValid(true);
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ if (channel != null) {
+ FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress)).addListener(ChannelFutureListener.CLOSE);
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/PiligrimProtocol.java b/src/main/java/org/traccar/protocol/PiligrimProtocol.java
new file mode 100644
index 000000000..d88c1ab72
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PiligrimProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class PiligrimProtocol extends BaseProtocol {
+
+ public PiligrimProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(16384));
+ pipeline.addLast(new PiligrimProtocolDecoder(PiligrimProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/PiligrimProtocolDecoder.java b/src/main/java/org/traccar/protocol/PiligrimProtocolDecoder.java
new file mode 100644
index 000000000..47aa86da7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PiligrimProtocolDecoder.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.QueryStringDecoder;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+
+public class PiligrimProtocolDecoder extends BaseProtocolDecoder {
+
+ public PiligrimProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private void sendResponse(Channel channel, String message) {
+ if (channel != null) {
+ FullHttpResponse response = new DefaultFullHttpResponse(
+ HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
+ Unpooled.copiedBuffer(message, StandardCharsets.US_ASCII));
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ public static final int MSG_GPS = 0xF1;
+ public static final int MSG_GPS_SENSORS = 0xF2;
+ public static final int MSG_EVENTS = 0xF3;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ String uri = request.uri();
+
+ if (uri.startsWith("/config")) {
+
+ sendResponse(channel, "CONFIG: OK");
+
+ } else if (uri.startsWith("/addlog")) {
+
+ sendResponse(channel, "ADDLOG: OK");
+
+ } else if (uri.startsWith("/inform")) {
+
+ sendResponse(channel, "INFORM: OK");
+
+ } else if (uri.startsWith("/bingps")) {
+
+ sendResponse(channel, "BINGPS: OK");
+
+ QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, decoder.parameters().get("imei").get(0));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+ ByteBuf buf = request.content();
+
+ while (buf.readableBytes() > 2) {
+
+ buf.readUnsignedByte(); // header
+ int type = buf.readUnsignedByte();
+ buf.readUnsignedByte(); // length
+
+ if (type == MSG_GPS || type == MSG_GPS_SENSORS) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDay(buf.readUnsignedByte())
+ .setMonth(buf.getByte(buf.readerIndex()) & 0x0f)
+ .setYear(2010 + (buf.readUnsignedByte() >> 4))
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ position.setTime(dateBuilder.getDate());
+
+ double latitude = buf.readUnsignedByte();
+ latitude += buf.readUnsignedByte() / 60.0;
+ latitude += buf.readUnsignedByte() / 6000.0;
+ latitude += buf.readUnsignedByte() / 600000.0;
+
+ double longitude = buf.readUnsignedByte();
+ longitude += buf.readUnsignedByte() / 60.0;
+ longitude += buf.readUnsignedByte() / 6000.0;
+ longitude += buf.readUnsignedByte() / 600000.0;
+
+ int flags = buf.readUnsignedByte();
+ if (BitUtil.check(flags, 0)) {
+ latitude = -latitude;
+ }
+ if (BitUtil.check(flags, 1)) {
+ longitude = -longitude;
+ }
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+
+ int satellites = buf.readUnsignedByte();
+ position.set(Position.KEY_SATELLITES, satellites);
+ position.setValid(satellites >= 3);
+
+ position.setSpeed(buf.readUnsignedByte());
+
+ double course = buf.readUnsignedByte() << 1;
+ course += (flags >> 2) & 1;
+ course += buf.readUnsignedByte() / 100.0;
+ position.setCourse(course);
+
+ if (type == MSG_GPS_SENSORS) {
+ double power = buf.readUnsignedByte();
+ power += buf.readUnsignedByte() << 8;
+ position.set(Position.KEY_POWER, power * 0.01);
+
+ double battery = buf.readUnsignedByte();
+ battery += buf.readUnsignedByte() << 8;
+ position.set(Position.KEY_BATTERY, battery * 0.01);
+
+ buf.skipBytes(6);
+ }
+
+ positions.add(position);
+
+ } else if (type == MSG_EVENTS) {
+
+ buf.skipBytes(13);
+ }
+ }
+
+ return positions;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/PretraceProtocol.java b/src/main/java/org/traccar/protocol/PretraceProtocol.java
new file mode 100644
index 000000000..f753cbdb4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PretraceProtocol.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class PretraceProtocol extends BaseProtocol {
+
+ public PretraceProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_POSITION_PERIODIC);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, ')'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new PretraceProtocolEncoder());
+ pipeline.addLast(new PretraceProtocolDecoder(PretraceProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/PretraceProtocolDecoder.java b/src/main/java/org/traccar/protocol/PretraceProtocolDecoder.java
new file mode 100644
index 000000000..a19384e62
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PretraceProtocolDecoder.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class PretraceProtocolDecoder extends BaseProtocolDecoder {
+
+ public PretraceProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("(")
+ .number("(d{15})") // imei
+ .number("Uddd") // type
+ .number("d") // gps type
+ .expression("([AV])") // validity
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number("(dd)(dd.dddd)") // latitude
+ .expression("([NS])")
+ .number("(ddd)(dd.dddd)") // longitude
+ .expression("([EW])")
+ .number("(ddd)") // speed
+ .number("(ddd)") // course
+ .number("(xxx)") // altitude
+ .number("(x{8})") // odometer
+ .number("(x)") // satellites
+ .number("(dd)") // hdop
+ .number("(dd)") // gsm
+ .expression("(.{8}),&") // state
+ .expression("(.+)?") // optional data
+ .text("^")
+ .number("xx") // checksum
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.next().equals("A"));
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt(0)));
+ position.setCourse(parser.nextInt(0));
+ position.setAltitude(parser.nextHexInt(0));
+
+ position.set(Position.KEY_ODOMETER, parser.nextHexInt(0));
+ position.set(Position.KEY_SATELLITES, parser.nextHexInt(0));
+ position.set(Position.KEY_HDOP, parser.nextInt(0));
+ position.set(Position.KEY_RSSI, parser.nextInt(0));
+
+ parser.next(); // state
+
+ if (parser.hasNext()) {
+ for (String value : parser.next().split(",")) {
+ switch (value.charAt(0)) {
+ case 'P':
+ if (value.charAt(1) == '1') {
+ if (value.charAt(4) == '%') {
+ position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(value.substring(2, 4)));
+ } else {
+ position.set(Position.KEY_BATTERY, Integer.parseInt(value.substring(2), 16) * 0.01);
+ }
+ } else {
+ position.set(Position.KEY_POWER, Integer.parseInt(value.substring(2), 16) * 0.01);
+ }
+ break;
+ case 'T':
+ double temperature = Integer.parseInt(value.substring(2), 16) * 0.25;
+ if (value.charAt(1) == '1') {
+ position.set(Position.KEY_DEVICE_TEMP, temperature);
+ } else {
+ position.set(Position.PREFIX_TEMP + (value.charAt(1) - '0'), temperature);
+ }
+ break;
+ case 'F':
+ position.set("fuel" + (value.charAt(1) - '0'), Integer.parseInt(value.substring(2), 16) * 0.01);
+ break;
+ case 'R':
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, value.substring(3));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/PretraceProtocolEncoder.java b/src/main/java/org/traccar/protocol/PretraceProtocolEncoder.java
new file mode 100644
index 000000000..9cf951e3b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PretraceProtocolEncoder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.Context;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+public class PretraceProtocolEncoder extends BaseProtocolEncoder {
+
+ private String formatCommand(String uniqueId, String data) {
+ String content = uniqueId + data;
+ return String.format("(%s^%02X)", content, Checksum.xor(content));
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ String uniqueId = Context.getIdentityManager().getById(command.getDeviceId()).getUniqueId();
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatCommand(uniqueId, command.getString(Command.KEY_DATA));
+ case Command.TYPE_POSITION_PERIODIC:
+ return formatCommand(
+ uniqueId, String.format("D221%1$d,%1$d,,", command.getInteger(Command.KEY_FREQUENCY)));
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/PricolProtocol.java b/src/main/java/org/traccar/protocol/PricolProtocol.java
new file mode 100644
index 000000000..6821cd949
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PricolProtocol.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.FixedLengthFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class PricolProtocol extends BaseProtocol {
+
+ public PricolProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new FixedLengthFrameDecoder(64));
+ pipeline.addLast(new PricolProtocolDecoder(PricolProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new PricolProtocolDecoder(PricolProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/PricolProtocolDecoder.java b/src/main/java/org/traccar/protocol/PricolProtocolDecoder.java
new file mode 100644
index 000000000..190c68258
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/PricolProtocolDecoder.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+
+public class PricolProtocolDecoder extends BaseProtocolDecoder {
+
+ public PricolProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedByte(); // header
+
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, buf.readSlice(7).toString(StandardCharsets.US_ASCII));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set("eventType", buf.readUnsignedByte());
+ position.set("packetVersion", buf.readUnsignedByte());
+ position.set(Position.KEY_STATUS, buf.readUnsignedByte());
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ position.set(Position.KEY_GPS, buf.readUnsignedByte());
+
+ position.setTime(new DateBuilder()
+ .setDateReverse(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte()).getDate());
+
+ position.setValid(true);
+
+ double lat = buf.getUnsignedShort(buf.readerIndex()) / 100;
+ lat += (buf.readUnsignedShort() % 100 * 10000 + buf.readUnsignedShort()) / 600000.0;
+ position.setLatitude(buf.readUnsignedByte() == 'S' ? -lat : lat);
+
+ double lon = buf.getUnsignedMedium(buf.readerIndex()) / 100;
+ lon += (buf.readUnsignedMedium() % 100 * 10000 + buf.readUnsignedShort()) / 600000.0;
+ position.setLongitude(buf.readUnsignedByte() == 'W' ? -lon : lon);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+
+ position.set(Position.KEY_INPUT, buf.readUnsignedShort());
+ position.set(Position.KEY_OUTPUT, buf.readUnsignedByte());
+
+ position.set("analogAlerts", buf.readUnsignedByte());
+ position.set("customAlertTypes", buf.readUnsignedShort());
+
+ for (int i = 1; i <= 5; i++) {
+ position.set(Position.PREFIX_ADC + i, buf.readUnsignedShort());
+ }
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedMedium());
+ position.set(Position.KEY_RPM, buf.readUnsignedShort());
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(
+ Unpooled.copiedBuffer("ACK", StandardCharsets.US_ASCII), remoteAddress));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ProgressProtocol.java b/src/main/java/org/traccar/protocol/ProgressProtocol.java
new file mode 100644
index 000000000..aac84205d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ProgressProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import java.nio.ByteOrder;
+public class ProgressProtocol extends BaseProtocol {
+
+ public ProgressProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 2, 2, 4, 0, true));
+ pipeline.addLast(new ProgressProtocolDecoder(ProgressProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ProgressProtocolDecoder.java b/src/main/java/org/traccar/protocol/ProgressProtocolDecoder.java
new file mode 100644
index 000000000..0025cd9e7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ProgressProtocolDecoder.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class ProgressProtocolDecoder extends BaseProtocolDecoder {
+
+ private long lastIndex;
+ private long newIndex;
+
+ public ProgressProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_NULL = 0;
+ public static final int MSG_IDENT = 1;
+ public static final int MSG_IDENT_FULL = 2;
+ public static final int MSG_POINT = 10;
+ public static final int MSG_LOG_SYNC = 100;
+ public static final int MSG_LOGMSG = 101;
+ public static final int MSG_TEXT = 102;
+ public static final int MSG_ALARM = 200;
+ public static final int MSG_ALARM_RECIEVED = 201;
+
+ private void requestArchive(Channel channel) {
+ if (lastIndex == 0) {
+ lastIndex = newIndex;
+ } else if (newIndex > lastIndex) {
+ ByteBuf request = Unpooled.buffer(12);
+ request.writeShortLE(MSG_LOG_SYNC);
+ request.writeShortLE(4);
+ request.writeIntLE((int) lastIndex);
+ request.writeIntLE(0);
+ channel.writeAndFlush(new NetworkMessage(request, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+ int type = buf.readUnsignedShortLE();
+ buf.readUnsignedShortLE(); // length
+
+ if (type == MSG_IDENT || type == MSG_IDENT_FULL) {
+
+ buf.readUnsignedIntLE(); // id
+ int length = buf.readUnsignedShortLE();
+ buf.skipBytes(length);
+ length = buf.readUnsignedShortLE();
+ buf.skipBytes(length);
+ length = buf.readUnsignedShortLE();
+ String imei = buf.readSlice(length).toString(StandardCharsets.US_ASCII);
+ getDeviceSession(channel, remoteAddress, imei);
+
+ } else if (type == MSG_POINT || type == MSG_ALARM || type == MSG_LOGMSG) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ int recordCount = 1;
+ if (type == MSG_LOGMSG) {
+ recordCount = buf.readUnsignedShortLE();
+ }
+
+ for (int j = 0; j < recordCount; j++) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (type == MSG_LOGMSG) {
+ position.set(Position.KEY_ARCHIVE, true);
+ int subtype = buf.readUnsignedShortLE();
+ if (subtype == MSG_ALARM) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ if (buf.readUnsignedShortLE() > buf.readableBytes()) {
+ lastIndex += 1;
+ break; // workaround for device bug
+ }
+ lastIndex = buf.readUnsignedIntLE();
+ position.set(Position.KEY_INDEX, lastIndex);
+ } else {
+ newIndex = buf.readUnsignedIntLE();
+ }
+
+ position.setTime(new Date(buf.readUnsignedIntLE() * 1000));
+ position.setLatitude(buf.readIntLE() * 180.0 / 0x7FFFFFFF);
+ position.setLongitude(buf.readIntLE() * 180.0 / 0x7FFFFFFF);
+ position.setSpeed(buf.readUnsignedIntLE() * 0.01);
+ position.setCourse(buf.readUnsignedShortLE() * 0.01);
+ position.setAltitude(buf.readUnsignedShortLE() * 0.01);
+
+ int satellites = buf.readUnsignedByte();
+ position.setValid(satellites >= 3);
+ position.set(Position.KEY_SATELLITES, satellites);
+
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+
+ long extraFlags = buf.readLongLE();
+
+ if (BitUtil.check(extraFlags, 0)) {
+ int count = buf.readUnsignedShortLE();
+ for (int i = 1; i <= count; i++) {
+ position.set(Position.PREFIX_ADC + i, buf.readUnsignedShortLE());
+ }
+ }
+
+ if (BitUtil.check(extraFlags, 1)) {
+ int size = buf.readUnsignedShortLE();
+ position.set("can", buf.toString(buf.readerIndex(), size, StandardCharsets.US_ASCII));
+ buf.skipBytes(size);
+ }
+
+ if (BitUtil.check(extraFlags, 2)) {
+ position.set("passenger", ByteBufUtil.hexDump(buf.readSlice(buf.readUnsignedShortLE())));
+ }
+
+ if (type == MSG_ALARM) {
+ position.set(Position.KEY_ALARM, true);
+ byte[] response = {(byte) 0xC9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+ channel.writeAndFlush(new NetworkMessage(Unpooled.wrappedBuffer(response), remoteAddress));
+ }
+
+ buf.readUnsignedIntLE(); // crc
+
+ positions.add(position);
+ }
+
+ requestArchive(channel);
+
+ return positions;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Pt3000Protocol.java b/src/main/java/org/traccar/protocol/Pt3000Protocol.java
new file mode 100644
index 000000000..1ad0026a3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Pt3000Protocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Pt3000Protocol extends BaseProtocol {
+
+ public Pt3000Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, 'd')); // probably wrong
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Pt3000ProtocolDecoder(Pt3000Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Pt3000ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Pt3000ProtocolDecoder.java
new file mode 100644
index 000000000..e7f9e062a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Pt3000ProtocolDecoder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Pt3000ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Pt3000ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("%(d+),") // imei
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Pt502FrameDecoder.java b/src/main/java/org/traccar/protocol/Pt502FrameDecoder.java
new file mode 100644
index 000000000..316cd987f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Pt502FrameDecoder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+import java.nio.charset.StandardCharsets;
+
+public class Pt502FrameDecoder extends BaseFrameDecoder {
+
+ private static final int BINARY_HEADER = 5;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0xbf
+ && buf.toString(buf.readerIndex() + BINARY_HEADER, 4, StandardCharsets.US_ASCII).equals("$PHD")) {
+
+ int length = buf.getUnsignedShortLE(buf.readerIndex() + 3);
+ if (buf.readableBytes() >= length) {
+ buf.skipBytes(BINARY_HEADER);
+ ByteBuf result = buf.readRetainedSlice(length - BINARY_HEADER - 2);
+ buf.skipBytes(2); // line break
+ return result;
+ }
+
+ } else {
+
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0xbf) {
+ buf.skipBytes(BINARY_HEADER);
+ }
+
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '\r');
+ if (index < 0) {
+ index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '\n');
+ }
+
+ if (index > 0) {
+ ByteBuf result = buf.readRetainedSlice(index - buf.readerIndex());
+ while (buf.isReadable()
+ && (buf.getByte(buf.readerIndex()) == '\r' || buf.getByte(buf.readerIndex()) == '\n')) {
+ buf.skipBytes(1);
+ }
+ return result;
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Pt502Protocol.java b/src/main/java/org/traccar/protocol/Pt502Protocol.java
new file mode 100644
index 000000000..5afb9451d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Pt502Protocol.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class Pt502Protocol extends BaseProtocol {
+
+ public Pt502Protocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_SET_TIMEZONE,
+ Command.TYPE_ALARM_SPEED,
+ Command.TYPE_OUTPUT_CONTROL,
+ Command.TYPE_REQUEST_PHOTO);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Pt502FrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Pt502ProtocolEncoder());
+ pipeline.addLast(new Pt502ProtocolDecoder(Pt502Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Pt502ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Pt502ProtocolDecoder.java
new file mode 100644
index 000000000..0afec67ad
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Pt502ProtocolDecoder.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2012 Luis Parada (luis.parada@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+public class Pt502ProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final int MAX_CHUNK_SIZE = 960;
+
+ private ByteBuf photo;
+
+ public Pt502ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .any().text("$")
+ .expression("([^,]+),") // type
+ .number("(d+),") // id
+ .number("(dd)(dd)(dd).(ddd),") // time (hhmmss.sss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.dddd),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.dddd),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+)?,") // speed
+ .number("(d+.d+)?,") // course
+ .number("(dd)(dd)(dd),,,") // date (ddmmyy)
+ .expression("./")
+ .expression("([01])+,") // input
+ .expression("([01])+/") // output
+ .expression("([^/]+)?/") // adc
+ .number("(d+)") // odometer
+ .expression("/([^/]+)?/") // rfid
+ .number("(xxx)").optional(2) // state
+ .any()
+ .compile();
+
+ private String decodeAlarm(String value) {
+ switch (value) {
+ case "IN1":
+ return Position.ALARM_SOS;
+ case "GOF":
+ return Position.ALARM_GEOFENCE;
+ case "TOW":
+ return Position.ALARM_TOW;
+ case "HDA":
+ return Position.ALARM_ACCELERATION;
+ case "HDB":
+ return Position.ALARM_BRAKING;
+ case "FDA":
+ return Position.ALARM_FATIGUE_DRIVING;
+ case "SKA":
+ return Position.ALARM_VIBRATION;
+ case "PMA":
+ return Position.ALARM_MOVEMENT;
+ case "CPA":
+ return Position.ALARM_POWER_CUT;
+ default:
+ return null;
+ }
+ }
+
+ private Position decodePosition(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.next()));
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+
+ if (parser.hasNext()) {
+ String[] values = parser.next().split(",");
+ for (int i = 0; i < values.length; i++) {
+ position.set(Position.PREFIX_ADC + (i + 1), Integer.parseInt(values[i], 16));
+ }
+ }
+
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ if (parser.hasNext()) {
+ int value = parser.nextHexInt(0);
+ position.set(Position.KEY_BATTERY, value >> 8);
+ position.set(Position.KEY_RSSI, (value >> 4) & 0xf);
+ position.set(Position.KEY_SATELLITES, value & 0xf);
+ }
+
+ return position;
+ }
+
+ private void requestPhotoFragment(Channel channel) {
+ if (channel != null) {
+ int offset = photo.writerIndex();
+ int size = Math.min(photo.writableBytes(), MAX_CHUNK_SIZE);
+ channel.writeAndFlush(new NetworkMessage("#PHD" + offset + "," + size + "\r\n", channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int typeEndIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ',');
+ String type = buf.toString(buf.readerIndex(), typeEndIndex - buf.readerIndex(), StandardCharsets.US_ASCII);
+
+ if (type.startsWith("$PHD")) {
+
+ int dataIndex = buf.indexOf(typeEndIndex + 1, buf.writerIndex(), (byte) ',') + 1;
+ buf.readerIndex(dataIndex);
+
+ if (photo != null) {
+
+ photo.writeBytes(buf.readSlice(buf.readableBytes()));
+
+ if (photo.writableBytes() > 0) {
+
+ requestPhotoFragment(channel);
+
+ } else {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ String uniqueId = Context.getIdentityManager().getById(deviceSession.getDeviceId()).getUniqueId();
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_IMAGE, Context.getMediaManager().writeFile(uniqueId, photo, "jpg"));
+ photo.release();
+ photo = null;
+
+ return position;
+
+ }
+
+ }
+
+ } else {
+
+ if (type.startsWith("$PHO")) {
+ int size = Integer.parseInt(type.split("-")[0].substring(4));
+ if (size > 0) {
+ photo = Unpooled.buffer(size);
+ requestPhotoFragment(channel);
+ }
+ }
+
+ return decodePosition(channel, remoteAddress, buf.toString(StandardCharsets.US_ASCII));
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Pt502ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Pt502ProtocolEncoder.java
new file mode 100644
index 000000000..ed18208cc
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Pt502ProtocolEncoder.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import java.util.TimeZone;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class Pt502ProtocolEncoder extends StringProtocolEncoder implements StringProtocolEncoder.ValueFormatter {
+
+ @Override
+ public String formatValue(String key, Object value) {
+ if (key.equals(Command.KEY_TIMEZONE)) {
+ return String.valueOf(TimeZone.getTimeZone((String) value).getRawOffset() / 3600000);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected String formatCommand(Command command, String format, String... keys) {
+ return formatCommand(command, format, this, keys);
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatCommand(command, "{%s}\r\n", Command.KEY_DATA);
+ case Command.TYPE_OUTPUT_CONTROL:
+ return formatCommand(command, "#OPC{%s},{%s}\r\n", Command.KEY_INDEX, Command.KEY_DATA);
+ case Command.TYPE_SET_TIMEZONE:
+ return formatCommand(command, "#TMZ{%s}\r\n", Command.KEY_TIMEZONE);
+ case Command.TYPE_ALARM_SPEED:
+ return formatCommand(command, "#SPD{%s}\r\n", Command.KEY_DATA);
+ case Command.TYPE_REQUEST_PHOTO:
+ return formatCommand(command, "#PHO\r\n");
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Pt60Protocol.java b/src/main/java/org/traccar/protocol/Pt60Protocol.java
new file mode 100644
index 000000000..c502426c5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Pt60Protocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Pt60Protocol extends BaseProtocol {
+
+ public Pt60Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "@R#@", "@E#@"));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Pt60ProtocolDecoder(Pt60Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Pt60ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Pt60ProtocolDecoder.java
new file mode 100644
index 000000000..6a3fe2734
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Pt60ProtocolDecoder.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class Pt60ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Pt60ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_G_TRACK = 6;
+ public static final int MSG_G_STEP_COUNT = 13;
+ public static final int MSG_G_HEART_RATE = 14;
+
+ public static final int MSG_B_POSITION = 1;
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("@(.)#@[,|]") // header
+ .number("V?dd[,|]") // protocol version
+ .number("(d+)[,|]") // type
+ .number("(d+)[,|]") // imei
+ .number("d+[,|]") // imsi
+ .groupBegin()
+ .expression("[^,|]+[,|]").optional() // firmware version
+ .number("[01][,|]") // state
+ .number("d+[,|]") // battery
+ .groupEnd("?")
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd)[,|]") // time (hhmmss)
+ .expression("(.*)") // data
+ .expression("[,|]")
+ .compile();
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress, String format, int type, String imei) {
+ if (channel != null) {
+ String message;
+ String time = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+ if (format.equals("G")) {
+ message = String.format("@G#@,V01,38,%s,@R#@", time);
+ } else {
+ message = String.format("@B#@|01|%03d|%s|0|%s|@E#@", type + 1, imei, time);
+ }
+ channel.writeAndFlush(new NetworkMessage(message, remoteAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String format = parser.next();
+ int type = parser.nextInt();
+ String imei = parser.next();
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ sendResponse(channel, remoteAddress, format, type, imei);
+
+ if (format.equals("G")) {
+
+ if (type != MSG_G_TRACK && type != MSG_G_STEP_COUNT && type != MSG_G_HEART_RATE) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setDeviceTime(parser.nextDateTime());
+
+ String[] values = parser.next().split(",");
+
+ if (type == MSG_G_TRACK) {
+
+ position.setValid(true);
+ position.setFixTime(position.getDeviceTime());
+
+ String[] coordinates = values[0].split(";");
+ position.setLatitude(Double.parseDouble(coordinates[0]));
+ position.setLongitude(Double.parseDouble(coordinates[1]));
+
+ } else {
+
+ getLastLocation(position, position.getDeviceTime());
+
+ switch (type) {
+ case MSG_G_STEP_COUNT:
+ position.set(Position.KEY_STEPS, Integer.parseInt(values[0]));
+ break;
+ case MSG_G_HEART_RATE:
+ position.set(Position.KEY_HEART_RATE, Integer.parseInt(values[0]));
+ position.set(Position.KEY_BATTERY, Integer.parseInt(values[1]));
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ return position;
+
+ } else {
+
+ if (type != MSG_B_POSITION) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setDeviceTime(parser.nextDateTime());
+
+ String[] values = parser.next().split("\\|");
+
+ if (Integer.parseInt(values[values.length - 1]) == 2) {
+
+ getLastLocation(position, position.getDeviceTime());
+
+ Network network = new Network();
+
+ for (int i = 0; i < values.length - 1; i++) {
+ String[] cellValues = values[i].split(",");
+ CellTower tower = new CellTower();
+ tower.setCellId(Long.parseLong(cellValues[0]));
+ tower.setLocationAreaCode(Integer.parseInt(cellValues[1]));
+ tower.setMobileNetworkCode(Integer.parseInt(cellValues[2]));
+ tower.setMobileCountryCode(Integer.parseInt(cellValues[3]));
+ tower.setSignalStrength(Integer.parseInt(cellValues[4]));
+ network.addCellTower(tower);
+ }
+
+ position.setNetwork(network);
+
+
+ } else {
+
+ position.setValid(true);
+ position.setFixTime(position.getDeviceTime());
+
+ position.setLatitude(Double.parseDouble(values[0]));
+ position.setLongitude(Double.parseDouble(values[1]));
+
+ }
+
+ return position;
+
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RaveonProtocol.java b/src/main/java/org/traccar/protocol/RaveonProtocol.java
new file mode 100644
index 000000000..44faadb3b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RaveonProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class RaveonProtocol extends BaseProtocol {
+
+ public RaveonProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new RaveonProtocolDecoder(RaveonProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RaveonProtocolDecoder.java b/src/main/java/org/traccar/protocol/RaveonProtocolDecoder.java
new file mode 100644
index 000000000..50acd20a1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RaveonProtocolDecoder.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class RaveonProtocolDecoder extends BaseProtocolDecoder {
+
+ public RaveonProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$PRAVE,")
+ .number("(d+),") // id
+ .number("d+,")
+ .number("(-?)(d+)(dd.d+),") // latitude
+ .number("(-?)(d+)(dd.d+),") // longitude
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d),") // validity
+ .number("(d+),") // satellites
+ .number("(-?d+),") // altitude
+ .number("(-?d+),") // temperature
+ .number("(d+.d+),") // power
+ .number("(d+),") // inputs
+ .number("(-?d+),") // gsm
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .expression("([PMACIVSX])?,") // status
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS));
+
+ position.setValid(parser.nextInt(0) != 0);
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+
+ position.setAltitude(parser.nextInt(0));
+
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt(0));
+ position.set(Position.KEY_POWER, parser.nextDouble(0));
+ position.set(Position.KEY_INPUT, parser.nextInt(0));
+ position.set(Position.KEY_RSSI, parser.nextInt(0));
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt(0)));
+ position.setCourse(parser.nextInt(0));
+
+ position.set(Position.KEY_ALARM, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RecodaProtocol.java b/src/main/java/org/traccar/protocol/RecodaProtocol.java
new file mode 100644
index 000000000..0bc9870bc
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RecodaProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import java.nio.ByteOrder;
+public class RecodaProtocol extends BaseProtocol {
+
+ public RecodaProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 4, 4, -8, 0, true));
+ pipeline.addLast(new RecodaProtocolDecoder(RecodaProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RecodaProtocolDecoder.java b/src/main/java/org/traccar/protocol/RecodaProtocolDecoder.java
new file mode 100644
index 000000000..04098225f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RecodaProtocolDecoder.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class RecodaProtocolDecoder extends BaseProtocolDecoder {
+
+ public RecodaProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_HEARTBEAT = 0x00001001;
+ public static final int MSG_REQUEST_RESPONSE = 0x20000001;
+ public static final int MSG_SIGNAL_LINK_REGISTRATION = 0x20001001;
+ public static final int MSG_EVENT_NOTICE = 0x20002001;
+ public static final int MSG_GPS_DATA = 0x20001011;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int type = buf.readIntLE();
+ buf.readUnsignedIntLE(); // length
+
+ if (type != MSG_HEARTBEAT) {
+ buf.readUnsignedShortLE(); // version
+ buf.readUnsignedShortLE(); // index
+ }
+
+ if (type == MSG_SIGNAL_LINK_REGISTRATION) {
+
+ getDeviceSession(channel, remoteAddress, buf.readSlice(12).toString(StandardCharsets.US_ASCII));
+
+ } else if (type == MSG_GPS_DATA) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(buf.readLongLE()));
+
+ int flags = buf.readUnsignedByte();
+
+ if (BitUtil.check(flags, 0)) {
+
+ buf.readUnsignedShortLE(); // declination
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShortLE()));
+
+ position.setLongitude(buf.readUnsignedByte() + buf.readUnsignedByte() / 60.0);
+ position.setLatitude(buf.readUnsignedByte() + buf.readUnsignedByte() / 60.0);
+
+ position.setLongitude(position.getLongitude() + buf.readUnsignedIntLE() / 3600.0);
+ position.setLatitude(position.getLatitude() + buf.readUnsignedIntLE() / 3600.0);
+
+ int status = buf.readUnsignedByte();
+
+ position.setValid(BitUtil.check(status, 0));
+ if (BitUtil.check(status, 1)) {
+ position.setLongitude(-position.getLongitude());
+ }
+ if (!BitUtil.check(status, 2)) {
+ position.setLatitude(-position.getLatitude());
+ }
+
+ } else {
+
+ getLastLocation(position, position.getDeviceTime());
+
+ }
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RetranslatorFrameDecoder.java b/src/main/java/org/traccar/protocol/RetranslatorFrameDecoder.java
new file mode 100644
index 000000000..4edd09418
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RetranslatorFrameDecoder.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class RetranslatorFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int length = 4 + buf.getIntLE(buf.readerIndex());
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RetranslatorProtocol.java b/src/main/java/org/traccar/protocol/RetranslatorProtocol.java
new file mode 100644
index 000000000..fae81f7d2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RetranslatorProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class RetranslatorProtocol extends BaseProtocol {
+
+ public RetranslatorProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new RetranslatorFrameDecoder());
+ pipeline.addLast(new RetranslatorProtocolDecoder(RetranslatorProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RetranslatorProtocolDecoder.java b/src/main/java/org/traccar/protocol/RetranslatorProtocolDecoder.java
new file mode 100644
index 000000000..0688c9b0e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RetranslatorProtocolDecoder.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class RetranslatorProtocolDecoder extends BaseProtocolDecoder {
+
+ public RetranslatorProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(Unpooled.wrappedBuffer(new byte[]{0x11}), remoteAddress));
+ }
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedInt(); // length
+
+ int idLength = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0x00) - buf.readerIndex();
+ String id = buf.readBytes(idLength).toString(StandardCharsets.US_ASCII);
+ buf.readByte();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+
+ buf.readUnsignedInt(); // bit flags
+
+ while (buf.isReadable()) {
+
+ buf.readUnsignedShort(); // block type
+ int blockEnd = buf.readInt() + buf.readerIndex();
+ buf.readUnsignedByte(); // security attribute
+ int dataType = buf.readUnsignedByte();
+
+ int nameLength = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0x00) - buf.readerIndex();
+ String name = buf.readBytes(nameLength).toString(StandardCharsets.US_ASCII);
+ buf.readByte();
+
+ if (name.equals("posinfo")) {
+ position.setValid(true);
+ position.setLongitude(buf.readDoubleLE());
+ position.setLatitude(buf.readDoubleLE());
+ position.setAltitude(buf.readDoubleLE());
+ position.setSpeed(buf.readShort());
+ position.setCourse(buf.readShort());
+ position.set(Position.KEY_SATELLITES, buf.readByte());
+ } else {
+ switch (dataType) {
+ case 1:
+ int len = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0x00) - buf.readerIndex();
+ position.set(name, buf.readBytes(len).toString(StandardCharsets.US_ASCII));
+ buf.readByte();
+ break;
+ case 3:
+ position.set(name, buf.readInt());
+ break;
+ case 4:
+ position.set(name, buf.readDoubleLE());
+ break;
+ case 5:
+ position.set(name, buf.readLong());
+ break;
+ default:
+ break;
+ }
+ }
+
+ buf.readerIndex(blockEnd);
+
+ }
+
+ if (position.getLatitude() == 0 && position.getLongitude() == 0) {
+ getLastLocation(position, position.getDeviceTime());
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RitiProtocol.java b/src/main/java/org/traccar/protocol/RitiProtocol.java
new file mode 100644
index 000000000..de1026672
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RitiProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import java.nio.ByteOrder;
+public class RitiProtocol extends BaseProtocol {
+
+ public RitiProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 105, 2, 3, 0, true));
+ pipeline.addLast(new RitiProtocolDecoder(RitiProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RitiProtocolDecoder.java b/src/main/java/org/traccar/protocol/RitiProtocolDecoder.java
new file mode 100644
index 000000000..46267ca90
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RitiProtocolDecoder.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+public class RitiProtocolDecoder extends BaseProtocolDecoder {
+
+ public RitiProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(buf.readUnsignedShort()));
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set("mode", buf.readUnsignedByte());
+ position.set(Position.KEY_COMMAND, buf.readUnsignedByte());
+ position.set(Position.KEY_POWER, buf.readUnsignedShortLE() * 0.001);
+
+ buf.skipBytes(5); // status
+ buf.readUnsignedShortLE(); // idleCount
+ buf.readUnsignedShortLE(); // idleTime in seconds
+
+ position.set(Position.KEY_DISTANCE, buf.readUnsignedIntLE());
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedIntLE());
+
+ // Parse GPRMC
+ int end = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '*');
+ String gprmc = buf.toString(buf.readerIndex(), end - buf.readerIndex(), StandardCharsets.US_ASCII);
+ Parser parser = new Parser(PATTERN, gprmc);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RoboTrackFrameDecoder.java b/src/main/java/org/traccar/protocol/RoboTrackFrameDecoder.java
new file mode 100644
index 000000000..85ed6c76f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RoboTrackFrameDecoder.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class RoboTrackFrameDecoder extends BaseFrameDecoder {
+
+ private int messageLength(ByteBuf buf) {
+ switch (buf.getUnsignedByte(buf.readerIndex())) {
+ case RoboTrackProtocolDecoder.MSG_ID:
+ return 69;
+ case RoboTrackProtocolDecoder.MSG_ACK:
+ return 3;
+ case RoboTrackProtocolDecoder.MSG_GPS:
+ case RoboTrackProtocolDecoder.MSG_GSM:
+ case RoboTrackProtocolDecoder.MSG_IMAGE_START:
+ return 24;
+ case RoboTrackProtocolDecoder.MSG_IMAGE_DATA:
+ return 8 + buf.getUnsignedShortLE(buf.readerIndex() + 1);
+ case RoboTrackProtocolDecoder.MSG_IMAGE_END:
+ return 6;
+ default:
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int length = messageLength(buf);
+
+ if (buf.readableBytes() >= length) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RoboTrackProtocol.java b/src/main/java/org/traccar/protocol/RoboTrackProtocol.java
new file mode 100644
index 000000000..c2c531293
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RoboTrackProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class RoboTrackProtocol extends BaseProtocol {
+
+ public RoboTrackProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new RoboTrackFrameDecoder());
+ pipeline.addLast(new RoboTrackProtocolDecoder(RoboTrackProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RoboTrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/RoboTrackProtocolDecoder.java
new file mode 100644
index 000000000..b613f31d7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RoboTrackProtocolDecoder.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class RoboTrackProtocolDecoder extends BaseProtocolDecoder {
+
+ public RoboTrackProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_ID = 0x00;
+ public static final int MSG_ACK = 0x80;
+ public static final int MSG_GPS = 0x03;
+ public static final int MSG_GSM = 0x04;
+ public static final int MSG_IMAGE_START = 0x06;
+ public static final int MSG_IMAGE_DATA = 0x07;
+ public static final int MSG_IMAGE_END = 0x08;
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int type = buf.readUnsignedByte();
+
+ if (type == MSG_ID) {
+
+ buf.skipBytes(16); // name
+
+ String imei = buf.readSlice(15).toString(StandardCharsets.US_ASCII);
+
+ if (getDeviceSession(channel, remoteAddress, imei) != null && channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(MSG_ACK);
+ response.writeByte(0x01); // success
+ response.writeByte(0x66); // checksum
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+
+ } else if (type == MSG_GPS || type == MSG_GSM) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setDeviceTime(new Date(buf.readUnsignedIntLE() * 1000));
+
+ if (type == MSG_GPS) {
+
+ position.setValid(true);
+ position.setFixTime(position.getDeviceTime());
+ position.setLatitude(buf.readIntLE() * 0.000001);
+ position.setLongitude(buf.readIntLE() * 0.000001);
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readByte()));
+
+ } else {
+
+ getLastLocation(position, position.getDeviceTime());
+
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShortLE(), buf.readUnsignedShortLE(),
+ buf.readUnsignedShortLE(), buf.readUnsignedShortLE())));
+
+ buf.readUnsignedByte(); // reserved
+
+ }
+
+ int value = buf.readUnsignedByte();
+
+ position.set(Position.KEY_SATELLITES, BitUtil.to(value, 4));
+ position.set(Position.KEY_RSSI, BitUtil.between(value, 4, 7));
+ position.set(Position.KEY_MOTION, BitUtil.check(value, 7));
+
+ value = buf.readUnsignedByte();
+
+ position.set(Position.KEY_CHARGE, BitUtil.check(value, 0));
+
+ for (int i = 1; i <= 4; i++) {
+ position.set(Position.PREFIX_IN + i, BitUtil.check(value, i));
+ }
+
+ position.set(Position.KEY_BATTERY_LEVEL, BitUtil.from(value, 5) * 100 / 7);
+ position.set(Position.KEY_DEVICE_TEMP, buf.readByte());
+
+ for (int i = 1; i <= 3; i++) {
+ position.set(Position.PREFIX_ADC + i, buf.readUnsignedShortLE());
+ }
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RuptelaProtocol.java b/src/main/java/org/traccar/protocol/RuptelaProtocol.java
new file mode 100644
index 000000000..1ac62570a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RuptelaProtocol.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class RuptelaProtocol extends BaseProtocol {
+
+ public RuptelaProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_CONFIGURATION,
+ Command.TYPE_GET_VERSION,
+ Command.TYPE_FIRMWARE_UPDATE,
+ Command.TYPE_OUTPUT_CONTROL,
+ Command.TYPE_SET_CONNECTION,
+ Command.TYPE_SET_ODOMETER);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 2, 0));
+ pipeline.addLast(new RuptelaProtocolEncoder());
+ pipeline.addLast(new RuptelaProtocolDecoder(RuptelaProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RuptelaProtocolDecoder.java b/src/main/java/org/traccar/protocol/RuptelaProtocolDecoder.java
new file mode 100644
index 000000000..b043b6201
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RuptelaProtocolDecoder.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DataConverter;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class RuptelaProtocolDecoder extends BaseProtocolDecoder {
+
+ public RuptelaProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_RECORDS = 1;
+ public static final int MSG_DEVICE_CONFIGURATION = 2;
+ public static final int MSG_DEVICE_VERSION = 3;
+ public static final int MSG_FIRMWARE_UPDATE = 4;
+ public static final int MSG_SET_CONNECTION = 5;
+ public static final int MSG_SET_ODOMETER = 6;
+ public static final int MSG_SMS_VIA_GPRS_RESPONSE = 7;
+ public static final int MSG_SMS_VIA_GPRS = 8;
+ public static final int MSG_DTCS = 9;
+ public static final int MSG_SET_IO = 17;
+ public static final int MSG_EXTENDED_RECORDS = 68;
+
+ private Position decodeCommandResponse(DeviceSession deviceSession, int type, ByteBuf buf) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_TYPE, type);
+
+ switch (type) {
+ case MSG_DEVICE_CONFIGURATION:
+ case MSG_DEVICE_VERSION:
+ case MSG_FIRMWARE_UPDATE:
+ case MSG_SMS_VIA_GPRS_RESPONSE:
+ position.set(Position.KEY_RESULT,
+ buf.toString(buf.readerIndex(), buf.readableBytes() - 2, StandardCharsets.US_ASCII).trim());
+ return position;
+ case MSG_SET_IO:
+ position.set(Position.KEY_RESULT,
+ String.valueOf(buf.readUnsignedByte()));
+ return position;
+ default:
+ return null;
+ }
+ }
+
+ private long readValue(ByteBuf buf, int length, boolean signed) {
+ switch (length) {
+ case 1:
+ return signed ? buf.readByte() : buf.readUnsignedByte();
+ case 2:
+ return signed ? buf.readShort() : buf.readUnsignedShort();
+ case 4:
+ return signed ? buf.readInt() : buf.readUnsignedInt();
+ default:
+ return buf.readLong();
+ }
+ }
+
+ private void decodeParameter(Position position, int id, ByteBuf buf, int length) {
+ switch (id) {
+ case 2:
+ case 3:
+ case 4:
+ position.set("di" + (id - 1), readValue(buf, length, false));
+ break;
+ case 5:
+ position.set(Position.KEY_IGNITION, readValue(buf, length, false) == 1);
+ break;
+ case 74:
+ position.set(Position.PREFIX_TEMP + 3, readValue(buf, length, true) * 0.1);
+ break;
+ case 78:
+ case 79:
+ case 80:
+ position.set(Position.PREFIX_TEMP + (id - 78), readValue(buf, length, true) * 0.1);
+ break;
+ default:
+ position.set(Position.PREFIX_IO + id, readValue(buf, length, false));
+ break;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedShort(); // data length
+
+ String imei = String.format("%015d", buf.readLong());
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type = buf.readUnsignedByte();
+
+ if (type == MSG_RECORDS || type == MSG_EXTENDED_RECORDS) {
+
+ List<Position> positions = new LinkedList<>();
+
+ buf.readUnsignedByte(); // records left
+ int count = buf.readUnsignedByte();
+
+ for (int i = 0; i < count; i++) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+ buf.readUnsignedByte(); // timestamp extension
+
+ if (type == MSG_EXTENDED_RECORDS) {
+ buf.readUnsignedByte(); // record extension
+ }
+
+ buf.readUnsignedByte(); // priority (reserved)
+
+ position.setValid(true);
+ position.setLongitude(buf.readInt() / 10000000.0);
+ position.setLatitude(buf.readInt() / 10000000.0);
+ position.setAltitude(buf.readUnsignedShort() / 10.0);
+ position.setCourse(buf.readUnsignedShort() / 100.0);
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte() / 10.0);
+
+ if (type == MSG_EXTENDED_RECORDS) {
+ position.set(Position.KEY_EVENT, buf.readUnsignedShort());
+ } else {
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+ }
+
+ // Read 1 byte data
+ int cnt = buf.readUnsignedByte();
+ for (int j = 0; j < cnt; j++) {
+ int id = type == MSG_EXTENDED_RECORDS ? buf.readUnsignedShort() : buf.readUnsignedByte();
+ decodeParameter(position, id, buf, 1);
+ }
+
+ // Read 2 byte data
+ cnt = buf.readUnsignedByte();
+ for (int j = 0; j < cnt; j++) {
+ int id = type == MSG_EXTENDED_RECORDS ? buf.readUnsignedShort() : buf.readUnsignedByte();
+ decodeParameter(position, id, buf, 2);
+ }
+
+ // Read 4 byte data
+ cnt = buf.readUnsignedByte();
+ for (int j = 0; j < cnt; j++) {
+ int id = type == MSG_EXTENDED_RECORDS ? buf.readUnsignedShort() : buf.readUnsignedByte();
+ decodeParameter(position, id, buf, 4);
+ }
+
+ // Read 8 byte data
+ cnt = buf.readUnsignedByte();
+ for (int j = 0; j < cnt; j++) {
+ int id = type == MSG_EXTENDED_RECORDS ? buf.readUnsignedShort() : buf.readUnsignedByte();
+ decodeParameter(position, id, buf, 8);
+ }
+
+ positions.add(position);
+ }
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(
+ Unpooled.wrappedBuffer(DataConverter.parseHex("0002640113bc")), remoteAddress));
+ }
+
+ return positions;
+
+ } else if (type == MSG_DTCS) {
+
+ List<Position> positions = new LinkedList<>();
+
+ int count = buf.readUnsignedByte();
+
+ for (int i = 0; i < count; i++) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedByte(); // reserved
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+
+ position.setValid(true);
+ position.setLongitude(buf.readInt() / 10000000.0);
+ position.setLatitude(buf.readInt() / 10000000.0);
+
+ if (buf.readUnsignedByte() == 2) {
+ position.set(Position.KEY_ARCHIVE, true);
+ }
+
+ position.set(Position.KEY_DTCS, buf.readSlice(5).toString(StandardCharsets.US_ASCII));
+
+ positions.add(position);
+ }
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(
+ Unpooled.wrappedBuffer(DataConverter.parseHex("00026d01c4a4")), remoteAddress));
+ }
+
+ return positions;
+
+ } else {
+
+ return decodeCommandResponse(deviceSession, type, buf);
+
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/RuptelaProtocolEncoder.java b/src/main/java/org/traccar/protocol/RuptelaProtocolEncoder.java
new file mode 100644
index 000000000..4242584c9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/RuptelaProtocolEncoder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class RuptelaProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeContent(int type, ByteBuf content) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeShort(1 + content.readableBytes());
+ buf.writeByte(100 + type);
+ buf.writeBytes(content);
+ buf.writeShort(Checksum.crc16(Checksum.CRC16_KERMIT, buf.nioBuffer(2, buf.writerIndex() - 2)));
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ ByteBuf content = Unpooled.buffer();
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ content.writeBytes(command.getString(Command.KEY_DATA).getBytes(StandardCharsets.US_ASCII));
+ return encodeContent(RuptelaProtocolDecoder.MSG_SMS_VIA_GPRS, content);
+ case Command.TYPE_CONFIGURATION:
+ content.writeBytes((command.getString(Command.KEY_DATA) + "\r\n").getBytes(StandardCharsets.US_ASCII));
+ return encodeContent(RuptelaProtocolDecoder.MSG_DEVICE_CONFIGURATION, content);
+ case Command.TYPE_GET_VERSION:
+ return encodeContent(RuptelaProtocolDecoder.MSG_DEVICE_VERSION, content);
+ case Command.TYPE_FIRMWARE_UPDATE:
+ content.writeBytes("|FU_STRT*\r\n".getBytes(StandardCharsets.US_ASCII));
+ return encodeContent(RuptelaProtocolDecoder.MSG_FIRMWARE_UPDATE, content);
+ case Command.TYPE_OUTPUT_CONTROL:
+ content.writeInt(command.getInteger(Command.KEY_INDEX));
+ content.writeInt(Integer.parseInt(command.getString(Command.KEY_DATA)));
+ return encodeContent(RuptelaProtocolDecoder.MSG_SET_IO, content);
+ case Command.TYPE_SET_CONNECTION:
+ String c = command.getString(Command.KEY_SERVER) + "," + command.getInteger(Command.KEY_PORT) + ",TCP";
+ content.writeBytes(c.getBytes(StandardCharsets.US_ASCII));
+ return encodeContent(RuptelaProtocolDecoder.MSG_SET_CONNECTION, content);
+ case Command.TYPE_SET_ODOMETER:
+ content.writeInt(Integer.parseInt(command.getString(Command.KEY_DATA)));
+ return encodeContent(RuptelaProtocolDecoder.MSG_SET_ODOMETER, content);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SabertekFrameDecoder.java b/src/main/java/org/traccar/protocol/SabertekFrameDecoder.java
new file mode 100644
index 000000000..ad5000bf8
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SabertekFrameDecoder.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class SabertekFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int beginIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0x02);
+ if (beginIndex >= 0) {
+ int endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0x03);
+ if (endIndex >= 0) {
+ buf.readerIndex(beginIndex + 1);
+ ByteBuf frame = buf.readRetainedSlice(endIndex - beginIndex - 1);
+ buf.readerIndex(endIndex + 1);
+ buf.skipBytes(2); // end line
+ return frame;
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SabertekProtocol.java b/src/main/java/org/traccar/protocol/SabertekProtocol.java
new file mode 100644
index 000000000..0ec847b60
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SabertekProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SabertekProtocol extends BaseProtocol {
+
+ public SabertekProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new SabertekFrameDecoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new SabertekProtocolDecoder(SabertekProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SabertekProtocolDecoder.java b/src/main/java/org/traccar/protocol/SabertekProtocolDecoder.java
new file mode 100644
index 000000000..3033aa2cc
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SabertekProtocolDecoder.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class SabertekProtocolDecoder extends BaseProtocolDecoder {
+
+ public SabertekProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text(",")
+ .number("(d+),") // id
+ .number("d,") // type
+ .groupBegin()
+ .number("d+,") // imei
+ .number("d+,") // scid
+ .expression("[^,]*,") // phone
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .groupEnd("?")
+ .number("(d+),") // battery
+ .number("(d+),") // rssi
+ .number("(d+),") // state
+ .number("(d+),") // events
+ .number("(d),") // valid
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // altitude
+ .number("(d+),") // satellites
+ .number("(d+),") // odometer
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(
+ Unpooled.wrappedBuffer(new byte[]{(byte) (deviceSession != null ? 0x06 : 0x15)}), remoteAddress));
+ }
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (parser.hasNext(6)) {
+ position.setTime(parser.nextDateTime());
+ } else {
+ position.setTime(new Date());
+ }
+
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+ position.set(Position.KEY_RSSI, parser.nextInt());
+
+ int state = parser.nextInt();
+
+ position.set(Position.KEY_IGNITION, BitUtil.check(state, 0));
+ position.set(Position.KEY_CHARGE, BitUtil.check(state, 1));
+
+ if (BitUtil.check(state, 2)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_JAMMING);
+ }
+ if (BitUtil.check(state, 3)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_TAMPERING);
+ }
+
+ int events = parser.nextInt();
+
+ if (BitUtil.check(events, 0)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ }
+ if (BitUtil.check(events, 1)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ }
+ if (BitUtil.check(events, 2)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCIDENT);
+ }
+ if (BitUtil.check(events, 3)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_CORNERING);
+ }
+
+ position.setValid(parser.nextInt() == 1);
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt()));
+ position.setCourse(parser.nextInt());
+ position.setAltitude(parser.nextInt());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_ODOMETER, parser.nextInt() * 1000L);
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SanavProtocol.java b/src/main/java/org/traccar/protocol/SanavProtocol.java
new file mode 100644
index 000000000..6799c57e6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SanavProtocol.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SanavProtocol extends BaseProtocol {
+
+ public SanavProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new SanavProtocolDecoder(SanavProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new SanavProtocolDecoder(SanavProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SanavProtocolDecoder.java b/src/main/java/org/traccar/protocol/SanavProtocolDecoder.java
new file mode 100644
index 000000000..7e1c158e6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SanavProtocolDecoder.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class SanavProtocolDecoder extends BaseProtocolDecoder {
+
+ public SanavProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("imei[:=]")
+ .number("(d+)") // imei
+ .expression("&?rmc[:=]")
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).d+,") // time (hhmmss.sss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+),") // speed
+ .number("(d+.d+)?,") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .groupBegin()
+ .expression("[^*]*")
+ .text("*")
+ .number("xx,")
+ .expression("[^,]+,") // status
+ .number("(d+),") // io
+ .groupEnd("?")
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble());
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ if (parser.hasNext()) {
+ int io = parser.nextHexInt();
+ for (int i = 0; i < 5; i++) {
+ position.set(Position.PREFIX_IN + (i + 1), BitUtil.check(io, i));
+ }
+ position.set(Position.KEY_IGNITION, BitUtil.check(io, 5));
+ position.set(Position.PREFIX_OUT + 1, BitUtil.check(io, 6));
+ position.set(Position.PREFIX_OUT + 2, BitUtil.check(io, 7));
+ position.set(Position.KEY_CHARGE, BitUtil.check(io, 8));
+ if (!BitUtil.check(io, 9)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ }
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SatsolProtocol.java b/src/main/java/org/traccar/protocol/SatsolProtocol.java
new file mode 100644
index 000000000..b69fdd1fe
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SatsolProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import java.nio.ByteOrder;
+
+public class SatsolProtocol extends BaseProtocol {
+
+ public SatsolProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1400, 8, 2, 0, 0, true));
+ pipeline.addLast(new SatsolProtocolDecoder(SatsolProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SatsolProtocolDecoder.java b/src/main/java/org/traccar/protocol/SatsolProtocolDecoder.java
new file mode 100644
index 000000000..c457d5620
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SatsolProtocolDecoder.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class SatsolProtocolDecoder extends BaseProtocolDecoder {
+
+ public SatsolProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedShortLE(); // checksum
+ buf.readUnsignedShortLE(); // preamble
+ long id = buf.readUnsignedIntLE();
+ buf.readUnsignedShortLE(); // length
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(id));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ while (buf.isReadable()) {
+
+ buf.readUnsignedShortLE(); // checksum
+ buf.readUnsignedShortLE(); // checksum
+ buf.readUnsignedShortLE(); // type
+ int length = buf.readUnsignedShortLE();
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(buf.readUnsignedIntLE() * 1000));
+ position.setLatitude(buf.readUnsignedIntLE() * 0.000001);
+ position.setLongitude(buf.readUnsignedIntLE() * 0.000001);
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShortLE() * 0.01));
+ position.setAltitude(buf.readShortLE());
+ position.setCourse(buf.readUnsignedShortLE());
+ position.setValid(buf.readUnsignedByte() > 0);
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+
+ if (BitUtil.check(buf.readUnsignedByte(), 0)) {
+ position.set(Position.KEY_ARCHIVE, true);
+ }
+
+ positions.add(position);
+
+ buf.skipBytes(length);
+
+ }
+
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeShortLE(0);
+ response.writeShortLE(0x4CBF); // preamble
+ response.writeIntLE((int) id);
+ response.writeShortLE(0);
+ response.setShortLE(0, Checksum.crc16(
+ Checksum.CRC16_CCITT_FALSE, response.nioBuffer(2, response.readableBytes() - 2)));
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SigfoxProtocol.java b/src/main/java/org/traccar/protocol/SigfoxProtocol.java
new file mode 100644
index 000000000..e2f2cbe1f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SigfoxProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SigfoxProtocol extends BaseProtocol {
+
+ public SigfoxProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(65535));
+ pipeline.addLast(new SigfoxProtocolDecoder(SigfoxProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SigfoxProtocolDecoder.java b/src/main/java/org/traccar/protocol/SigfoxProtocolDecoder.java
new file mode 100644
index 000000000..d7836b35d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SigfoxProtocolDecoder.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.traccar.BaseHttpProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DataConverter;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import java.io.StringReader;
+import java.net.SocketAddress;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class SigfoxProtocolDecoder extends BaseHttpProtocolDecoder {
+
+ public SigfoxProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+ JsonObject json = Json.createReader(new StringReader(URLDecoder.decode(
+ request.content().toString(StandardCharsets.UTF_8).split("=")[0], "UTF-8"))).readObject();
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, json.getString("device"));
+ if (deviceSession == null) {
+ sendResponse(channel, HttpResponseStatus.BAD_REQUEST);
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(json.getInt("time") * 1000L));
+
+ ByteBuf buf = Unpooled.wrappedBuffer(DataConverter.parseHex(json.getString("data")));
+ try {
+ int type = buf.readUnsignedByte() >> 4;
+ if (type == 0) {
+
+ position.setValid(true);
+ position.setLatitude(buf.readIntLE() * 0.0000001);
+ position.setLongitude(buf.readIntLE() * 0.0000001);
+ position.setCourse(buf.readUnsignedByte() * 2);
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte() * 0.025);
+
+ } else {
+
+ getLastLocation(position, position.getDeviceTime());
+
+ }
+ } finally {
+ buf.release();
+ }
+
+ position.set(Position.KEY_RSSI, json.getJsonNumber("rssi").doubleValue());
+ position.set(Position.KEY_INDEX, json.getInt("seqNumber"));
+
+ sendResponse(channel, HttpResponseStatus.OK);
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SiwiProtocol.java b/src/main/java/org/traccar/protocol/SiwiProtocol.java
new file mode 100644
index 000000000..8963721c8
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SiwiProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SiwiProtocol extends BaseProtocol {
+
+ public SiwiProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new SiwiProtocolDecoder(SiwiProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SiwiProtocolDecoder.java b/src/main/java/org/traccar/protocol/SiwiProtocolDecoder.java
new file mode 100644
index 000000000..6b97f5fe0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SiwiProtocolDecoder.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class SiwiProtocolDecoder extends BaseProtocolDecoder {
+
+ public SiwiProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$").expression("[A-Z]+,") // header
+ .number("(d+),") // device id
+ .number("d+,") // unit no
+ .expression("([A-Z]),") // reason
+ .number("d+,") // command code
+ .number("[^,]*,") // command value
+ .expression("([01]),") // ignition
+ .expression("[01],") // power cut
+ .expression("[01],") // box open
+ .number("d+,") // message key
+ .number("(d+),") // odometer
+ .number("(d+),") // speed
+ .number("(d+),") // satellites
+ .expression("([AV]),") // valid
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+),") // altitude
+ .number("(d+),") // course
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_EVENT, parser.next());
+ position.set(Position.KEY_IGNITION, parser.next().equals("1"));
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt(0)));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+ position.setCourse(parser.nextInt(0));
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY, "IST"));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SkypatrolProtocol.java b/src/main/java/org/traccar/protocol/SkypatrolProtocol.java
new file mode 100644
index 000000000..7c6203d86
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SkypatrolProtocol.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SkypatrolProtocol extends BaseProtocol {
+
+ public SkypatrolProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new SkypatrolProtocolDecoder(SkypatrolProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SkypatrolProtocolDecoder.java b/src/main/java/org/traccar/protocol/SkypatrolProtocolDecoder.java
new file mode 100644
index 000000000..3c7ca6dc5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SkypatrolProtocolDecoder.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+
+public class SkypatrolProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SkypatrolProtocolDecoder.class);
+
+ private final long defaultMask;
+
+ public SkypatrolProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ defaultMask = Context.getConfig().getInteger(getProtocolName() + ".mask");
+ }
+
+ private static double convertCoordinate(long coordinate) {
+ int sign = 1;
+ if (coordinate > 0x7fffffffL) {
+ sign = -1;
+ coordinate = 0xffffffffL - coordinate;
+ }
+
+ long degrees = coordinate / 1000000;
+ double minutes = (coordinate % 1000000) / 10000.0;
+
+ return sign * (degrees + minutes / 60);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int apiNumber = buf.readUnsignedShort();
+ int commandType = buf.readUnsignedByte();
+ int messageType = BitUtil.from(buf.readUnsignedByte(), 4);
+ long mask = defaultMask;
+ if (buf.readUnsignedByte() == 4) {
+ mask = buf.readUnsignedInt();
+ }
+
+ // Binary position report
+ if (apiNumber == 5 && commandType == 2 && messageType == 1 && BitUtil.check(mask, 0)) {
+
+ Position position = new Position(getProtocolName());
+
+ if (BitUtil.check(mask, 1)) {
+ position.set(Position.KEY_STATUS, buf.readUnsignedInt());
+ }
+
+ String id;
+ if (BitUtil.check(mask, 23)) {
+ id = buf.toString(buf.readerIndex(), 8, StandardCharsets.US_ASCII).trim();
+ buf.skipBytes(8);
+ } else if (BitUtil.check(mask, 2)) {
+ id = buf.toString(buf.readerIndex(), 22, StandardCharsets.US_ASCII).trim();
+ buf.skipBytes(22);
+ } else {
+ LOGGER.warn("No device id field");
+ return null;
+ }
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (BitUtil.check(mask, 3)) {
+ position.set(Position.PREFIX_IO + 1, buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(mask, 4)) {
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(mask, 5)) {
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(mask, 7)) {
+ buf.readUnsignedByte(); // function category
+ }
+
+ DateBuilder dateBuilder = new DateBuilder();
+
+ if (BitUtil.check(mask, 8)) {
+ dateBuilder.setDateReverse(
+ buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ }
+
+ if (BitUtil.check(mask, 9)) {
+ position.setValid(buf.readUnsignedByte() == 1); // gps status
+ }
+
+ if (BitUtil.check(mask, 10)) {
+ position.setLatitude(convertCoordinate(buf.readUnsignedInt()));
+ }
+
+ if (BitUtil.check(mask, 11)) {
+ position.setLongitude(convertCoordinate(buf.readUnsignedInt()));
+ }
+
+ if (BitUtil.check(mask, 12)) {
+ position.setSpeed(buf.readUnsignedShort() / 10.0);
+ }
+
+ if (BitUtil.check(mask, 13)) {
+ position.setCourse(buf.readUnsignedShort() / 10.0);
+ }
+
+ if (BitUtil.check(mask, 14)) {
+ dateBuilder.setTime(
+ buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+ }
+
+ position.setTime(dateBuilder.getDate());
+
+ if (BitUtil.check(mask, 15)) {
+ position.setAltitude(buf.readMedium());
+ }
+
+ if (BitUtil.check(mask, 16)) {
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ }
+
+ if (BitUtil.check(mask, 17)) {
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(mask, 20)) {
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedInt());
+ }
+
+ if (BitUtil.check(mask, 21)) {
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+ }
+
+ if (BitUtil.check(mask, 22)) {
+ buf.skipBytes(6); // time of message generation
+ }
+
+ if (BitUtil.check(mask, 24)) {
+ position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.001);
+ }
+
+ if (BitUtil.check(mask, 25)) {
+ buf.skipBytes(18); // gps overspeed
+ }
+
+ if (BitUtil.check(mask, 26)) {
+ buf.skipBytes(54); // cell information
+ }
+
+ if (BitUtil.check(mask, 28)) {
+ position.set(Position.KEY_INDEX, buf.readUnsignedShort());
+ }
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SmartSoleProtocol.java b/src/main/java/org/traccar/protocol/SmartSoleProtocol.java
new file mode 100644
index 000000000..bcf43f68b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SmartSoleProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SmartSoleProtocol extends BaseProtocol {
+
+ public SmartSoleProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '$'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new SmartSoleProtocolDecoder(SmartSoleProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SmartSoleProtocolDecoder.java b/src/main/java/org/traccar/protocol/SmartSoleProtocolDecoder.java
new file mode 100644
index 000000000..04920c969
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SmartSoleProtocolDecoder.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class SmartSoleProtocolDecoder extends BaseProtocolDecoder {
+
+ public SmartSoleProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("#GTXRP=")
+ .number("(d+),") // imei
+ .number("d+,") // report type
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+),") // altitude
+ .number("(d+),") // speed
+ .number("([01]),") // valid
+ .number("(d+),") // satellites
+ .number("(d+.d+),") // hdop
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d+.d+),") // battery
+ .number("(d+)") // status
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setFixTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+ position.setAltitude(parser.nextInt());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextInt()));
+ position.setValid(parser.nextInt() == 1);
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ position.setDeviceTime(parser.nextDateTime());
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.KEY_STATUS, parser.nextInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SmokeyProtocol.java b/src/main/java/org/traccar/protocol/SmokeyProtocol.java
new file mode 100644
index 000000000..482c8347c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SmokeyProtocol.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SmokeyProtocol extends BaseProtocol {
+
+ public SmokeyProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new SmokeyProtocolDecoder(SmokeyProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SmokeyProtocolDecoder.java b/src/main/java/org/traccar/protocol/SmokeyProtocolDecoder.java
new file mode 100644
index 000000000..9da52e97a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SmokeyProtocolDecoder.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+import org.traccar.model.WifiAccessPoint;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+public class SmokeyProtocolDecoder extends BaseProtocolDecoder {
+
+ public SmokeyProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_DATE_RECORD = 0;
+ public static final int MSG_DATE_RECORD_ACK = 1;
+
+ private static void sendResponse(
+ Channel channel, SocketAddress remoteAddress, ByteBuf id, int index, int report) {
+
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeBytes("SM".getBytes(StandardCharsets.US_ASCII));
+ response.writeByte(3); // protocol version
+ response.writeByte(MSG_DATE_RECORD_ACK);
+ response.writeBytes(id);
+ response.writeInt(
+ (int) ChronoUnit.SECONDS.between(Instant.parse("2000-01-01T00:00:00.00Z"), Instant.now()));
+ response.writeByte(index);
+ response.writeByte(report - 0x200);
+
+ short checksum = (short) 0xF5A0;
+ for (int i = 0; i < response.readableBytes(); i += 2) {
+ checksum ^= response.getShortLE(i);
+ }
+ response.writeShort(checksum);
+
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ buf.readUnsignedByte(); // protocol version
+
+ int type = buf.readUnsignedByte();
+
+ ByteBuf id = buf.readSlice(8);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, ByteBufUtil.hexDump(id));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (type == MSG_DATE_RECORD) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_VERSION_FW, buf.readUnsignedShort());
+
+ int status = buf.readUnsignedShort();
+ position.set(Position.KEY_STATUS, status);
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(2000, 1, 1).addSeconds(buf.readUnsignedInt());
+
+ getLastLocation(position, dateBuilder.getDate());
+
+ int index = buf.readUnsignedByte();
+ position.set(Position.KEY_INDEX, index);
+
+ int report = buf.readUnsignedShort();
+
+ buf.readUnsignedShort(); // length
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort());
+
+ Network network = new Network();
+
+ if (report != 0x0203) {
+
+ int count = 1;
+ if (report != 0x0200) {
+ count = buf.readUnsignedByte();
+ }
+
+ for (int i = 0; i < count; i++) {
+ int mcc = buf.readUnsignedShort();
+ int mnc = buf.readUnsignedShort();
+ int lac = buf.readUnsignedShort();
+ int cid = buf.readUnsignedShort();
+ if (i == 0) {
+ buf.readByte(); // timing advance
+ }
+ int rssi = buf.readByte();
+ network.addCellTower(CellTower.from(mcc, mnc, lac, cid, rssi));
+ }
+
+ }
+
+ if (report == 0x0202 || report == 0x0203) {
+
+ int count = buf.readUnsignedByte();
+
+ for (int i = 0; i < count; i++) {
+ buf.readerIndex(buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0) + 1); // ssid
+
+ String mac = String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+ buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte(),
+ buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte());
+
+ network.addWifiAccessPoint(WifiAccessPoint.from(mac, buf.readByte()));
+ }
+
+ }
+
+ position.setNetwork(network);
+
+ sendResponse(channel, remoteAddress, id, index, report);
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SpotProtocol.java b/src/main/java/org/traccar/protocol/SpotProtocol.java
new file mode 100644
index 000000000..bbf0e8d8a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SpotProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SpotProtocol extends BaseProtocol {
+
+ public SpotProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new HttpResponseEncoder());
+ pipeline.addLast(new HttpRequestDecoder());
+ pipeline.addLast(new HttpObjectAggregator(65535));
+ pipeline.addLast(new SpotProtocolDecoder(SpotProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SpotProtocolDecoder.java b/src/main/java/org/traccar/protocol/SpotProtocolDecoder.java
new file mode 100644
index 000000000..da36c2048
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SpotProtocolDecoder.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.traccar.BaseHttpProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Position;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.net.SocketAddress;
+import java.util.LinkedList;
+import java.util.List;
+
+public class SpotProtocolDecoder extends BaseHttpProtocolDecoder {
+
+ private DocumentBuilder documentBuilder;
+ private XPath xPath;
+ private XPathExpression messageExpression;
+
+ public SpotProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ try {
+ DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+ builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ builderFactory.setXIncludeAware(false);
+ builderFactory.setExpandEntityReferences(false);
+ documentBuilder = builderFactory.newDocumentBuilder();
+ xPath = XPathFactory.newInstance().newXPath();
+ messageExpression = xPath.compile("//messageList/message");
+ } catch (ParserConfigurationException | XPathExpressionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ FullHttpRequest request = (FullHttpRequest) msg;
+
+ Document document = documentBuilder.parse(new ByteBufferBackedInputStream(request.content().nioBuffer()));
+ NodeList nodes = (NodeList) messageExpression.evaluate(document, XPathConstants.NODESET);
+
+ List<Position> positions = new LinkedList<>();
+
+ for (int i = 0; i < nodes.getLength(); i++) {
+ Node node = nodes.item(i);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, xPath.evaluate("esnName", node));
+ if (deviceSession != null) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(true);
+ position.setTime(DateUtil.parseDate(xPath.evaluate("timestamp", node)));
+ position.setLatitude(Double.parseDouble(xPath.evaluate("latitude", node)));
+ position.setLongitude(Double.parseDouble(xPath.evaluate("longitude", node)));
+
+ position.set(Position.KEY_EVENT, xPath.evaluate("messageType", node));
+
+ positions.add(position);
+
+ }
+ }
+
+ sendResponse(channel, HttpResponseStatus.OK);
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/StarLinkProtocol.java b/src/main/java/org/traccar/protocol/StarLinkProtocol.java
new file mode 100644
index 000000000..5630722ee
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/StarLinkProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class StarLinkProtocol extends BaseProtocol {
+
+ public StarLinkProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StarLinkProtocolDecoder(StarLinkProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/StarLinkProtocolDecoder.java b/src/main/java/org/traccar/protocol/StarLinkProtocolDecoder.java
new file mode 100644
index 000000000..ed5f81c1c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/StarLinkProtocolDecoder.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+public class StarLinkProtocolDecoder extends BaseProtocolDecoder {
+
+ private String[] dataTags;
+ private DateFormat dateFormat;
+
+ public StarLinkProtocolDecoder(Protocol protocol) {
+ super(protocol);
+
+ String format = Context.getConfig().getString(
+ getProtocolName() + ".format", "#EDT#,#EID#,#PDT#,#LAT#,#LONG#,#SPD#,#HEAD#,#ODO#,"
+ + "#IN1#,#IN2#,#IN3#,#IN4#,#OUT1#,#OUT2#,#OUT3#,#OUT4#,#LAC#,#CID#,#VIN#,#VBAT#,#DEST#,#IGN#,#ENG#");
+ dataTags = format.split(",");
+
+ dateFormat = new SimpleDateFormat(
+ Context.getConfig().getString(getProtocolName() + ".dateFormat", "yyMMddHHmmss"));
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression(".") // protocol head
+ .text("SLU") // message head
+ .number("(x{6}|d{15}),") // id
+ .number("(d+),") // type
+ .number("(d+),") // index
+ .expression("(.+)") // data
+ .text("*")
+ .number("xx") // checksum
+ .compile();
+
+ public static final int MSG_EVENT_REPORT = 6;
+
+ private double parseCoordinate(String value) {
+ int minutesIndex = value.indexOf('.') - 2;
+ double result = Double.parseDouble(value.substring(1, minutesIndex));
+ result += Double.parseDouble(value.substring(minutesIndex)) / 60;
+ return value.charAt(0) == '+' ? result : -result;
+ }
+
+ private String decodeAlarm(int event) {
+ switch (event) {
+ case 6:
+ return Position.ALARM_OVERSPEED;
+ case 7:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 8:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 9:
+ return Position.ALARM_POWER_CUT;
+ case 11:
+ return Position.ALARM_LOW_BATTERY;
+ case 26:
+ return Position.ALARM_TOW;
+ case 36:
+ return Position.ALARM_SOS;
+ case 42:
+ return Position.ALARM_JAMMING;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type = parser.nextInt(0);
+ if (type != MSG_EVENT_REPORT) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setValid(true);
+
+ position.set(Position.KEY_INDEX, parser.nextInt(0));
+
+ String[] data = parser.next().split(",");
+ Integer lac = null, cid = null;
+ int event = 0;
+
+ for (int i = 0; i < Math.min(data.length, dataTags.length); i++) {
+ if (data[i].isEmpty()) {
+ continue;
+ }
+ switch (dataTags[i]) {
+ case "#EDT#":
+ position.setDeviceTime(dateFormat.parse(data[i]));
+ break;
+ case "#EID#":
+ event = Integer.parseInt(data[i]);
+ position.set(Position.KEY_ALARM, decodeAlarm(event));
+ position.set(Position.KEY_EVENT, event);
+ break;
+ case "#PDT#":
+ position.setFixTime(dateFormat.parse(data[i]));
+ break;
+ case "#LAT#":
+ position.setLatitude(parseCoordinate(data[i]));
+ break;
+ case "#LONG#":
+ position.setLongitude(parseCoordinate(data[i]));
+ break;
+ case "#SPD#":
+ position.setSpeed(Double.parseDouble(data[i]));
+ break;
+ case "#HEAD#":
+ position.setCourse(Integer.parseInt(data[i]));
+ break;
+ case "#ODO#":
+ position.set(Position.KEY_ODOMETER, Long.parseLong(data[i]) * 1000);
+ break;
+ case "#IN1#":
+ position.set(Position.PREFIX_IN + 1, Integer.parseInt(data[i]));
+ break;
+ case "#IN2#":
+ position.set(Position.PREFIX_IN + 2, Integer.parseInt(data[i]));
+ break;
+ case "#IN3#":
+ position.set(Position.PREFIX_IN + 3, Integer.parseInt(data[i]));
+ break;
+ case "#IN4#":
+ position.set(Position.PREFIX_IN + 4, Integer.parseInt(data[i]));
+ break;
+ case "#OUT1#":
+ position.set(Position.PREFIX_OUT + 1, Integer.parseInt(data[i]));
+ break;
+ case "#OUT2#":
+ position.set(Position.PREFIX_OUT + 2, Integer.parseInt(data[i]));
+ break;
+ case "#OUT3#":
+ position.set(Position.PREFIX_OUT + 3, Integer.parseInt(data[i]));
+ break;
+ case "#OUT4#":
+ position.set(Position.PREFIX_OUT + 4, Integer.parseInt(data[i]));
+ break;
+ case "#LAC#":
+ if (!data[i].isEmpty()) {
+ lac = Integer.parseInt(data[i]);
+ }
+ break;
+ case "#CID#":
+ if (!data[i].isEmpty()) {
+ cid = Integer.parseInt(data[i]);
+ }
+ break;
+ case "#VIN#":
+ position.set(Position.KEY_POWER, Double.parseDouble(data[i]));
+ break;
+ case "#VBAT#":
+ position.set(Position.KEY_BATTERY, Double.parseDouble(data[i]));
+ break;
+ case "#DEST#":
+ position.set("destination", data[i]);
+ break;
+ case "#IGN#":
+ position.set(Position.KEY_IGNITION, data[i].equals("1"));
+ break;
+ case "#ENG#":
+ position.set("engine", data[i].equals("1"));
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (position.getFixTime() == null) {
+ getLastLocation(position, null);
+ }
+
+ if (lac != null && cid != null) {
+ position.setNetwork(new Network(CellTower.fromLacCid(lac, cid)));
+ }
+
+ if (event == 20) {
+ String rfid = data[data.length - 1];
+ if (rfid.matches("0+")) {
+ rfid = data[data.length - 2];
+ }
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, rfid);
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Stl060FrameDecoder.java b/src/main/java/org/traccar/protocol/Stl060FrameDecoder.java
new file mode 100644
index 000000000..f72474e2b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Stl060FrameDecoder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.CharacterDelimiterFrameDecoder;
+
+public class Stl060FrameDecoder extends CharacterDelimiterFrameDecoder {
+
+ public Stl060FrameDecoder(int maxFrameLength) {
+ super(maxFrameLength, '#');
+ }
+
+ @Override
+ protected Object decode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
+
+ ByteBuf result = (ByteBuf) super.decode(ctx, buf);
+
+ if (result != null) {
+
+ int index = result.indexOf(result.readerIndex(), result.writerIndex(), (byte) '$');
+ if (index == -1) {
+ return result;
+ } else {
+ result.skipBytes(index);
+ return result.readRetainedSlice(result.readableBytes());
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Stl060Protocol.java b/src/main/java/org/traccar/protocol/Stl060Protocol.java
new file mode 100644
index 000000000..2711e936b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Stl060Protocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Stl060Protocol extends BaseProtocol {
+
+ public Stl060Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Stl060FrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Stl060ProtocolDecoder(Stl060Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Stl060ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Stl060ProtocolDecoder.java
new file mode 100644
index 000000000..7b0055aa1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Stl060ProtocolDecoder.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Stl060ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Stl060ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .any()
+ .text("$1,")
+ .number("(d+),") // imei
+ .text("D001,") // type
+ .expression("[^,]*,") // vehicle
+ .number("(dd)/(dd)/(dd),") // date (dd/mm/yy)
+ .number("(dd):(dd):(dd),") // time (hh:mm:ss)
+ .number("(dd)(dd).?(d+)([NS]),") // latitude
+ .number("(ddd)(dd).?(d+)([EW]),") // longitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .groupBegin()
+ .number("(d+),") // odometer
+ .number("(d+),") // Ignition
+ .number("(d+),") // di1
+ .number("(d+),") // di2
+ .number("(d+),") // fuel
+ .or()
+ .expression("([01]),") // charging
+ .expression("([01]),") // ignition
+ .expression("0,0,") // reserved
+ .number("(d+),") // di
+ .expression("([^,]+),") // rfid
+ .number("(d+),") // odometer
+ .number("(d+),") // temperature
+ .number("(d+),") // fuel
+ .expression("([01]),") // accelerometer
+ .expression("([01]),") // do1
+ .expression("([01]),") // do2
+ .groupEnd()
+ .expression("([AV])") // validity
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_MIN_MIN_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_MIN_MIN_HEM));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ // Old format
+ if (parser.hasNext(5)) {
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+ position.set(Position.KEY_IGNITION, parser.nextInt(0) == 1);
+ position.set(Position.KEY_INPUT, parser.nextInt(0) + parser.nextInt(0) << 1);
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt(0));
+ }
+
+ // New format
+ if (parser.hasNext(10)) {
+ position.set(Position.KEY_CHARGE, parser.nextInt(0) == 1);
+ position.set(Position.KEY_IGNITION, parser.nextInt(0) == 1);
+ position.set(Position.KEY_INPUT, parser.nextInt(0));
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0));
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt(0));
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt(0));
+ position.set(Position.KEY_ACCELERATION, parser.nextInt(0) == 1);
+ position.set(Position.KEY_OUTPUT, parser.nextInt(0) + parser.nextInt(0) << 1);
+ }
+
+ position.setValid(parser.next().equals("A"));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SuntechProtocol.java b/src/main/java/org/traccar/protocol/SuntechProtocol.java
new file mode 100644
index 000000000..29ae114e7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SuntechProtocol.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class SuntechProtocol extends BaseProtocol {
+
+ public SuntechProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_OUTPUT_CONTROL,
+ Command.TYPE_REBOOT_DEVICE,
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_ALARM_ARM,
+ Command.TYPE_ALARM_DISARM);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '\r'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new SuntechProtocolEncoder());
+ pipeline.addLast(new SuntechProtocolDecoder(SuntechProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java b/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java
new file mode 100644
index 000000000..922431021
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright 2013 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+
+public class SuntechProtocolDecoder extends BaseProtocolDecoder {
+
+ private int protocolType;
+ private boolean hbm;
+ private boolean includeAdc;
+ private boolean includeTemp;
+
+ public SuntechProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public void setProtocolType(int protocolType) {
+ this.protocolType = protocolType;
+ }
+
+ public int getProtocolType(long deviceId) {
+ return Context.getIdentityManager().lookupAttributeInteger(
+ deviceId, getProtocolName() + ".protocolType", protocolType, true);
+ }
+
+ public void setHbm(boolean hbm) {
+ this.hbm = hbm;
+ }
+
+ public boolean isHbm(long deviceId) {
+ return Context.getIdentityManager().lookupAttributeBoolean(
+ deviceId, getProtocolName() + ".hbm", hbm, true);
+ }
+
+ public void setIncludeAdc(boolean includeAdc) {
+ this.includeAdc = includeAdc;
+ }
+
+ public boolean isIncludeAdc(long deviceId) {
+ return Context.getIdentityManager().lookupAttributeBoolean(
+ deviceId, getProtocolName() + ".includeAdc", includeAdc, true);
+ }
+
+ public void setIncludeTemp(boolean includeTemp) {
+ this.includeTemp = includeTemp;
+ }
+
+ public boolean isIncludeTemp(long deviceId) {
+ return Context.getIdentityManager().lookupAttributeBoolean(
+ deviceId, getProtocolName() + ".includeTemp", includeTemp, true);
+ }
+
+ private Position decode9(
+ Channel channel, SocketAddress remoteAddress, String[] values) throws ParseException {
+ int index = 1;
+
+ String type = values[index++];
+
+ if (!type.equals("Location") && !type.equals("Emergency") && !type.equals("Alert")) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (type.equals("Emergency") || type.equals("Alert")) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+
+ if (!type.equals("Alert") || getProtocolType(deviceSession.getDeviceId()) == 0) {
+ position.set(Position.KEY_VERSION_FW, values[index++]);
+ }
+
+ DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ position.setTime(dateFormat.parse(values[index++] + values[index++]));
+
+ if (getProtocolType(deviceSession.getDeviceId()) == 1) {
+ index += 1; // cell
+ }
+
+ position.setLatitude(Double.parseDouble(values[index++]));
+ position.setLongitude(Double.parseDouble(values[index++]));
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
+ position.setCourse(Double.parseDouble(values[index++]));
+
+ position.setValid(values[index++].equals("1"));
+
+ if (getProtocolType(deviceSession.getDeviceId()) == 1) {
+ position.set(Position.KEY_ODOMETER, Integer.parseInt(values[index++]));
+ }
+
+ return position;
+ }
+
+ private String decodeEmergency(int value) {
+ switch (value) {
+ case 1:
+ return Position.ALARM_SOS;
+ case 2:
+ return Position.ALARM_PARKING;
+ case 3:
+ return Position.ALARM_POWER_CUT;
+ case 5:
+ case 6:
+ return Position.ALARM_DOOR;
+ case 7:
+ return Position.ALARM_MOVEMENT;
+ case 8:
+ return Position.ALARM_SHOCK;
+ default:
+ return null;
+ }
+ }
+
+ private String decodeAlert(int value) {
+ switch (value) {
+ case 1:
+ return Position.ALARM_OVERSPEED;
+ case 5:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 6:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 14:
+ return Position.ALARM_LOW_BATTERY;
+ case 15:
+ return Position.ALARM_SHOCK;
+ case 16:
+ return Position.ALARM_ACCIDENT;
+ case 46:
+ return Position.ALARM_ACCELERATION;
+ case 47:
+ return Position.ALARM_BRAKING;
+ case 48:
+ return Position.ALARM_ACCIDENT;
+ case 50:
+ return Position.ALARM_JAMMING;
+ default:
+ return null;
+ }
+ }
+ private Position decode4(
+ Channel channel, SocketAddress remoteAddress, String[] values) throws ParseException {
+ int index = 0;
+
+ String type = values[index++].substring(5);
+
+ if (!type.equals("STT") && !type.equals("ALT")) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.set(Position.KEY_TYPE, type);
+
+ position.set(Position.KEY_VERSION_FW, values[index++]);
+ index += 1; // model
+
+ Network network = new Network();
+
+ for (int i = 0; i < 7; i++) {
+ int cid = Integer.parseInt(values[index++]);
+ int mcc = Integer.parseInt(values[index++]);
+ int mnc = Integer.parseInt(values[index++]);
+ int lac, rssi;
+ if (i == 0) {
+ rssi = Integer.parseInt(values[index++]);
+ lac = Integer.parseInt(values[index++]);
+ } else {
+ lac = Integer.parseInt(values[index++]);
+ rssi = Integer.parseInt(values[index++]);
+ }
+ index += 1; // timing advance
+ if (cid > 0) {
+ network.addCellTower(CellTower.from(mcc, mnc, lac, cid, rssi));
+ }
+ }
+
+ position.setNetwork(network);
+
+ position.set(Position.KEY_BATTERY, Double.parseDouble(values[index++]));
+ position.set(Position.KEY_ARCHIVE, values[index++].equals("0") ? true : null);
+ position.set(Position.KEY_INDEX, Integer.parseInt(values[index++]));
+ position.set(Position.KEY_STATUS, Integer.parseInt(values[index++]));
+
+ DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ position.setTime(dateFormat.parse(values[index++] + values[index++]));
+
+ position.setLatitude(Double.parseDouble(values[index++]));
+ position.setLongitude(Double.parseDouble(values[index++]));
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
+ position.setCourse(Double.parseDouble(values[index++]));
+
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(values[index++]));
+
+ position.setValid(values[index++].equals("1"));
+
+ return position;
+ }
+
+ private Position decode2356(
+ Channel channel, SocketAddress remoteAddress, String protocol, String[] values) throws ParseException {
+ int index = 0;
+
+ String type = values[index++].substring(5);
+
+ if (!type.equals("STT") && !type.equals("EMG") && !type.equals("EVT") && !type.equals("ALT")) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.set(Position.KEY_TYPE, type);
+
+ if (protocol.equals("ST300") || protocol.equals("ST500") || protocol.equals("ST600")) {
+ index += 1; // model
+ }
+
+ position.set(Position.KEY_VERSION_FW, values[index++]);
+
+ DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ position.setTime(dateFormat.parse(values[index++] + values[index++]));
+
+ if (!protocol.equals("ST500")) {
+ long cid = Long.parseLong(values[index++], 16);
+ if (protocol.equals("ST600")) {
+ position.setNetwork(new Network(CellTower.from(
+ Integer.parseInt(values[index++]), Integer.parseInt(values[index++]),
+ Integer.parseInt(values[index++], 16), cid, Integer.parseInt(values[index++]))));
+ }
+ }
+
+ position.setLatitude(Double.parseDouble(values[index++]));
+ position.setLongitude(Double.parseDouble(values[index++]));
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
+ position.setCourse(Double.parseDouble(values[index++]));
+
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(values[index++]));
+
+ position.setValid(values[index++].equals("1"));
+
+ position.set(Position.KEY_ODOMETER, Integer.parseInt(values[index++]));
+ position.set(Position.KEY_POWER, Double.parseDouble(values[index++]));
+
+ String io = values[index++];
+ if (io.length() == 6) {
+ position.set(Position.KEY_IGNITION, io.charAt(0) == '1');
+ position.set(Position.PREFIX_IN + 1, io.charAt(1) == '1');
+ position.set(Position.PREFIX_IN + 2, io.charAt(2) == '1');
+ position.set(Position.PREFIX_IN + 3, io.charAt(3) == '1');
+ position.set(Position.PREFIX_OUT + 1, io.charAt(4) == '1');
+ position.set(Position.PREFIX_OUT + 2, io.charAt(5) == '1');
+ }
+
+ switch (type) {
+ case "STT":
+ position.set(Position.KEY_STATUS, Integer.parseInt(values[index++]));
+ position.set(Position.KEY_INDEX, Integer.parseInt(values[index++]));
+ break;
+ case "EMG":
+ position.set(Position.KEY_ALARM, decodeEmergency(Integer.parseInt(values[index++])));
+ break;
+ case "EVT":
+ position.set(Position.KEY_EVENT, Integer.parseInt(values[index++]));
+ break;
+ case "ALT":
+ position.set(Position.KEY_ALARM, decodeAlert(Integer.parseInt(values[index++])));
+ break;
+ default:
+ break;
+ }
+
+ if (isHbm(deviceSession.getDeviceId())) {
+
+ if (index < values.length) {
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromMinutes(Integer.parseInt(values[index++])));
+ }
+
+ if (index < values.length) {
+ position.set(Position.KEY_BATTERY, Double.parseDouble(values[index++]));
+ }
+
+ if (index < values.length && values[index++].equals("0")) {
+ position.set(Position.KEY_ARCHIVE, true);
+ }
+
+ if (isIncludeAdc(deviceSession.getDeviceId())) {
+ for (int i = 1; i <= 3; i++) {
+ if (!values[index++].isEmpty()) {
+ position.set(Position.PREFIX_ADC + i, Double.parseDouble(values[index - 1]));
+ }
+ }
+ }
+
+ if (values.length - index >= 2) {
+ String driverUniqueId = values[index++];
+ if (values[index++].equals("1") && !driverUniqueId.isEmpty()) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, driverUniqueId);
+ }
+ }
+
+ if (isIncludeTemp(deviceSession.getDeviceId())) {
+ for (int i = 1; i <= 3; i++) {
+ String temperature = values[index++];
+ String value = temperature.substring(temperature.indexOf(':') + 1);
+ if (!value.isEmpty()) {
+ position.set(Position.PREFIX_TEMP + i, Double.parseDouble(value));
+ }
+ }
+
+ }
+
+ }
+
+ return position;
+ }
+
+ private Position decodeUniversal(
+ Channel channel, SocketAddress remoteAddress, String[] values) throws ParseException {
+ int index = 0;
+
+ String type = values[index++];
+
+ if (!type.equals("STT")) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[index++]);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.set(Position.KEY_TYPE, type);
+
+ int mask = Integer.parseInt(values[index++], 16);
+
+ if (BitUtil.check(mask, 1)) {
+ index += 1; // model
+ }
+
+ if (BitUtil.check(mask, 2)) {
+ position.set(Position.KEY_VERSION_FW, values[index++]);
+ }
+
+ if (BitUtil.check(mask, 3) && values[index++].equals("0")) {
+ position.set(Position.KEY_ARCHIVE, true);
+ }
+
+ if (BitUtil.check(mask, 4) && BitUtil.check(mask, 5)) {
+ DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ position.setTime(dateFormat.parse(values[index++] + values[index++]));
+ }
+
+ if (BitUtil.check(mask, 6)) {
+ index += 1; // cell
+ }
+
+ if (BitUtil.check(mask, 7)) {
+ index += 1; // mcc
+ }
+
+ if (BitUtil.check(mask, 8)) {
+ index += 1; // mnc
+ }
+
+ if (BitUtil.check(mask, 9)) {
+ index += 1; // lac
+ }
+
+ if (BitUtil.check(mask, 10)) {
+ position.set(Position.KEY_RSSI, Integer.parseInt(values[index++]));
+ }
+
+ if (BitUtil.check(mask, 11)) {
+ position.setLatitude(Double.parseDouble(values[index++]));
+ }
+
+ if (BitUtil.check(mask, 12)) {
+ position.setLongitude(Double.parseDouble(values[index++]));
+ }
+
+ if (BitUtil.check(mask, 13)) {
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[index++])));
+ }
+
+ if (BitUtil.check(mask, 14)) {
+ position.setCourse(Double.parseDouble(values[index++]));
+ }
+
+ if (BitUtil.check(mask, 15)) {
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(values[index++]));
+ }
+
+ if (BitUtil.check(mask, 16)) {
+ position.setValid(values[index++].equals("1"));
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String[] values = ((String) msg).split(";");
+
+ if (values[0].length() < 5) {
+ return decodeUniversal(channel, remoteAddress, values);
+ } else if (values[0].startsWith("ST9")) {
+ return decode9(channel, remoteAddress, values);
+ } else if (values[0].startsWith("ST4")) {
+ return decode4(channel, remoteAddress, values);
+ } else {
+ return decode2356(channel, remoteAddress, values[0].substring(0, 5), values);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SuntechProtocolEncoder.java b/src/main/java/org/traccar/protocol/SuntechProtocolEncoder.java
new file mode 100644
index 000000000..90fa4aa39
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SuntechProtocolEncoder.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class SuntechProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_REBOOT_DEVICE:
+ return formatCommand(command, "SA200CMD;{%s};02;Reboot\r", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_POSITION_SINGLE:
+ return formatCommand(command, "SA200GTR;{%s};02;\r", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_OUTPUT_CONTROL:
+ if (command.getAttributes().containsKey(Command.KEY_DATA)) {
+ if (command.getAttributes().get(Command.KEY_DATA).equals("1")) {
+ return formatCommand(command, "SA200CMD;{%s};02;Enable{%s}\r",
+ Command.KEY_UNIQUE_ID, Command.KEY_INDEX);
+ } else {
+ return formatCommand(command, "SA200CMD;{%s};02;Disable{%s}\r",
+ Command.KEY_UNIQUE_ID, Command.KEY_INDEX);
+ }
+ }
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, "SA200CMD;{%s};02;Enable1\r", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, "SA200CMD;{%s};02;Disable1\r", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ALARM_ARM:
+ return formatCommand(command, "SA200CMD;{%s};02;Enable2\r", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ALARM_DISARM:
+ return formatCommand(command, "SA200CMD;{%s};02;Disable2\r", Command.KEY_UNIQUE_ID);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SupermateProtocol.java b/src/main/java/org/traccar/protocol/SupermateProtocol.java
new file mode 100644
index 000000000..46625ddc7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SupermateProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class SupermateProtocol extends BaseProtocol {
+
+ public SupermateProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "#"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new SupermateProtocolDecoder(SupermateProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SupermateProtocolDecoder.java b/src/main/java/org/traccar/protocol/SupermateProtocolDecoder.java
new file mode 100644
index 000000000..40a25bb91
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SupermateProtocolDecoder.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Calendar;
+import java.util.regex.Pattern;
+
+public class SupermateProtocolDecoder extends BaseProtocolDecoder {
+
+ public SupermateProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("d+:") // header
+ .number("(d+):") // imei
+ .number("d+:").text("*,")
+ .number("(d+),") // command id
+ .expression("([^,]{2}),") // command
+ .expression("([AV]),") // validity
+ .number("(xx)(xx)(xx),") // date (yymmdd)
+ .number("(xx)(xx)(xx),") // time (hhmmss)
+ .number("(x)(x{7}),") // latitude
+ .number("(x)(x{7}),") // longitude
+ .number("(x{4}),") // speed
+ .number("(x{4}),") // course
+ .number("(x{12}),") // status
+ .number("(x+),") // signal
+ .number("(d+),") // power
+ .number("(x{4}),") // oil
+ .number("(x+)?") // odometer
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ String imei = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set("commandId", parser.next());
+ position.set(Position.KEY_COMMAND, parser.next());
+
+ position.setValid(parser.next().equals("A"));
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(parser.nextHexInt(0), parser.nextHexInt(0), parser.nextHexInt(0))
+ .setTime(parser.nextHexInt(0), parser.nextHexInt(0), parser.nextHexInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ if (parser.nextHexInt(0) == 8) {
+ position.setLatitude(-parser.nextHexInt(0) / 600000.0);
+ } else {
+ position.setLatitude(parser.nextHexInt(0) / 600000.0);
+ }
+
+ if (parser.nextHexInt(0) == 8) {
+ position.setLongitude(-parser.nextHexInt(0) / 600000.0);
+ } else {
+ position.setLongitude(parser.nextHexInt(0) / 600000.0);
+ }
+
+ position.setSpeed(parser.nextHexInt(0) / 100.0);
+ position.setCourse(parser.nextHexInt(0) / 100.0);
+
+ position.set(Position.KEY_STATUS, parser.next());
+ position.set("signal", parser.next());
+ position.set(Position.KEY_POWER, parser.nextDouble(0));
+ position.set("oil", parser.nextHexInt(0));
+ position.set(Position.KEY_ODOMETER, parser.nextHexInt(0));
+
+ if (channel != null) {
+ Calendar calendar = Calendar.getInstance();
+ String content = String.format("#1:%s:1:*,00000000,UP,%02x%02x%02x,%02x%02x%02x#", imei,
+ calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH),
+ calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), calendar.get(Calendar.SECOND));
+ channel.writeAndFlush(new NetworkMessage(
+ Unpooled.copiedBuffer(content, StandardCharsets.US_ASCII), remoteAddress));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SviasProtocol.java b/src/main/java/org/traccar/protocol/SviasProtocol.java
new file mode 100644
index 000000000..f01f28389
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SviasProtocol.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import org.traccar.model.Command;
+
+public class SviasProtocol extends BaseProtocol {
+
+ public SviasProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_SET_ODOMETER,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_ALARM_ARM,
+ Command.TYPE_ALARM_DISARM,
+ Command.TYPE_ALARM_REMOVE);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "]"));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new SviasProtocolEncoder());
+ pipeline.addLast(new SviasProtocolDecoder(SviasProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SviasProtocolDecoder.java b/src/main/java/org/traccar/protocol/SviasProtocolDecoder.java
new file mode 100644
index 000000000..7e783f6cd
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SviasProtocolDecoder.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.PatternBuilder;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+import org.traccar.DeviceSession;
+import org.traccar.helper.Parser;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+public class SviasProtocolDecoder extends BaseProtocolDecoder {
+
+ public SviasProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("[") // delimiter
+ .number("d{4},") // hardware version
+ .number("d{4},") // software version
+ .number("d+,") // index
+ .number("(d+),") // imei
+ .number("d+,") // hour meter
+ .number("(d+)(dd)(dd),") // date (dmmyy)
+ .number("(d+)(dd)(dd),") // time (hmmss)
+ .number("(-?)(d+)(dd)(d{5}),") // latitude
+ .number("(-?)(d+)(dd)(d{5}),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // odometer
+ .number("(d+),") // input
+ .number("(d+),") // output / status
+ .number("(d),")
+ .number("(d),")
+ .number("(d+),") // power
+ .number("(d+),") // battery level
+ .number("(d+),") // rssi
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg)
+ throws Exception {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("@", remoteAddress));
+ }
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN_MIN));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble() * 0.01));
+ position.setCourse(parser.nextDouble() * 0.01);
+
+ position.set(Position.KEY_ODOMETER, parser.nextInt() * 100);
+
+ int input = parser.nextInt();
+ int output = parser.nextInt();
+
+ position.set(Position.KEY_ALARM, BitUtil.check(input, 0) ? Position.ALARM_SOS : null);
+ position.set(Position.KEY_IGNITION, BitUtil.check(input, 4));
+ position.setValid(BitUtil.check(output, 0));
+
+ position.set(Position.KEY_POWER, parser.nextInt() * 0.001);
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt());
+ position.set(Position.KEY_RSSI, parser.nextInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/SviasProtocolEncoder.java b/src/main/java/org/traccar/protocol/SviasProtocolEncoder.java
new file mode 100644
index 000000000..8bfbef119
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/SviasProtocolEncoder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class SviasProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatCommand(command, "{%s}", Command.KEY_DATA);
+ case Command.TYPE_POSITION_SINGLE:
+ return formatCommand(command, "AT+STR=1*");
+ case Command.TYPE_SET_ODOMETER:
+ return formatCommand(command, "AT+ODT={%s}*", Command.KEY_DATA);
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, "AT+OUT=1,1*");
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, "AT+OUT=1,0*");
+ case Command.TYPE_ALARM_ARM:
+ return formatCommand(command, "AT+OUT=2,1*");
+ case Command.TYPE_ALARM_DISARM:
+ return formatCommand(command, "AT+OUT=2,0*");
+ case Command.TYPE_ALARM_REMOVE:
+ return formatCommand(command, "AT+PNC=600*");
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/T55Protocol.java b/src/main/java/org/traccar/protocol/T55Protocol.java
new file mode 100644
index 000000000..f5ec19094
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/T55Protocol.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class T55Protocol extends BaseProtocol {
+
+ public T55Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new T55ProtocolDecoder(T55Protocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new T55ProtocolDecoder(T55Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/T55ProtocolDecoder.java b/src/main/java/org/traccar/protocol/T55ProtocolDecoder.java
new file mode 100644
index 000000000..ba231a635
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/T55ProtocolDecoder.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.channels.DatagramChannel;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class T55ProtocolDecoder extends BaseProtocolDecoder {
+
+ public T55ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_GPRMC = new PatternBuilder()
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d{2,3})(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .expression("[^*]+")
+ .text("*")
+ .expression("[^,]+")
+ .number(",(d+)") // satellites
+ .number(",(d+)") // imei
+ .expression(",([01])") // ignition
+ .number(",(d+)") // fuel
+ .number(",(d+)").optional(7) // battery
+ .number("((?:,d+)+)?") // parameters
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_GPGGA = new PatternBuilder()
+ .text("$GPGGA,")
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_GPRMA = new PatternBuilder()
+ .text("$GPRMA,")
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),,,")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_TRCCR = new PatternBuilder()
+ .text("$TRCCR,")
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd).?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(d+.d+),") // speed
+ .number("(d+.d+),") // course
+ .number("(-?d+.d+),") // altitude
+ .number("(d+.?d*),") // battery
+ .any()
+ .compile();
+
+ private Position position = null;
+
+ private Position decodeGprmc(
+ DeviceSession deviceSession, String sentence, SocketAddress remoteAddress, Channel channel) {
+
+ if (deviceSession != null && channel != null && !(channel instanceof DatagramChannel)
+ && Context.getIdentityManager().lookupAttributeBoolean(
+ deviceSession.getDeviceId(), getProtocolName() + ".ack", false, true)) {
+ channel.writeAndFlush(new NetworkMessage("OK1\r\n", remoteAddress));
+ }
+
+ Parser parser = new Parser(PATTERN_GPRMC, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ if (deviceSession != null) {
+ position.setDeviceId(deviceSession.getDeviceId());
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ if (parser.hasNext(5)) {
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+
+ deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_IGNITION, parser.hasNext() && parser.next().equals("1"));
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY, parser.nextInt());
+ }
+
+ if (parser.hasNext()) {
+ String[] parameters = parser.next().split(",");
+ for (int i = 1; i < parameters.length; i++) {
+ position.set(Position.PREFIX_IO + i, parameters[i]);
+ }
+ }
+
+ if (deviceSession != null) {
+ return position;
+ } else {
+ this.position = position; // save position
+ return null;
+ }
+ }
+
+ private Position decodeGpgga(DeviceSession deviceSession, String sentence) {
+
+ Parser parser = new Parser(PATTERN_GPGGA, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setCurrentDate()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ return position;
+ }
+
+ private Position decodeGprma(DeviceSession deviceSession, String sentence) {
+
+ Parser parser = new Parser(PATTERN_GPRMA, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date());
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ return position;
+ }
+
+ private Position decodeTrccr(DeviceSession deviceSession, String sentence) {
+
+ Parser parser = new Parser(PATTERN_TRCCR, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0));
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ DeviceSession deviceSession;
+
+ if (!sentence.startsWith("$") && sentence.contains("$")) {
+ int index = sentence.indexOf("$");
+ String id = sentence.substring(0, index);
+ if (id.endsWith(",")) {
+ id = id.substring(0, id.length() - 1);
+ } else if (id.endsWith("/")) {
+ id = id.substring(id.indexOf('/') + 1, id.length() - 1);
+ }
+ deviceSession = getDeviceSession(channel, remoteAddress, id);
+ sentence = sentence.substring(index);
+ } else {
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ }
+
+ if (sentence.startsWith("$PGID")) {
+ getDeviceSession(channel, remoteAddress, sentence.substring(6, sentence.length() - 3));
+ } else if (sentence.startsWith("$DEVID")) {
+ getDeviceSession(channel, remoteAddress, sentence.substring(7, sentence.lastIndexOf('*')));
+ } else if (sentence.startsWith("$PCPTI")) {
+ getDeviceSession(channel, remoteAddress, sentence.substring(7, sentence.indexOf(",", 7)));
+ } else if (sentence.startsWith("IMEI")) {
+ getDeviceSession(channel, remoteAddress, sentence.substring(5));
+ } else if (sentence.startsWith("$IMEI")) {
+ getDeviceSession(channel, remoteAddress, sentence.substring(6));
+ } else if (sentence.startsWith("$GPFID")) {
+ deviceSession = getDeviceSession(channel, remoteAddress, sentence.substring(7));
+ if (deviceSession != null && position != null) {
+ Position position = this.position;
+ position.setDeviceId(deviceSession.getDeviceId());
+ this.position = null;
+ return position;
+ }
+ } else if (sentence.matches("^[0-9A-F]+$")) {
+ getDeviceSession(channel, remoteAddress, sentence);
+ } else if (sentence.startsWith("$GPRMC")) {
+ return decodeGprmc(deviceSession, sentence, remoteAddress, channel);
+ } else if (sentence.startsWith("$GPGGA") && deviceSession != null) {
+ return decodeGpgga(deviceSession, sentence);
+ } else if (sentence.startsWith("$GPRMA") && deviceSession != null) {
+ return decodeGprma(deviceSession, sentence);
+ } else if (sentence.startsWith("$TRCCR") && deviceSession != null) {
+ return decodeTrccr(deviceSession, sentence);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/T57FrameDecoder.java b/src/main/java/org/traccar/protocol/T57FrameDecoder.java
new file mode 100644
index 000000000..14ba31453
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/T57FrameDecoder.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+import java.nio.charset.StandardCharsets;
+
+public class T57FrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ String type = buf.toString(buf.readerIndex() + 5, 2, StandardCharsets.US_ASCII);
+ int count = type.equals("F3") ? 12 : 14;
+
+ int index = 0;
+ while (index >= 0 && count > 0) {
+ index = buf.indexOf(index + 1, buf.writerIndex(), (byte) '#');
+ if (index > 0) {
+ count -= 1;
+ }
+ }
+
+ return index > 0 ? buf.readRetainedSlice(index + 1 - buf.readerIndex()) : null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/T57Protocol.java b/src/main/java/org/traccar/protocol/T57Protocol.java
new file mode 100644
index 000000000..f67f82318
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/T57Protocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class T57Protocol extends BaseProtocol {
+
+ public T57Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new T57FrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new T57ProtocolDecoder(T57Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/T57ProtocolDecoder.java b/src/main/java/org/traccar/protocol/T57ProtocolDecoder.java
new file mode 100644
index 000000000..2a3cca3e4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/T57ProtocolDecoder.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class T57ProtocolDecoder extends BaseProtocolDecoder {
+
+ public T57ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("*T57#")
+ .number("Fd#") // type
+ .number("([^#]+)#") // device id
+ .number("(dd)(dd)(dd)#") // date (ddmmyy)
+ .number("(dd)(dd)(dd)#") // time (hhmmss)
+ .number("(dd)(dd.d+)#") // latitude
+ .expression("([NS])#")
+ .number("(ddd)(dd.d+)#") // longitude
+ .expression("([EW])#")
+ .expression("[^#]+#")
+ .number("(d+.d+)#") // speed
+ .number("(d+.d+)#") // altitude
+ .expression("([AV])") // valid
+ .number("d#") // fix type
+ .number("(d+.d+)#") // battery
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble());
+ position.setAltitude(parser.nextDouble());
+
+ position.setValid(parser.next().equals("A"));
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/T800xProtocol.java b/src/main/java/org/traccar/protocol/T800xProtocol.java
new file mode 100644
index 000000000..85749d0cf
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/T800xProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class T800xProtocol extends BaseProtocol {
+
+ public T800xProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 3, 2, -5, 0));
+ pipeline.addLast(new T800xProtocolEncoder());
+ pipeline.addLast(new T800xProtocolDecoder(T800xProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/T800xProtocolDecoder.java b/src/main/java/org/traccar/protocol/T800xProtocolDecoder.java
new file mode 100644
index 000000000..dfb286257
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/T800xProtocolDecoder.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BcdUtil;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class T800xProtocolDecoder extends BaseProtocolDecoder {
+
+ public T800xProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_LOGIN = 0x01;
+ public static final int MSG_GPS = 0x02;
+ public static final int MSG_HEARTBEAT = 0x03;
+ public static final int MSG_ALARM = 0x04;
+ public static final int MSG_COMMAND = 0x81;
+
+ private void sendResponse(Channel channel, short header, int type, int index, ByteBuf imei) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer(15);
+ response.writeShort(header);
+ response.writeByte(type);
+ response.writeShort(response.capacity()); // length
+ response.writeShort(index);
+ response.writeBytes(imei);
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ private String decodeAlarm(short value) {
+ switch (value) {
+ case 1:
+ return Position.ALARM_POWER_CUT;
+ case 2:
+ return Position.ALARM_LOW_BATTERY;
+ case 3:
+ return Position.ALARM_SOS;
+ case 4:
+ return Position.ALARM_OVERSPEED;
+ case 5:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 6:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 7:
+ return Position.ALARM_TOW;
+ case 8:
+ case 10:
+ return Position.ALARM_VIBRATION;
+ case 21:
+ return Position.ALARM_JAMMING;
+ case 23:
+ return Position.ALARM_POWER_RESTORED;
+ case 24:
+ return Position.ALARM_LOW_POWER;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ short header = buf.readShort();
+ int type = buf.readUnsignedByte();
+ buf.readUnsignedShort(); // length
+ int index = buf.readUnsignedShort();
+ ByteBuf imei = buf.readSlice(8);
+
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, ByteBufUtil.hexDump(imei).substring(1));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ sendResponse(channel, header, type, index, imei);
+
+ if (type == MSG_GPS || type == MSG_ALARM) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_INDEX, index);
+
+ buf.readUnsignedShort(); // acc on interval
+ buf.readUnsignedShort(); // acc off interval
+ buf.readUnsignedByte(); // angle compensation
+ buf.readUnsignedShort(); // distance compensation
+
+ position.set(Position.KEY_RSSI, BitUtil.to(buf.readUnsignedShort(), 7));
+
+ int status = buf.readUnsignedByte();
+ position.set(Position.KEY_SATELLITES, BitUtil.to(status, 5));
+
+ buf.readUnsignedByte(); // gsensor manager status
+ buf.readUnsignedByte(); // other flags
+ buf.readUnsignedByte(); // heartbeat
+ buf.readUnsignedByte(); // relay status
+ buf.readUnsignedShort(); // drag alarm setting
+
+ int io = buf.readUnsignedShort();
+ position.set(Position.KEY_IGNITION, BitUtil.check(io, 14));
+ position.set("ac", BitUtil.check(io, 13));
+
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShort());
+
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte()));
+
+ buf.readUnsignedByte(); // reserved
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+
+ int battery = BcdUtil.readInteger(buf, 2);
+ if (battery == 0) {
+ battery = 100;
+ }
+ position.set(Position.KEY_BATTERY, battery);
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setYear(BcdUtil.readInteger(buf, 2))
+ .setMonth(BcdUtil.readInteger(buf, 2))
+ .setDay(BcdUtil.readInteger(buf, 2))
+ .setHour(BcdUtil.readInteger(buf, 2))
+ .setMinute(BcdUtil.readInteger(buf, 2))
+ .setSecond(BcdUtil.readInteger(buf, 2));
+
+ if (BitUtil.check(status, 6)) {
+
+ position.setValid(!BitUtil.check(status, 7));
+ position.setTime(dateBuilder.getDate());
+ position.setAltitude(buf.readFloatLE());
+ position.setLongitude(buf.readFloatLE());
+ position.setLatitude(buf.readFloatLE());
+ position.setSpeed(UnitsConverter.knotsFromKph(BcdUtil.readInteger(buf, 4) * 0.1));
+ position.setCourse(buf.readUnsignedShort());
+
+ } else {
+
+ getLastLocation(position, dateBuilder.getDate());
+
+ int mcc = buf.readUnsignedShortLE();
+ int mnc = buf.readUnsignedShortLE();
+
+ if (mcc != 0xffff && mnc != 0xffff) {
+ Network network = new Network();
+ for (int i = 0; i < 3; i++) {
+ network.addCellTower(CellTower.from(
+ mcc, mnc, buf.readUnsignedShortLE(), buf.readUnsignedShortLE()));
+ }
+ position.setNetwork(network);
+ }
+
+ }
+
+ if (buf.readableBytes() >= 2) {
+ position.set(Position.KEY_POWER, BcdUtil.readInteger(buf, 4) * 0.01);
+ }
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/T800xProtocolEncoder.java b/src/main/java/org/traccar/protocol/T800xProtocolEncoder.java
new file mode 100644
index 000000000..1d0f3dabe
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/T800xProtocolEncoder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.DataConverter;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class T800xProtocolEncoder extends BaseProtocolEncoder {
+
+ public static final int MODE_SETTING = 0x01;
+ public static final int MODE_BROADCAST = 0x02;
+ public static final int MODE_FORWARD = 0x03;
+
+ private ByteBuf encodeContent(Command command, String content) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeByte('#');
+ buf.writeByte('#');
+ buf.writeByte(T800xProtocolDecoder.MSG_COMMAND);
+ buf.writeShort(7 + 8 + 1 + content.length());
+ buf.writeShort(1); // serial number
+ buf.writeBytes(DataConverter.parseHex("0" + getUniqueId(command.getDeviceId())));
+ buf.writeByte(MODE_SETTING);
+ buf.writeBytes(content.getBytes(StandardCharsets.US_ASCII));
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return encodeContent(command, command.getString(Command.KEY_DATA));
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TaipProtocol.java b/src/main/java/org/traccar/protocol/TaipProtocol.java
new file mode 100644
index 000000000..b8f40a183
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TaipProtocol.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TaipProtocol extends BaseProtocol {
+
+ public TaipProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '<'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new TaipProtocolDecoder(TaipProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new TaipProtocolDecoder(TaipProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TaipProtocolDecoder.java b/src/main/java/org/traccar/protocol/TaipProtocolDecoder.java
new file mode 100644
index 000000000..8a0cb870b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TaipProtocolDecoder.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.DateUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class TaipProtocolDecoder extends BaseProtocolDecoder {
+
+ public TaipProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .groupBegin()
+ .expression("R[EP]V") // type
+ .groupBegin()
+ .number("(dd)") // event
+ .number("(dddd)") // week
+ .number("(d)") // day
+ .groupEnd("?")
+ .number("(d{5})") // seconds
+ .or()
+ .expression("(?:RGP|RCQ|RCV|RBR)") // type
+ .number("(dd)?") // event
+ .number("(dd)(dd)(dd)") // date (mmddyy)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .groupEnd()
+ .groupBegin()
+ .number("([-+]dd)(d{5})") // latitude
+ .number("([-+]ddd)(d{5})") // longitude
+ .or()
+ .number("([-+])(dd)(dd.dddd)") // latitude
+ .number("([-+])(ddd)(dd.dddd)") // longitude
+ .groupEnd()
+ .number("(ddd)") // speed
+ .number("(ddd)") // course
+ .groupBegin()
+ .number("([023])") // fix mode
+ .number("xx") // data age
+ .number("(xx)") // input
+ .number("(dd)") // event
+ .number("(dd)") // hdop
+ .or()
+ .groupBegin()
+ .number("(xx)") // input
+ .number("(xx)") // satellites
+ .number("(ddd)") // battery
+ .number("(x{8})") // odometer
+ .number("[01]") // gps power
+ .groupBegin()
+ .number("([023])") // fix mode
+ .number("(dd)") // pdop
+ .number("dd") // satellites
+ .number("xxxx") // data age
+ .number("[01]") // modem power
+ .number("[0-5]") // gsm status
+ .number("(dd)") // rssi
+ .number("([-+]dddd)") // temperature 1
+ .number("xx") // seconds from last
+ .number("([-+]dddd)") // temperature 2
+ .number("xx") // seconds from last
+ .groupEnd("?")
+ .groupEnd("?")
+ .groupEnd()
+ .any()
+ .compile();
+
+ private Date getTime(long week, long day, long seconds) {
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(1980, 1, 6)
+ .addMillis(((week * 7 + day) * 24 * 60 * 60 + seconds) * 1000);
+ return dateBuilder.getDate();
+ }
+
+ private Date getTime(long seconds) {
+ DateBuilder dateBuilder = new DateBuilder(new Date())
+ .setTime(0, 0, 0, 0)
+ .addMillis(seconds * 1000);
+ return DateUtil.correctDay(dateBuilder.getDate());
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ int beginIndex = sentence.indexOf('>');
+ if (beginIndex != -1) {
+ sentence = sentence.substring(beginIndex + 1);
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ Boolean valid = null;
+ Integer event = null;
+
+ if (parser.hasNext(3)) {
+ event = parser.nextInt();
+ position.setTime(getTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0)));
+ } else if (parser.hasNext()) {
+ position.setTime(getTime(parser.nextInt(0)));
+ }
+
+ if (parser.hasNext()) {
+ event = parser.nextInt();
+ }
+
+ if (parser.hasNext(6)) {
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+ }
+
+ if (parser.hasNext(4)) {
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_DEG));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_DEG));
+ }
+ if (parser.hasNext(6)) {
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ }
+
+ position.setSpeed(UnitsConverter.knotsFromMph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ if (parser.hasNext(4)) {
+ valid = parser.nextInt() > 0;
+ int input = parser.nextHexInt();
+ position.set(Position.KEY_IGNITION, BitUtil.check(input, 7));
+ position.set(Position.KEY_INPUT, input);
+ event = parser.nextInt();
+ position.set(Position.KEY_HDOP, parser.nextInt());
+ }
+
+ if (parser.hasNext(4)) {
+ position.set(Position.KEY_INPUT, parser.nextHexInt(0));
+ position.set(Position.KEY_SATELLITES, parser.nextHexInt(0));
+ position.set(Position.KEY_BATTERY, parser.nextInt(0));
+ position.set(Position.KEY_ODOMETER, parser.nextLong(16, 0));
+ }
+
+ if (parser.hasNext(4)) {
+ valid = parser.nextInt() > 0;
+ position.set(Position.KEY_PDOP, parser.nextInt());
+ position.set(Position.KEY_RSSI, parser.nextInt());
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt() * 0.01);
+ position.set(Position.PREFIX_TEMP + 2, parser.nextInt() * 0.01);
+ }
+
+ position.setValid(valid == null || valid);
+
+ if (event != null) {
+ position.set(Position.KEY_EVENT, event);
+ switch (event) {
+ case 22:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ break;
+ case 23:
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ break;
+ case 24:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCIDENT);
+ break;
+ case 26:
+ case 28:
+ position.set(Position.KEY_ALARM, Position.ALARM_CORNERING);
+ break;
+ default:
+ break;
+ }
+ }
+
+ String[] attributes = null;
+ beginIndex = sentence.indexOf(';');
+ if (beginIndex != -1) {
+ int endIndex = sentence.indexOf('<', beginIndex);
+ if (endIndex == -1) {
+ endIndex = sentence.length();
+ }
+ attributes = sentence.substring(beginIndex, endIndex).split(";");
+ }
+
+ return decodeAttributes(channel, remoteAddress, position, attributes);
+ }
+
+ private Position decodeAttributes(
+ Channel channel, SocketAddress remoteAddress, Position position, String[] attributes) {
+
+ String uniqueId = null;
+ DeviceSession deviceSession = null;
+ String messageIndex = null;
+
+ if (attributes != null) {
+ for (String attribute : attributes) {
+ int index = attribute.indexOf('=');
+ if (index != -1) {
+ String key = attribute.substring(0, index).toLowerCase();
+ String value = attribute.substring(index + 1);
+ switch (key) {
+ case "id":
+ uniqueId = value;
+ deviceSession = getDeviceSession(channel, remoteAddress, value);
+ if (deviceSession != null) {
+ position.setDeviceId(deviceSession.getDeviceId());
+ }
+ break;
+ case "io":
+ position.set(Position.KEY_IGNITION, BitUtil.check(value.charAt(0) - '0', 0));
+ position.set(Position.KEY_CHARGE, BitUtil.check(value.charAt(0) - '0', 1));
+ position.set(Position.KEY_OUTPUT, value.charAt(1) - '0');
+ position.set(Position.KEY_INPUT, value.charAt(2) - '0');
+ break;
+ case "ix":
+ position.set(Position.PREFIX_IO + 1, value);
+ break;
+ case "ad":
+ position.set(Position.PREFIX_ADC + 1, Integer.parseInt(value));
+ break;
+ case "sv":
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(value));
+ break;
+ case "bl":
+ position.set(Position.KEY_BATTERY, Integer.parseInt(value) * 0.001);
+ break;
+ case "vo":
+ position.set(Position.KEY_ODOMETER, Long.parseLong(value));
+ break;
+ default:
+ position.set(key, value);
+ break;
+ }
+ } else if (attribute.startsWith("#")) {
+ messageIndex = attribute;
+ }
+ }
+ }
+
+ if (deviceSession != null) {
+ if (channel != null) {
+ if (messageIndex != null) {
+ String response = ">ACK;ID=" + uniqueId + ";" + messageIndex + ";*";
+ response += String.format("%02X", Checksum.xor(response)) + "<";
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ } else {
+ channel.writeAndFlush(new NetworkMessage(uniqueId, remoteAddress));
+ }
+ }
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TekFrameDecoder.java b/src/main/java/org/traccar/protocol/TekFrameDecoder.java
new file mode 100644
index 000000000..44d2c590e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TekFrameDecoder.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+import org.traccar.helper.BitUtil;
+
+public class TekFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 17) {
+ return null;
+ }
+
+ int length = 17 + buf.getUnsignedByte(16) + (BitUtil.from(buf.getUnsignedByte(15), 6) << 6);
+ if (buf.readableBytes() >= length) {
+ return buf.readBytes(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TekProtocol.java b/src/main/java/org/traccar/protocol/TekProtocol.java
new file mode 100644
index 000000000..c1d78e6f5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TekProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TekProtocol extends BaseProtocol {
+
+ public TekProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new TekFrameDecoder());
+ pipeline.addLast(new TekProtocolDecoder(TekProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TekProtocolDecoder.java b/src/main/java/org/traccar/protocol/TekProtocolDecoder.java
new file mode 100644
index 000000000..a9101e65f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TekProtocolDecoder.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+public class TekProtocolDecoder extends BaseProtocolDecoder {
+
+ public TekProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number(",d+,")
+ .number("(dd)(dd)(dd).d,") // time (hhmmss)
+ .number("(dd)(dd.d+)") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+)") // longitude
+ .expression("([EW]),")
+ .number("(d+.d+),") // hdop
+ .number("(d+.d+),") // altitude
+ .number("(d+),") // fix mode
+ .number("(d+.d+),") // course
+ .number("d+.d+,") // speed km
+ .number("(d+.d+),") // speed kn
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(d+),") // satellites
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedByte(); // product type
+ buf.readUnsignedByte(); // hardware version
+ buf.readUnsignedByte(); // firmware version
+ buf.readUnsignedByte(); // contact reason
+ buf.readUnsignedByte(); // alarm / status
+ buf.readUnsignedByte(); // rssi
+ buf.readUnsignedByte(); // battery / status
+
+ String imei = ByteBufUtil.hexDump(buf.readBytes(8)).substring(1);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type = BitUtil.to(buf.readUnsignedByte(), 6);
+ buf.readUnsignedByte(); // length
+
+ if (type == 4 || type == 8) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int count = buf.readUnsignedShort();
+ buf.readUnsignedByte(); // hours / tickets
+ buf.readUnsignedByte(); // error code
+ buf.readUnsignedByte(); // reserved
+ buf.readUnsignedByte(); // logger speed
+ buf.readUnsignedByte(); // login time
+ buf.readUnsignedByte(); // minutes
+
+ for (int i = 0; i < count; i++) {
+ position.set("rssi" + (i + 1), buf.readUnsignedByte());
+ position.set("temp" + (i + 1), buf.readUnsignedByte() - 30);
+ int data = buf.readUnsignedShort();
+ position.set("src" + (i + 1), BitUtil.from(data, 10));
+ position.set("ullage" + (i + 1), BitUtil.to(data, 10));
+ }
+
+ return position;
+
+ } else if (type == 17) {
+
+ String sentence = buf.toString(StandardCharsets.US_ASCII);
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ position.setAltitude(parser.nextDouble());
+ position.setValid(parser.nextInt() > 0);
+ position.setCourse(parser.nextDouble());
+ position.setSpeed(parser.nextDouble());
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TelemaxProtocol.java b/src/main/java/org/traccar/protocol/TelemaxProtocol.java
new file mode 100644
index 000000000..838da9df1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TelemaxProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TelemaxProtocol extends BaseProtocol {
+
+ public TelemaxProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new TelemaxProtocolDecoder(TelemaxProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TelemaxProtocolDecoder.java b/src/main/java/org/traccar/protocol/TelemaxProtocolDecoder.java
new file mode 100644
index 000000000..9369ab101
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TelemaxProtocolDecoder.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class TelemaxProtocolDecoder extends BaseProtocolDecoder {
+
+ public TelemaxProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private String readValue(String sentence, int[] index, int length) {
+ String value = sentence.substring(index[0], index[0] + length);
+ index[0] += length;
+ return value;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("%")) {
+ int length = Integer.parseInt(sentence.substring(1, 3));
+ getDeviceSession(channel, remoteAddress, sentence.substring(3, 3 + length));
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int[] index = {0};
+
+ if (!readValue(sentence, index, 1).equals("Y")) {
+ return null;
+ }
+
+ readValue(sentence, index, 8); // command id
+ readValue(sentence, index, 6); // password
+ readValue(sentence, index, Integer.parseInt(readValue(sentence, index, 2), 16)); // unit id
+ readValue(sentence, index, 2); // frame count
+
+ readValue(sentence, index, 2); // data format
+
+ int interval = Integer.parseInt(readValue(sentence, index, 4), 16);
+
+ readValue(sentence, index, 2); // info flags
+ readValue(sentence, index, 2); // version
+
+ int count = Integer.parseInt(readValue(sentence, index, 2), 16);
+
+ Date time = null;
+ List<Position> positions = new LinkedList<>();
+
+ for (int i = 0; i < count; i++) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int speed = Integer.parseInt(readValue(sentence, index, 2), 16);
+
+ position.setValid(BitUtil.check(speed, 7));
+ position.setSpeed(BitUtil.to(speed, 7));
+
+ position.setLongitude((Integer.parseInt(readValue(sentence, index, 6), 16) - 5400000) / 30000.0);
+ position.setLatitude((Integer.parseInt(readValue(sentence, index, 6), 16) - 5400000) / 30000.0);
+
+ if (i == 0 | i == count - 1) {
+ time = new SimpleDateFormat("yyMMddHHmmss").parse(readValue(sentence, index, 12));
+ position.set(Position.KEY_STATUS, readValue(sentence, index, 8));
+ } else {
+ time = new Date(time.getTime() + interval * 1000);
+ }
+
+ position.setTime(time);
+
+ positions.add(position);
+
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TelicFrameDecoder.java b/src/main/java/org/traccar/protocol/TelicFrameDecoder.java
new file mode 100644
index 000000000..d1fef1b5b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TelicFrameDecoder.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+public class TelicFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 4) {
+ return null;
+ }
+
+ long length = buf.getUnsignedIntLE(buf.readerIndex());
+
+ if (length < 1024) {
+ if (buf.readableBytes() >= length + 4) {
+ buf.readUnsignedIntLE();
+ return buf.readRetainedSlice((int) length);
+ }
+ } else {
+ int endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) 0);
+ if (endIndex >= 0) {
+ ByteBuf frame = buf.readRetainedSlice(endIndex - buf.readerIndex());
+ buf.readByte();
+ if (frame.readableBytes() > 0) {
+ return frame;
+ }
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TelicProtocol.java b/src/main/java/org/traccar/protocol/TelicProtocol.java
new file mode 100644
index 000000000..991befa19
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TelicProtocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TelicProtocol extends BaseProtocol {
+
+ public TelicProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new TelicFrameDecoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new TelicProtocolDecoder(TelicProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TelicProtocolDecoder.java b/src/main/java/org/traccar/protocol/TelicProtocolDecoder.java
new file mode 100644
index 000000000..6d5e8f21e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TelicProtocolDecoder.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class TelicProtocolDecoder extends BaseProtocolDecoder {
+
+ public TelicProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("dddd")
+ .number("(d{6}|d{15})") // device id
+ .number("(d{1,2}),") // type
+ .number("d{12},") // event time
+ .number("d+,")
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .groupBegin()
+ .number("(ddd)(dd)(dddd),") // longitude
+ .number("(dd)(dd)(dddd),") // latitude
+ .or()
+ .number("(-?d+),") // longitude
+ .number("(-?d+),") // latitude
+ .groupEnd()
+ .number("(d),") // validity
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+)?,") // satellites
+ .expression("(?:[^,]*,){7}")
+ .number("(d+),") // battery
+ .any()
+ .compile();
+
+ private String decodeAlarm(int eventId) {
+
+ switch (eventId) {
+ case 1:
+ return Position.ALARM_POWER_ON;
+ case 2:
+ return Position.ALARM_SOS;
+ case 5:
+ return Position.ALARM_POWER_OFF;
+ case 7:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 8:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 22:
+ return Position.ALARM_LOW_BATTERY;
+ case 25:
+ return Position.ALARM_MOVEMENT;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int event = parser.nextInt(0);
+ position.set(Position.KEY_EVENT, event);
+
+ position.set(Position.KEY_ALARM, decodeAlarm(event));
+
+ if (event == 11) {
+ position.set(Position.KEY_IGNITION, true);
+ } else if (event == 12) {
+ position.set(Position.KEY_IGNITION, false);
+ }
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ if (parser.hasNext(6)) {
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_MIN_MIN));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_MIN_MIN));
+ }
+
+ if (parser.hasNext(2)) {
+ position.setLongitude(parser.nextDouble(0) / 10000);
+ position.setLatitude(parser.nextDouble(0) / 10000);
+ }
+
+ position.setValid(parser.nextInt(0) != 1);
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ }
+
+ position.set(Position.KEY_BATTERY, parser.nextInt(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TeltonikaFrameDecoder.java b/src/main/java/org/traccar/protocol/TeltonikaFrameDecoder.java
new file mode 100644
index 000000000..4d4d79d8d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TeltonikaFrameDecoder.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+public class TeltonikaFrameDecoder extends BaseFrameDecoder {
+
+ private static final int MESSAGE_MINIMUM_LENGTH = 12;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ // Check minimum length
+ if (buf.readableBytes() < MESSAGE_MINIMUM_LENGTH) {
+ return null;
+ }
+
+ // Read packet
+ int length = buf.getUnsignedShort(buf.readerIndex());
+ if (length > 0) {
+ if (buf.readableBytes() >= (length + 2)) {
+ return buf.readRetainedSlice(length + 2);
+ }
+ } else {
+ int dataLength = buf.getInt(buf.readerIndex() + 4);
+ if (buf.readableBytes() >= (dataLength + 12)) {
+ return buf.readRetainedSlice(dataLength + 12);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TeltonikaProtocol.java b/src/main/java/org/traccar/protocol/TeltonikaProtocol.java
new file mode 100644
index 000000000..eef9662d7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TeltonikaProtocol.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class TeltonikaProtocol extends BaseProtocol {
+
+ public TeltonikaProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new TeltonikaFrameDecoder());
+ pipeline.addLast(new TeltonikaProtocolEncoder());
+ pipeline.addLast(new TeltonikaProtocolDecoder(TeltonikaProtocol.this, false));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new TeltonikaProtocolEncoder());
+ pipeline.addLast(new TeltonikaProtocolDecoder(TeltonikaProtocol.this, true));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java b/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java
new file mode 100644
index 000000000..974d2c106
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java
@@ -0,0 +1,597 @@
+/*
+ * Copyright 2013 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class TeltonikaProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final int IMAGE_PACKET_MAX = 2048;
+
+ private boolean connectionless;
+ private boolean extended;
+ private Map<Long, ByteBuf> photos = new HashMap<>();
+
+ public void setExtended(boolean extended) {
+ this.extended = extended;
+ }
+
+ public TeltonikaProtocolDecoder(Protocol protocol, boolean connectionless) {
+ super(protocol);
+ this.connectionless = connectionless;
+ this.extended = Context.getConfig().getBoolean(getProtocolName() + ".extended");
+ }
+
+ private void parseIdentification(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ int length = buf.readUnsignedShort();
+ String imei = buf.toString(buf.readerIndex(), length, StandardCharsets.US_ASCII);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer(1);
+ if (deviceSession != null) {
+ response.writeByte(1);
+ } else {
+ response.writeByte(0);
+ }
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ public static final int CODEC_GH3000 = 0x07;
+ public static final int CODEC_8 = 0x08;
+ public static final int CODEC_8_EXT = 0x8E;
+ public static final int CODEC_12 = 0x0C;
+ public static final int CODEC_16 = 0x10;
+
+ private void sendImageRequest(Channel channel, SocketAddress remoteAddress, long id, int offset, int size) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeInt(0);
+ response.writeShort(0);
+ response.writeShort(19); // length
+ response.writeByte(CODEC_12);
+ response.writeByte(1); // nod
+ response.writeByte(0x0D); // camera
+ response.writeInt(11); // payload length
+ response.writeByte(2); // command
+ response.writeInt((int) id);
+ response.writeInt(offset);
+ response.writeShort(size);
+ response.writeByte(1); // nod
+ response.writeShort(0);
+ response.writeShort(Checksum.crc16(
+ Checksum.CRC16_IBM, response.nioBuffer(8, response.readableBytes() - 10)));
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ private void decodeSerial(Channel channel, SocketAddress remoteAddress, Position position, ByteBuf buf) {
+
+ getLastLocation(position, null);
+
+ int type = buf.readUnsignedByte();
+ if (type == 0x0D) {
+
+ buf.readInt(); // length
+ int subtype = buf.readUnsignedByte();
+ if (subtype == 0x01) {
+
+ long photoId = buf.readUnsignedInt();
+ ByteBuf photo = Unpooled.buffer(buf.readInt());
+ photos.put(photoId, photo);
+ sendImageRequest(
+ channel, remoteAddress, photoId,
+ 0, Math.min(IMAGE_PACKET_MAX, photo.capacity()));
+
+ } else if (subtype == 0x02) {
+
+ long photoId = buf.readUnsignedInt();
+ buf.readInt(); // offset
+ ByteBuf photo = photos.get(photoId);
+ photo.writeBytes(buf, buf.readUnsignedShort());
+ if (photo.writableBytes() > 0) {
+ sendImageRequest(
+ channel, remoteAddress, photoId,
+ photo.writerIndex(), Math.min(IMAGE_PACKET_MAX, photo.writableBytes()));
+ } else {
+ String uniqueId = Context.getIdentityManager().getById(position.getDeviceId()).getUniqueId();
+ photos.remove(photoId);
+ try {
+ position.set(Position.KEY_IMAGE, Context.getMediaManager().writeFile(uniqueId, photo, "jpg"));
+ } finally {
+ photo.release();
+ }
+ }
+
+ }
+
+ } else {
+
+ position.set(Position.KEY_TYPE, type);
+ position.set(Position.KEY_RESULT, buf.readSlice(buf.readInt()).toString(StandardCharsets.US_ASCII));
+
+ }
+ }
+
+ private long readValue(ByteBuf buf, int length, boolean signed) {
+ switch (length) {
+ case 1:
+ return signed ? buf.readByte() : buf.readUnsignedByte();
+ case 2:
+ return signed ? buf.readShort() : buf.readUnsignedShort();
+ case 4:
+ return signed ? buf.readInt() : buf.readUnsignedInt();
+ default:
+ return buf.readLong();
+ }
+ }
+
+ private void decodeOtherParameter(Position position, int id, ByteBuf buf, int length) {
+ switch (id) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ position.set("di" + id, readValue(buf, length, false));
+ break;
+ case 9:
+ position.set(Position.PREFIX_ADC + 1, readValue(buf, length, false));
+ break;
+ case 17:
+ position.set("axisX", readValue(buf, length, true));
+ break;
+ case 18:
+ position.set("axisY", readValue(buf, length, true));
+ break;
+ case 19:
+ position.set("axisZ", readValue(buf, length, true));
+ break;
+ case 21:
+ position.set(Position.KEY_RSSI, readValue(buf, length, false));
+ break;
+ case 25:
+ case 26:
+ case 27:
+ case 28:
+ position.set(Position.PREFIX_TEMP + (id - 24), readValue(buf, length, true) * 0.1);
+ break;
+ case 66:
+ position.set(Position.KEY_POWER, readValue(buf, length, false) * 0.001);
+ break;
+ case 67:
+ position.set(Position.KEY_BATTERY, readValue(buf, length, false) * 0.001);
+ break;
+ case 69:
+ position.set("gpsStatus", readValue(buf, length, false));
+ break;
+ case 72:
+ case 73:
+ case 74:
+ position.set(Position.PREFIX_TEMP + (id - 71), readValue(buf, length, true) * 0.1);
+ break;
+ case 78:
+ long driverUniqueId = readValue(buf, length, false);
+ if (driverUniqueId != 0) {
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, String.format("%016X", driverUniqueId));
+ }
+ break;
+ case 80:
+ position.set("workMode", readValue(buf, length, false));
+ break;
+ case 129:
+ case 130:
+ case 131:
+ case 132:
+ case 133:
+ case 134:
+ String driver = id == 129 || id == 132 ? "" : position.getString("driver1");
+ position.set("driver" + (id >= 132 ? 2 : 1),
+ driver + buf.readSlice(length).toString(StandardCharsets.US_ASCII).trim());
+ break;
+ case 179:
+ position.set(Position.PREFIX_OUT + 1, readValue(buf, length, false) == 1);
+ break;
+ case 180:
+ position.set(Position.PREFIX_OUT + 2, readValue(buf, length, false) == 1);
+ break;
+ case 181:
+ position.set(Position.KEY_PDOP, readValue(buf, length, false) * 0.1);
+ break;
+ case 182:
+ position.set(Position.KEY_HDOP, readValue(buf, length, false) * 0.1);
+ break;
+ case 236:
+ if (readValue(buf, length, false) == 1) {
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ }
+ break;
+ case 237:
+ position.set(Position.KEY_MOTION, readValue(buf, length, false) == 0);
+ break;
+ case 238:
+ switch ((int) readValue(buf, length, false)) {
+ case 1:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ break;
+ case 2:
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ break;
+ case 3:
+ position.set(Position.KEY_ALARM, Position.ALARM_CORNERING);
+ break;
+ default:
+ break;
+ }
+ break;
+ case 239:
+ position.set(Position.KEY_IGNITION, readValue(buf, length, false) == 1);
+ break;
+ case 240:
+ position.set(Position.KEY_MOTION, readValue(buf, length, false) == 1);
+ break;
+ case 241:
+ position.set(Position.KEY_OPERATOR, readValue(buf, length, false));
+ break;
+ default:
+ position.set(Position.PREFIX_IO + id, readValue(buf, length, false));
+ break;
+ }
+ }
+
+ private void decodeGh3000Parameter(Position position, int id, ByteBuf buf, int length) {
+ switch (id) {
+ case 1:
+ position.set(Position.KEY_BATTERY_LEVEL, readValue(buf, length, false));
+ break;
+ case 2:
+ position.set("usbConnected", readValue(buf, length, false) == 1);
+ break;
+ case 5:
+ position.set("uptime", readValue(buf, length, false));
+ break;
+ case 20:
+ position.set(Position.KEY_HDOP, readValue(buf, length, false) * 0.1);
+ break;
+ case 21:
+ position.set(Position.KEY_VDOP, readValue(buf, length, false) * 0.1);
+ break;
+ case 22:
+ position.set(Position.KEY_PDOP, readValue(buf, length, false) * 0.1);
+ break;
+ case 67:
+ position.set(Position.KEY_BATTERY, readValue(buf, length, false) * 0.001);
+ break;
+ case 221:
+ position.set("button", readValue(buf, length, false));
+ break;
+ case 222:
+ if (readValue(buf, length, false) == 1) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ }
+ break;
+ case 240:
+ position.set(Position.KEY_MOTION, readValue(buf, length, false) == 1);
+ break;
+ case 244:
+ position.set(Position.KEY_ROAMING, readValue(buf, length, false) == 1);
+ break;
+ default:
+ position.set(Position.PREFIX_IO + id, readValue(buf, length, false));
+ break;
+ }
+ }
+
+ private void decodeParameter(Position position, int id, ByteBuf buf, int length, int codec) {
+ if (codec == CODEC_GH3000) {
+ decodeGh3000Parameter(position, id, buf, length);
+ } else {
+ decodeOtherParameter(position, id, buf, length);
+ }
+ }
+
+ private void decodeNetwork(Position position) {
+ long cid = position.getLong(Position.PREFIX_IO + 205);
+ int lac = position.getInteger(Position.PREFIX_IO + 206);
+ if (cid != 0 && lac != 0) {
+ CellTower cellTower = CellTower.fromLacCid(lac, cid);
+ long operator = position.getInteger(Position.KEY_OPERATOR);
+ if (operator != 0) {
+ cellTower.setOperator(operator);
+ }
+ position.setNetwork(new Network(cellTower));
+ }
+ }
+
+ private int readExtByte(ByteBuf buf, int codec, int... codecs) {
+ boolean ext = false;
+ for (int c : codecs) {
+ if (codec == c) {
+ ext = true;
+ break;
+ }
+ }
+ if (ext) {
+ return buf.readUnsignedShort();
+ } else {
+ return buf.readUnsignedByte();
+ }
+ }
+
+ private void decodeLocation(Position position, ByteBuf buf, int codec) {
+
+ int globalMask = 0x0f;
+
+ if (codec == CODEC_GH3000) {
+
+ long time = buf.readUnsignedInt() & 0x3fffffff;
+ time += 1167609600; // 2007-01-01 00:00:00
+
+ globalMask = buf.readUnsignedByte();
+ if (BitUtil.check(globalMask, 0)) {
+
+ position.setTime(new Date(time * 1000));
+
+ int locationMask = buf.readUnsignedByte();
+
+ if (BitUtil.check(locationMask, 0)) {
+ position.setLatitude(buf.readFloat());
+ position.setLongitude(buf.readFloat());
+ }
+
+ if (BitUtil.check(locationMask, 1)) {
+ position.setAltitude(buf.readUnsignedShort());
+ }
+
+ if (BitUtil.check(locationMask, 2)) {
+ position.setCourse(buf.readUnsignedByte() * 360.0 / 256);
+ }
+
+ if (BitUtil.check(locationMask, 3)) {
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ }
+
+ if (BitUtil.check(locationMask, 4)) {
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ }
+
+ if (BitUtil.check(locationMask, 5)) {
+ CellTower cellTower = CellTower.fromLacCid(buf.readUnsignedShort(), buf.readUnsignedShort());
+
+ if (BitUtil.check(locationMask, 6)) {
+ cellTower.setSignalStrength((int) buf.readUnsignedByte());
+ }
+
+ if (BitUtil.check(locationMask, 7)) {
+ cellTower.setOperator(buf.readUnsignedInt());
+ }
+
+ position.setNetwork(new Network(cellTower));
+
+ } else {
+ if (BitUtil.check(locationMask, 6)) {
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ }
+ if (BitUtil.check(locationMask, 7)) {
+ position.set(Position.KEY_OPERATOR, buf.readUnsignedInt());
+ }
+ }
+
+ } else {
+
+ getLastLocation(position, new Date(time * 1000));
+
+ }
+
+ } else {
+
+ position.setTime(new Date(buf.readLong()));
+
+ position.set("priority", buf.readUnsignedByte());
+
+ position.setLongitude(buf.readInt() / 10000000.0);
+ position.setLatitude(buf.readInt() / 10000000.0);
+ position.setAltitude(buf.readShort());
+ position.setCourse(buf.readUnsignedShort());
+
+ int satellites = buf.readUnsignedByte();
+ position.set(Position.KEY_SATELLITES, satellites);
+
+ position.setValid(satellites != 0);
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+
+ position.set(Position.KEY_EVENT, readExtByte(buf, codec, CODEC_8_EXT, CODEC_16));
+ if (codec == CODEC_16) {
+ buf.readUnsignedByte(); // generation type
+ }
+
+ readExtByte(buf, codec, CODEC_8_EXT); // total IO data records
+
+ }
+
+ // Read 1 byte data
+ if (BitUtil.check(globalMask, 1)) {
+ int cnt = readExtByte(buf, codec, CODEC_8_EXT);
+ for (int j = 0; j < cnt; j++) {
+ decodeParameter(position, readExtByte(buf, codec, CODEC_8_EXT, CODEC_16), buf, 1, codec);
+ }
+ }
+
+ // Read 2 byte data
+ if (BitUtil.check(globalMask, 2)) {
+ int cnt = readExtByte(buf, codec, CODEC_8_EXT);
+ for (int j = 0; j < cnt; j++) {
+ decodeParameter(position, readExtByte(buf, codec, CODEC_8_EXT, CODEC_16), buf, 2, codec);
+ }
+ }
+
+ // Read 4 byte data
+ if (BitUtil.check(globalMask, 3)) {
+ int cnt = readExtByte(buf, codec, CODEC_8_EXT);
+ for (int j = 0; j < cnt; j++) {
+ decodeParameter(position, readExtByte(buf, codec, CODEC_8_EXT, CODEC_16), buf, 4, codec);
+ }
+ }
+
+ // Read 8 byte data
+ if (codec == CODEC_8 || codec == CODEC_8_EXT || codec == CODEC_16) {
+ int cnt = readExtByte(buf, codec, CODEC_8_EXT);
+ for (int j = 0; j < cnt; j++) {
+ decodeOtherParameter(position, readExtByte(buf, codec, CODEC_8_EXT, CODEC_16), buf, 8);
+ }
+ }
+
+ // Read 16 byte data
+ if (extended) {
+ int cnt = readExtByte(buf, codec, CODEC_8_EXT);
+ for (int j = 0; j < cnt; j++) {
+ int id = readExtByte(buf, codec, CODEC_8_EXT, CODEC_16);
+ position.set(Position.PREFIX_IO + id, ByteBufUtil.hexDump(buf.readSlice(16)));
+ }
+ }
+
+ // Read X byte data
+ if (codec == CODEC_8_EXT) {
+ int cnt = buf.readUnsignedShort();
+ for (int j = 0; j < cnt; j++) {
+ int id = buf.readUnsignedShort();
+ int length = buf.readUnsignedShort();
+ if (id == 256) {
+ position.set(Position.KEY_VIN, buf.readSlice(length).toString(StandardCharsets.US_ASCII));
+ } else {
+ position.set(Position.PREFIX_IO + id, ByteBufUtil.hexDump(buf.readSlice(length)));
+ }
+ }
+ }
+
+ decodeNetwork(position);
+
+ }
+
+ private List<Position> parseData(
+ Channel channel, SocketAddress remoteAddress, ByteBuf buf, int locationPacketId, String... imei) {
+ List<Position> positions = new LinkedList<>();
+
+ if (!connectionless) {
+ buf.readUnsignedInt(); // data length
+ }
+
+ int codec = buf.readUnsignedByte();
+ int count = buf.readUnsignedByte();
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+
+ if (deviceSession == null) {
+ return null;
+ }
+
+ for (int i = 0; i < count; i++) {
+ Position position = new Position(getProtocolName());
+
+ position.setDeviceId(deviceSession.getDeviceId());
+ position.setValid(true);
+
+ if (codec == CODEC_12) {
+ decodeSerial(channel, remoteAddress, position, buf);
+ } else {
+ decodeLocation(position, buf, codec);
+ }
+
+ if (!position.getOutdated() || !position.getAttributes().isEmpty()) {
+ positions.add(position);
+ }
+ }
+
+ if (channel != null) {
+ if (connectionless) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeShort(5);
+ response.writeShort(0);
+ response.writeByte(0x01);
+ response.writeByte(locationPacketId);
+ response.writeByte(count);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ } else {
+ ByteBuf response = Unpooled.buffer();
+ response.writeInt(count);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ return positions.isEmpty() ? null : positions;
+ }
+
+ @Override
+ protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (connectionless) {
+ return decodeUdp(channel, remoteAddress, buf);
+ } else {
+ return decodeTcp(channel, remoteAddress, buf);
+ }
+ }
+
+ private Object decodeTcp(Channel channel, SocketAddress remoteAddress, ByteBuf buf) throws Exception {
+
+ if (buf.getUnsignedShort(0) > 0) {
+ parseIdentification(channel, remoteAddress, buf);
+ } else {
+ buf.skipBytes(4);
+ return parseData(channel, remoteAddress, buf, 0);
+ }
+
+ return null;
+ }
+
+ private Object decodeUdp(Channel channel, SocketAddress remoteAddress, ByteBuf buf) throws Exception {
+
+ buf.readUnsignedShort(); // length
+ buf.readUnsignedShort(); // packet id
+ buf.readUnsignedByte(); // packet type
+ int locationPacketId = buf.readUnsignedByte();
+ String imei = buf.readSlice(buf.readUnsignedShort()).toString(StandardCharsets.US_ASCII);
+
+ return parseData(channel, remoteAddress, buf, locationPacketId, imei);
+
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TeltonikaProtocolEncoder.java b/src/main/java/org/traccar/protocol/TeltonikaProtocolEncoder.java
new file mode 100644
index 000000000..944cec024
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TeltonikaProtocolEncoder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.helper.Checksum;
+import org.traccar.model.Command;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+import java.nio.charset.StandardCharsets;
+
+public class TeltonikaProtocolEncoder extends BaseProtocolEncoder {
+
+ private ByteBuf encodeContent(String content) {
+
+ ByteBuf buf = Unpooled.buffer();
+
+ buf.writeInt(0);
+ buf.writeInt(content.length() + 10);
+ buf.writeByte(TeltonikaProtocolDecoder.CODEC_12);
+ buf.writeByte(1); // quantity
+ buf.writeByte(5); // type
+ buf.writeInt(content.length() + 2);
+ buf.writeBytes(content.getBytes(StandardCharsets.US_ASCII));
+ buf.writeByte('\r');
+ buf.writeByte('\n');
+ buf.writeByte(1); // quantity
+ buf.writeInt(Checksum.crc16(Checksum.CRC16_IBM, buf.nioBuffer(8, buf.writerIndex() - 8)));
+
+ return buf;
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return encodeContent(command.getString(Command.KEY_DATA));
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ThinkRaceProtocol.java b/src/main/java/org/traccar/protocol/ThinkRaceProtocol.java
new file mode 100644
index 000000000..ca1237cef
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ThinkRaceProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class ThinkRaceProtocol extends BaseProtocol {
+
+ public ThinkRaceProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 2 + 12 + 1 + 1, 2, 2, 0));
+ pipeline.addLast(new ThinkRaceProtocolDecoder(ThinkRaceProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/ThinkRaceProtocolDecoder.java b/src/main/java/org/traccar/protocol/ThinkRaceProtocolDecoder.java
new file mode 100644
index 000000000..0928b25e0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/ThinkRaceProtocolDecoder.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class ThinkRaceProtocolDecoder extends BaseProtocolDecoder {
+
+ public ThinkRaceProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_LOGIN = 0x80;
+ public static final int MSG_GPS = 0x90;
+
+ private static double convertCoordinate(long raw, boolean negative) {
+ long degrees = raw / 1000000;
+ double minutes = (raw % 1000000) * 0.0001;
+ double result = degrees + minutes / 60;
+ if (negative) {
+ result = -result;
+ }
+ return result;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ ByteBuf id = buf.readSlice(12);
+ buf.readUnsignedByte(); // separator
+ int type = buf.readUnsignedByte();
+ buf.readUnsignedShort(); // length
+
+ if (type == MSG_LOGIN) {
+
+ int command = buf.readUnsignedByte(); // 0x00 - heartbeat
+
+ if (command == 0x01) {
+ String imei = buf.toString(buf.readerIndex(), 15, StandardCharsets.US_ASCII);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession != null && channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(0x48); response.writeByte(0x52); // header
+ response.writeBytes(id);
+ response.writeByte(0x2c); // separator
+ response.writeByte(type);
+ response.writeShort(0x0002); // length
+ response.writeShort(0x8000);
+ response.writeShort(0x0000); // checksum
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ } else if (type == MSG_GPS) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+
+ int flags = buf.readUnsignedByte();
+
+ position.setValid(true);
+ position.setLatitude(convertCoordinate(buf.readUnsignedInt(), !BitUtil.check(flags, 0)));
+ position.setLongitude(convertCoordinate(buf.readUnsignedInt(), !BitUtil.check(flags, 1)));
+
+ position.setSpeed(buf.readUnsignedByte());
+ position.setCourse(buf.readUnsignedByte());
+
+ position.setNetwork(new Network(
+ CellTower.fromLacCid(buf.readUnsignedShort(), buf.readUnsignedShort())));
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tk102Protocol.java b/src/main/java/org/traccar/protocol/Tk102Protocol.java
new file mode 100644
index 000000000..9f2463cd6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tk102Protocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Tk102Protocol extends BaseProtocol {
+
+ public Tk102Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 1 + 1 + 10, 1, 1, 0));
+ pipeline.addLast(new Tk102ProtocolDecoder(Tk102Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tk102ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Tk102ProtocolDecoder.java
new file mode 100644
index 000000000..da0c6928b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tk102ProtocolDecoder.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+public class Tk102ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Tk102ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_LOGIN_REQUEST = 0x80;
+ public static final int MSG_LOGIN_REQUEST_2 = 0x21;
+ public static final int MSG_LOGIN_RESPONSE = 0x00;
+ public static final int MSG_HEARTBEAT_REQUEST = 0xF0;
+ public static final int MSG_HEARTBEAT_RESPONSE = 0xFF;
+ public static final int MSG_REPORT_ONCE = 0x90;
+ public static final int MSG_REPORT_INTERVAL = 0x93;
+
+ public static final int MODE_GPRS = 0x30;
+ public static final int MODE_GPRS_SMS = 0x33;
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("(")
+ .expression("[A-Z]+")
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .expression("([AV])") // validity
+ .number("(dd)(dd.dddd)([NS])") // latitude
+ .number("(ddd)(dd.dddd)([EW])") // longitude
+ .number("(ddd.ddd)") // speed
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .text(")")
+ .compile();
+
+ private void sendResponse(Channel channel, int type, ByteBuf dataSequence, ByteBuf content) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte('[');
+ response.writeByte(type);
+ response.writeBytes(dataSequence);
+ response.writeByte(content.readableBytes());
+ response.writeBytes(content);
+ content.release();
+ response.writeByte(']');
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(1); // header
+ int type = buf.readUnsignedByte();
+ ByteBuf dataSequence = buf.readSlice(10);
+ int length = buf.readUnsignedByte();
+
+ if (type == MSG_LOGIN_REQUEST || type == MSG_LOGIN_REQUEST_2) {
+
+ ByteBuf data = buf.readSlice(length);
+
+ String id;
+ if (type == MSG_LOGIN_REQUEST) {
+ id = data.toString(StandardCharsets.US_ASCII);
+ } else {
+ id = data.copy(1, 15).toString(StandardCharsets.US_ASCII);
+ }
+
+ if (getDeviceSession(channel, remoteAddress, id) != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(MODE_GPRS);
+ response.writeBytes(data);
+ sendResponse(channel, MSG_LOGIN_RESPONSE, dataSequence, response);
+ }
+
+ } else if (type == MSG_HEARTBEAT_REQUEST) {
+
+ sendResponse(channel, MSG_HEARTBEAT_RESPONSE, dataSequence, buf.readRetainedSlice(length));
+
+ } else {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, buf.readSlice(length).toString(StandardCharsets.US_ASCII));
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tk103FrameDecoder.java b/src/main/java/org/traccar/protocol/Tk103FrameDecoder.java
new file mode 100644
index 000000000..b61a42563
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tk103FrameDecoder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 Valerii Vyshniak (val@val.one)
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class Tk103FrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 2) {
+ return null;
+ }
+
+ int frameStartIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '(');
+ if (frameStartIndex == -1) {
+ buf.clear();
+ return null;
+ }
+
+ int frameEndIndex, freeTextSymbolCounter;
+ for (frameEndIndex = frameStartIndex, freeTextSymbolCounter = 0;; frameEndIndex++) {
+ int freeTextIndex = frameEndIndex;
+ frameEndIndex = buf.indexOf(frameEndIndex, buf.writerIndex(), (byte) ')');
+ if (frameEndIndex == -1) {
+ break;
+ }
+ for (;; freeTextIndex++, freeTextSymbolCounter++) {
+ freeTextIndex = buf.indexOf(freeTextIndex, frameEndIndex, (byte) '$');
+ if (freeTextIndex == -1 || freeTextIndex >= frameEndIndex) {
+ break;
+ }
+ }
+ if (freeTextSymbolCounter % 2 == 0) {
+ break;
+ }
+ }
+
+ if (frameEndIndex == -1) {
+ while (buf.readableBytes() > 1024) {
+ int discardUntilIndex = buf.indexOf(buf.readerIndex() + 1, buf.writerIndex(), (byte) '(');
+ if (discardUntilIndex == -1) {
+ buf.clear();
+ } else {
+ buf.readerIndex(discardUntilIndex);
+ }
+ }
+ return null;
+ }
+
+ buf.readerIndex(frameStartIndex);
+
+ return buf.readRetainedSlice(frameEndIndex + 1 - frameStartIndex);
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tk103Protocol.java b/src/main/java/org/traccar/protocol/Tk103Protocol.java
new file mode 100644
index 000000000..fa83133e2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tk103Protocol.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Christoph Krey (c@ckrey.de)
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class Tk103Protocol extends BaseProtocol {
+
+ public Tk103Protocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_GET_DEVICE_STATUS,
+ Command.TYPE_IDENTIFICATION,
+ Command.TYPE_MODE_DEEP_SLEEP,
+ Command.TYPE_MODE_POWER_SAVING,
+ Command.TYPE_ALARM_SOS,
+ Command.TYPE_SET_CONNECTION,
+ Command.TYPE_SOS_NUMBER,
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_POSITION_PERIODIC,
+ Command.TYPE_POSITION_STOP,
+ Command.TYPE_GET_VERSION,
+ Command.TYPE_POWER_OFF,
+ Command.TYPE_REBOOT_DEVICE,
+ Command.TYPE_SET_ODOMETER,
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_OUTPUT_CONTROL);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Tk103FrameDecoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Tk103ProtocolEncoder());
+ pipeline.addLast(new Tk103ProtocolDecoder(Tk103Protocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Tk103ProtocolEncoder());
+ pipeline.addLast(new Tk103ProtocolDecoder(Tk103Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tk103ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Tk103ProtocolDecoder.java
new file mode 100644
index 000000000..9e28b5051
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tk103ProtocolDecoder.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+import org.traccar.model.WifiAccessPoint;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Tk103ProtocolDecoder extends BaseProtocolDecoder {
+
+ private boolean decodeLow;
+
+ public Tk103ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ decodeLow = Context.getConfig().getBoolean(getProtocolName() + ".decodeLow");
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("(").optional()
+ .number("(d+)(,)?") // device id
+ .expression("(.{4}),?") // command
+ .number("(d*)")
+ .number("(dd)(dd)(dd),?") // date (mmddyy if comma-delimited, otherwise yyddmm)
+ .expression("([AV]),?") // validity
+ .number("(d+)(dd.d+)") // latitude
+ .expression("([NS]),?")
+ .number("(d+)(dd.d+)") // longitude
+ .expression("([EW]),?")
+ .number("(d+.d)(?:d*,)?") // speed
+ .number("(dd)(dd)(dd),?") // time (hhmmss)
+ .groupBegin()
+ .number("(?:([d.]{6})|(dd)),?") // course
+ .number("([01])") // charge
+ .number("([01])") // ignition
+ .number("(x)") // io
+ .number("(x)") // io
+ .number("(x)") // io
+ .number("(xxx)") // fuel
+ .number("L(x+)") // odometer
+ .or()
+ .number("(d+.d+)") // course
+ .groupEnd()
+ .any()
+ .number("([+-]ddd.d)?") // temperature
+ .text(")").optional()
+ .compile();
+
+ private static final Pattern PATTERN_BATTERY = new PatternBuilder()
+ .text("(").optional()
+ .number("(d+),") // device id
+ .text("ZC20,")
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d+),") // battery level
+ .number("(d+),") // battery voltage
+ .number("(d+),") // power voltage
+ .number("d+") // installed
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_NETWORK = new PatternBuilder()
+ .text("(").optional()
+ .number("(d{12})") // device id
+ .text("BZ00,")
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(x+),") // lac
+ .number("(x+),") // cid
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_LBSWIFI = new PatternBuilder()
+ .text("(").optional()
+ .number("(d+),") // device id
+ .expression("(.{4}),") // command
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(d+),") // lac
+ .number("(d+),") // cid
+ .number("(d+),") // number of wifi macs
+ .number("((?:(?:xx:){5}(?:xx)\\*[-+]?d+\\*d+,)*)")
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_COMMAND_RESULT = new PatternBuilder()
+ .text("(").optional()
+ .number("(d+),") // device id
+ .expression(".{4},") // command
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("\\$([\\s\\S]*?)(?:\\$|$)") // message
+ .any()
+ .compile();
+
+ private String decodeAlarm(int value) {
+ switch (value) {
+ case 1:
+ return Position.ALARM_ACCIDENT;
+ case 2:
+ return Position.ALARM_SOS;
+ case 3:
+ return Position.ALARM_VIBRATION;
+ case 4:
+ return Position.ALARM_LOW_SPEED;
+ case 5:
+ return Position.ALARM_OVERSPEED;
+ case 6:
+ return Position.ALARM_GEOFENCE_EXIT;
+ default:
+ return null;
+ }
+ }
+
+ private void decodeType(Position position, String type, String data) {
+ switch (type) {
+ case "BO01":
+ position.set(Position.KEY_ALARM, decodeAlarm(data.charAt(0) - '0'));
+ break;
+ case "ZC11":
+ case "DW31":
+ case "DW51":
+ position.set(Position.KEY_ALARM, Position.ALARM_MOVEMENT);
+ break;
+ case "ZC12":
+ case "DW32":
+ case "DW52":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ case "ZC13":
+ case "DW33":
+ case "DW53":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_CUT);
+ break;
+ case "ZC15":
+ case "DW35":
+ case "DW55":
+ position.set(Position.KEY_IGNITION, true);
+ break;
+ case "ZC16":
+ case "DW36":
+ case "DW56":
+ position.set(Position.KEY_IGNITION, false);
+ break;
+ case "ZC29":
+ case "DW42":
+ case "DW62":
+ position.set(Position.KEY_IGNITION, true);
+ break;
+ case "ZC17":
+ case "DW37":
+ case "DW57":
+ position.set(Position.KEY_ALARM, Position.ALARM_REMOVING);
+ break;
+ case "ZC25":
+ case "DW3E":
+ case "DW5E":
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ break;
+ case "ZC26":
+ case "DW3F":
+ case "DW5F":
+ position.set(Position.KEY_ALARM, Position.ALARM_TAMPERING);
+ break;
+ case "ZC27":
+ case "DW40":
+ case "DW60":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_POWER);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private Integer decodeBattery(int value) {
+ switch (value) {
+ case 6:
+ return 100;
+ case 5:
+ return 80;
+ case 4:
+ return 50;
+ case 3:
+ return 20;
+ case 2:
+ return 10;
+ default:
+ return null;
+ }
+ }
+
+ private Position decodeBattery(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_BATTERY, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ int batterylevel = parser.nextInt(0);
+ if (batterylevel != 255) {
+ position.set(Position.KEY_BATTERY_LEVEL, decodeBattery(batterylevel));
+ }
+
+ int battery = parser.nextInt(0);
+ if (battery != 65535) {
+ position.set(Position.KEY_BATTERY, battery * 0.01);
+ }
+
+ int power = parser.nextInt(0);
+ if (power != 65535) {
+ position.set(Position.KEY_POWER, power * 0.1);
+ }
+
+ return position;
+ }
+
+ private Position decodeNetwork(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_NETWORK, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(0), parser.nextInt(0), parser.nextHexInt(0), parser.nextHexInt(0))));
+
+ return position;
+ }
+
+ private Position decodeLbsWifi(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_LBSWIFI, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ decodeType(position, parser.next(), "0");
+
+ getLastLocation(position, null);
+
+ Network network = new Network();
+
+ network.addCellTower(CellTower.from(
+ parser.nextInt(), parser.nextInt(), parser.nextInt(), parser.nextInt()));
+
+ int wifiCount = parser.nextInt();
+ if (parser.hasNext()) {
+ String[] wifimacs = parser.next().split(",");
+ if (wifimacs.length == wifiCount) {
+ for (int i = 0; i < wifiCount; i++) {
+ String[] wifiinfo = wifimacs[i].split("\\*");
+ network.addWifiAccessPoint(WifiAccessPoint.from(
+ wifiinfo[0], Integer.parseInt(wifiinfo[1]), Integer.parseInt(wifiinfo[2])));
+ }
+ }
+ }
+
+ if (network.getCellTowers() != null || network.getWifiAccessPoints() != null) {
+ position.setNetwork(network);
+ }
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ return position;
+ }
+
+ private Position decodeCommandResult(Channel channel, SocketAddress remoteAddress, String sentence) {
+ Parser parser = new Parser(PATTERN_COMMAND_RESULT, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.set(Position.KEY_RESULT, parser.next());
+
+ return position;
+
+ }
+
+@Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (channel != null) {
+ String id = sentence.substring(1, 13);
+ String type = sentence.substring(13, 17);
+ if (type.equals("BP00")) {
+ channel.writeAndFlush(new NetworkMessage("(" + id + "AP01HSO)", remoteAddress));
+ return null;
+ } else if (type.equals("BP05")) {
+ channel.writeAndFlush(new NetworkMessage("(" + id + "AP05)", remoteAddress));
+ }
+ }
+
+ if (sentence.contains("ZC20")) {
+ return decodeBattery(channel, remoteAddress, sentence);
+ } else if (sentence.contains("BZ00")) {
+ return decodeNetwork(channel, remoteAddress, sentence);
+ } else if (sentence.contains("ZC03")) {
+ return decodeCommandResult(channel, remoteAddress, sentence);
+ } else if (sentence.contains("DW5")) {
+ return decodeLbsWifi(channel, remoteAddress, sentence);
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ boolean alternative = parser.next() != null;
+
+ decodeType(position, parser.next(), parser.next());
+
+ DateBuilder dateBuilder = new DateBuilder();
+ if (alternative) {
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ } else {
+ dateBuilder.setDate(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ }
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ position.setSpeed(convertSpeed(parser.nextDouble(0), "kmh"));
+
+ dateBuilder.setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ if (parser.hasNext()) {
+ position.setCourse(parser.nextDouble());
+ }
+ if (parser.hasNext()) {
+ position.setCourse(parser.nextDouble());
+ }
+
+ if (parser.hasNext(7)) {
+ position.set(Position.KEY_CHARGE, parser.nextInt() == 0);
+ position.set(Position.KEY_IGNITION, parser.nextInt() == 1);
+
+ int mask1 = parser.nextHexInt();
+ position.set(Position.PREFIX_IN + 2, BitUtil.check(mask1, 0) ? 1 : 0);
+ position.set("panic", BitUtil.check(mask1, 1) ? 1 : 0);
+ position.set(Position.PREFIX_OUT + 2, BitUtil.check(mask1, 2) ? 1 : 0);
+ if (decodeLow || BitUtil.check(mask1, 3)) {
+ position.set(Position.KEY_BLOCKED, BitUtil.check(mask1, 3) ? 1 : 0);
+ }
+
+ int mask2 = parser.nextHexInt();
+ for (int i = 0; i < 3; i++) {
+ if (decodeLow || BitUtil.check(mask2, i)) {
+ position.set("hs" + (3 - i), BitUtil.check(mask2, i) ? 1 : 0);
+ }
+ }
+ if (decodeLow || BitUtil.check(mask2, 3)) {
+ position.set(Position.KEY_DOOR, BitUtil.check(mask2, 3) ? 1 : 0);
+ }
+
+ int mask3 = parser.nextHexInt();
+ for (int i = 1; i <= 3; i++) {
+ if (decodeLow || BitUtil.check(mask3, i)) {
+ position.set("ls" + (3 - i + 1), BitUtil.check(mask3, i) ? 1 : 0);
+ }
+ }
+
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextHexInt());
+ position.set(Position.KEY_ODOMETER, parser.nextLong(16, 0));
+ }
+
+ if (parser.hasNext()) {
+ position.setCourse(parser.nextDouble());
+ }
+
+ if (parser.hasNext()) {
+ position.set(Position.PREFIX_TEMP + 1, parser.nextDouble(0));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tk103ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Tk103ProtocolEncoder.java
new file mode 100644
index 000000000..98edc8cb5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tk103ProtocolEncoder.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 Christoph Krey (c@ckrey.de)
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.Context;
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class Tk103ProtocolEncoder extends StringProtocolEncoder {
+
+ private final boolean forceAlternative;
+
+ public Tk103ProtocolEncoder() {
+ this.forceAlternative = false;
+ }
+
+ public Tk103ProtocolEncoder(boolean forceAlternative) {
+ this.forceAlternative = forceAlternative;
+ }
+
+ private String formatAlt(Command command, String format, String... keys) {
+ return formatCommand(command, "[begin]sms2," + format + ",[end]", keys);
+ }
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ boolean alternative = forceAlternative || Context.getIdentityManager().lookupAttributeBoolean(
+ command.getDeviceId(), "tk103.alternative", false, true);
+
+ initDevicePassword(command, "123456");
+
+ if (alternative) {
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatAlt(command, "{%s}", Command.KEY_DATA);
+ case Command.TYPE_GET_VERSION:
+ return formatAlt(command, "*about*");
+ case Command.TYPE_POWER_OFF:
+ return formatAlt(command, "*turnoff*");
+ case Command.TYPE_REBOOT_DEVICE:
+ return formatAlt(command, "88888888");
+ case Command.TYPE_POSITION_SINGLE:
+ return formatAlt(command, "*getposl*");
+ case Command.TYPE_POSITION_PERIODIC:
+ return formatAlt(command, "*routetrack*99*");
+ case Command.TYPE_POSITION_STOP:
+ return formatAlt(command, "*routetrackoff*");
+ case Command.TYPE_GET_DEVICE_STATUS:
+ return formatAlt(command, "*status*");
+ case Command.TYPE_IDENTIFICATION:
+ return formatAlt(command, "999999");
+ case Command.TYPE_MODE_DEEP_SLEEP:
+ return formatAlt(command, command.getBoolean(Command.KEY_ENABLE) ? "*sleep*2*" : "*sleepoff*");
+ case Command.TYPE_MODE_POWER_SAVING:
+ return formatAlt(command, command.getBoolean(Command.KEY_ENABLE) ? "*sleepv*" : "*sleepoff*");
+ case Command.TYPE_ALARM_SOS:
+ return formatAlt(command, command.getBoolean(Command.KEY_ENABLE) ? "*soson*" : "*sosoff*");
+ case Command.TYPE_SET_CONNECTION:
+ return formatAlt(command, "*setip*%s*{%s}*",
+ command.getString(Command.KEY_SERVER).replace(".", "*"), Command.KEY_PORT);
+ case Command.TYPE_SOS_NUMBER:
+ return formatAlt(command, "*master*{%s}*{%s}*", Command.KEY_DEVICE_PASSWORD, Command.KEY_PHONE);
+ default:
+ return null;
+ }
+ } else {
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatCommand(command, "({%s}{%s})", Command.KEY_UNIQUE_ID, Command.KEY_DATA);
+ case Command.TYPE_GET_VERSION:
+ return formatCommand(command, "({%s}AP07)", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_REBOOT_DEVICE:
+ return formatCommand(command, "({%s}AT00)", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_SET_ODOMETER:
+ return formatCommand(command, "({%s}AX01)", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_POSITION_SINGLE:
+ return formatCommand(command, "({%s}AP00)", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_POSITION_PERIODIC:
+ return formatCommand(command, "({%s}AR00%s0000)", Command.KEY_UNIQUE_ID,
+ String.format("%04X", command.getInteger(Command.KEY_FREQUENCY)));
+ case Command.TYPE_POSITION_STOP:
+ return formatCommand(command, "({%s}AR0000000000)", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, "({%s}AV010)", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, "({%s}AV011)", Command.KEY_UNIQUE_ID);
+ case Command.TYPE_OUTPUT_CONTROL:
+ return formatCommand(command, "({%s}AV00{%s})", Command.KEY_UNIQUE_ID, Command.KEY_DATA);
+ default:
+ return null;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tlt2hProtocol.java b/src/main/java/org/traccar/protocol/Tlt2hProtocol.java
new file mode 100644
index 000000000..12fd92afa
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tlt2hProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Tlt2hProtocol extends BaseProtocol {
+
+ public Tlt2hProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(32 * 1024, "##\r\n"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Tlt2hProtocolDecoder(Tlt2hProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tlt2hProtocolDecoder.java b/src/main/java/org/traccar/protocol/Tlt2hProtocolDecoder.java
new file mode 100644
index 000000000..f67ff88db
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tlt2hProtocolDecoder.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class Tlt2hProtocolDecoder extends BaseProtocolDecoder {
+
+ public Tlt2hProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_HEADER = new PatternBuilder()
+ .number("#(d+)#") // imei
+ .any()
+ .expression("([^#]+)#") // status
+ .number("d+") // number of records
+ .compile();
+
+ private static final Pattern PATTERN_POSITION = new PatternBuilder()
+ .number("#(x+)?") // cell info
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).d+,") // time (hhmmss.sss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d+)(dd.d+),") // longitude
+ .number("([EW]),")
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .any()
+ .compile();
+
+ private void decodeStatus(Position position, String status) {
+ switch (status) {
+ case "AUTOSTART":
+ case "AUTO":
+ position.set(Position.KEY_IGNITION, true);
+ break;
+ case "AUTOSTOP":
+ case "AUTOLOW":
+ position.set(Position.KEY_IGNITION, false);
+ break;
+ case "TOWED":
+ position.set(Position.KEY_ALARM, Position.ALARM_TOW);
+ break;
+ case "SOS":
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ break;
+ case "DEF":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_CUT);
+ break;
+ case "BLP":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ case "CLP":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_POWER);
+ break;
+ case "OS":
+ position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE_EXIT);
+ break;
+ case "RS":
+ position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE_ENTER);
+ break;
+ case "OVERSPEED":
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ sentence = sentence.trim();
+
+ String header = sentence.substring(0, sentence.indexOf('\r'));
+ Parser parser = new Parser(PATTERN_HEADER, header);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ String status = parser.next();
+
+ String[] messages = sentence.substring(sentence.indexOf('\n') + 1).split("\r\n");
+ List<Position> positions = new LinkedList<>();
+
+ for (String message : messages) {
+ parser = new Parser(PATTERN_POSITION, message);
+ if (parser.matches()) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ parser.next(); // base station info
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ decodeStatus(position, status);
+
+ positions.add(position);
+ }
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TlvProtocol.java b/src/main/java/org/traccar/protocol/TlvProtocol.java
new file mode 100644
index 000000000..94f5da94f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TlvProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TlvProtocol extends BaseProtocol {
+
+ public TlvProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '\0'));
+ pipeline.addLast(new TlvProtocolDecoder(TlvProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TlvProtocolDecoder.java b/src/main/java/org/traccar/protocol/TlvProtocolDecoder.java
new file mode 100644
index 000000000..36cf7859f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TlvProtocolDecoder.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class TlvProtocolDecoder extends BaseProtocolDecoder {
+
+ public TlvProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress, String type, String... arguments) {
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeCharSequence(type, StandardCharsets.US_ASCII);
+ for (String argument : arguments) {
+ response.writeByte(argument.length());
+ response.writeCharSequence(argument, StandardCharsets.US_ASCII);
+ }
+ response.writeByte(0);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+ }
+
+ private String readArgument(ByteBuf buf) {
+ return buf.readSlice(buf.readUnsignedByte()).toString(StandardCharsets.US_ASCII);
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ String type = buf.readSlice(2).toString(StandardCharsets.US_ASCII);
+
+ if (channel != null) {
+ switch (type) {
+ case "0A":
+ case "0C":
+ sendResponse(channel, remoteAddress, type);
+ break;
+ case "0B":
+ sendResponse(channel, remoteAddress, type, "1482202689", "10", "20", "15");
+ break;
+ case "0E":
+ case "0F":
+ sendResponse(channel, remoteAddress, type, "30", "Unknown");
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (type.equals("0E")) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, readArgument(buf));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(true);
+ position.setTime(new Date(Long.parseLong(readArgument(buf)) * 1000));
+
+ readArgument(buf); // location identifier
+
+ position.setLongitude(Double.parseDouble(readArgument(buf)));
+ position.setLatitude(Double.parseDouble(readArgument(buf)));
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(readArgument(buf))));
+ position.setCourse(Double.parseDouble(readArgument(buf)));
+
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(readArgument(buf)));
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TmgFrameDecoder.java b/src/main/java/org/traccar/protocol/TmgFrameDecoder.java
new file mode 100644
index 000000000..205adaa51
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TmgFrameDecoder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+public class TmgFrameDecoder extends BaseFrameDecoder {
+
+ private boolean isLetter(byte c) {
+ return c >= 'a' && c <= 'z';
+ }
+
+ private int findHeader(ByteBuf buffer) {
+ int guessedIndex = buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte) '$');
+ while (guessedIndex != -1 && buffer.writerIndex() - guessedIndex >= 5) {
+ if (buffer.getByte(guessedIndex + 4) == ','
+ && isLetter(buffer.getByte(guessedIndex + 1))
+ && isLetter(buffer.getByte(guessedIndex + 2))
+ && isLetter(buffer.getByte(guessedIndex + 3))) {
+ return guessedIndex;
+ }
+ guessedIndex = buffer.indexOf(guessedIndex, buffer.writerIndex(), (byte) '$');
+ }
+ return -1;
+ }
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int beginIndex = findHeader(buf);
+
+ if (beginIndex >= 0) {
+
+ buf.readerIndex(beginIndex);
+
+ int endIndex = buf.indexOf(beginIndex, buf.writerIndex(), (byte) '\n');
+
+ if (endIndex >= 0) {
+ ByteBuf frame = buf.readRetainedSlice(endIndex - beginIndex);
+ buf.readByte(); // delimiter
+ return frame;
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TmgProtocol.java b/src/main/java/org/traccar/protocol/TmgProtocol.java
new file mode 100644
index 000000000..020332ce7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TmgProtocol.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TmgProtocol extends BaseProtocol {
+
+ public TmgProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new TmgFrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new TmgProtocolDecoder(TmgProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TmgProtocolDecoder.java b/src/main/java/org/traccar/protocol/TmgProtocolDecoder.java
new file mode 100644
index 000000000..d27849f8c
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TmgProtocolDecoder.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class TmgProtocolDecoder extends BaseProtocolDecoder {
+
+ public TmgProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$")
+ .expression("(...),") // type
+ .expression("[LH],").optional() // history
+ .number("(d+),") // imei
+ .number("(dd)(dd)(dddd),") // date (ddmmyyyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(d),") // status
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .groupBegin()
+ .number("(-?d+.?d*),") // altitude
+ .number("(d+.d+),") // hdop
+ .number("(d+),") // satellites
+ .number("(d+),") // visible satellites
+ .number("([^,]*),") // operator
+ .number("(d+),") // rssi
+ .number("[^,]*,") // cid
+ .expression("([01]),") // ignition
+ .number("(d+.?d*),") // battery
+ .number("(d+.?d*),") // power
+ .expression("([01]+),") // input
+ .expression("([01]+),") // output
+ .expression("[01]+,") // temper status
+ .number("(d+.?d*)[^,]*,") // adc1
+ .number("(d+.?d*)[^,]*,") // adc2
+ .number("d+.?d*,") // trip meter
+ .expression("([^,]*),") // software version
+ .expression("([^,]*),").optional() // rfid
+ .or()
+ .number("[^,]*,") // cid
+ .number("(d+),") // rssi
+ .number("(d+),") // satellites
+ .number("[^,]*,") // battery level
+ .expression("([01]),") // ignition
+ .expression("([LH]{4}),") // input
+ .expression("[NT]{4},") // tamper status
+ .expression("([LH]{2}),") // output
+ .number("(d+.d+),") // adc1
+ .number("(d+.d+),") // adc1
+ .number("[^,]*,") // device id
+ .number("(d+),") // odometer
+ .groupEnd()
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String type = parser.next();
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ switch (type) {
+ case "rmv":
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_CUT);
+ break;
+ case "ebl":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_POWER);
+ break;
+ case "ibl":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ case "tmp":
+ case "smt":
+ case "btt":
+ position.set(Position.KEY_ALARM, Position.ALARM_TAMPERING);
+ break;
+ case "ion":
+ position.set(Position.KEY_IGNITION, true);
+ break;
+ case "iof":
+ position.set(Position.KEY_IGNITION, false);
+ break;
+ default:
+ break;
+ }
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setValid(parser.nextInt() > 0);
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setCourse(parser.nextDouble());
+
+ if (parser.hasNext(15)) {
+
+ position.setAltitude(parser.nextDouble());
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_SATELLITES_VISIBLE, parser.nextInt());
+ position.set(Position.KEY_OPERATOR, parser.next());
+ position.set(Position.KEY_RSSI, parser.nextInt());
+ position.set(Position.KEY_IGNITION, parser.nextInt() == 1);
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ position.set(Position.KEY_POWER, parser.nextDouble());
+
+ int input = parser.nextBinInt();
+ int output = parser.nextBinInt();
+
+ if (!BitUtil.check(input, 0)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ }
+
+ position.set(Position.KEY_INPUT, input);
+ position.set(Position.KEY_OUTPUT, output);
+
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.PREFIX_ADC + 2, parser.nextDouble());
+ position.set(Position.KEY_VERSION_FW, parser.next());
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ }
+
+ if (parser.hasNext(6)) {
+
+ position.set(Position.KEY_RSSI, parser.nextInt());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_IGNITION, parser.nextInt() == 1);
+
+ char[] input = parser.next().toCharArray();
+ for (int i = 0; i < input.length; i++) {
+ position.set(Position.PREFIX_IN + (i + 1), input[i] == 'H');
+ }
+
+ char[] output = parser.next().toCharArray();
+ for (int i = 0; i < output.length; i++) {
+ position.set(Position.PREFIX_OUT + (i + 1), output[i] == 'H');
+ }
+
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.PREFIX_ADC + 2, parser.nextDouble());
+ position.set(Position.KEY_ODOMETER, parser.nextInt());
+
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TopflytechProtocol.java b/src/main/java/org/traccar/protocol/TopflytechProtocol.java
new file mode 100644
index 000000000..303072bdb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TopflytechProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TopflytechProtocol extends BaseProtocol {
+
+ public TopflytechProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, ')'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new TopflytechProtocolDecoder(TopflytechProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TopflytechProtocolDecoder.java b/src/main/java/org/traccar/protocol/TopflytechProtocolDecoder.java
new file mode 100644
index 000000000..6de053c32
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TopflytechProtocolDecoder.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class TopflytechProtocolDecoder extends BaseProtocolDecoder {
+
+ public TopflytechProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("(")
+ .number("(d+)") // imei
+ .any()
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .expression("([AV])")
+ .number("(dd)(dd.dddd)([NS])") // latitude
+ .number("(ddd)(dd.dddd)([EW])") // longitude
+ .number("(ddd.d)") // speed
+ .number("(d+.d+)") // course
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TotemFrameDecoder.java b/src/main/java/org/traccar/protocol/TotemFrameDecoder.java
new file mode 100644
index 000000000..3fa5abc7a
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TotemFrameDecoder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+import java.nio.charset.StandardCharsets;
+
+import org.traccar.BaseFrameDecoder;
+import org.traccar.helper.BufferUtil;
+
+public class TotemFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 10) {
+ return null;
+ }
+
+ int beginIndex = BufferUtil.indexOf("$$", buf);
+ if (beginIndex == -1) {
+ return null;
+ } else if (beginIndex > buf.readerIndex()) {
+ buf.readerIndex(beginIndex);
+ }
+
+ int length;
+
+ if (buf.getByte(buf.readerIndex() + 2) == (byte) '0') {
+ length = Integer.parseInt(buf.toString(buf.readerIndex() + 2, 4, StandardCharsets.US_ASCII));
+ } else {
+ length = Integer.parseInt(buf.toString(buf.readerIndex() + 2, 2, StandardCharsets.US_ASCII), 16);
+ }
+
+ if (length <= buf.readableBytes()) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TotemProtocol.java b/src/main/java/org/traccar/protocol/TotemProtocol.java
new file mode 100644
index 000000000..66e1ec4f1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TotemProtocol.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class TotemProtocol extends BaseProtocol {
+
+ public TotemProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ENGINE_RESUME,
+ Command.TYPE_ENGINE_STOP
+ );
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new TotemFrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new TotemProtocolEncoder());
+ pipeline.addLast(new TotemProtocolDecoder(TotemProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TotemProtocolDecoder.java b/src/main/java/org/traccar/protocol/TotemProtocolDecoder.java
new file mode 100644
index 000000000..cd7f684b8
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TotemProtocolDecoder.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2013 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class TotemProtocolDecoder extends BaseProtocolDecoder {
+
+ public TotemProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN1 = new PatternBuilder()
+ .text("$$") // header
+ .number("xx") // length
+ .number("(d+)|") // imei
+ .expression("(..)") // alarm
+ .text("$GPRMC,")
+ .number("(dd)(dd)(dd).d+,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(d+)(dd.d+),([NS]),") // latitude
+ .number("(d+)(dd.d+),([EW]),") // longitude
+ .number("(d+.?d*)?,") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .expression("[^*]*").text("*")
+ .number("xx|") // checksum
+ .number("(d+.d+)|") // pdop
+ .number("(d+.d+)|") // hdop
+ .number("(d+.d+)|") // vdop
+ .number("(d+)|") // io status
+ .number("d+|") // battery time
+ .number("d") // charged
+ .number("(ddd)") // battery
+ .number("(dddd)|") // power
+ .number("(d+)|").optional() // adc
+ .number("x*(xxxx)") // lac
+ .number("(xxxx)|") // cid
+ .number("(d+)|") // temperature
+ .number("(d+.d+)|") // odometer
+ .number("d+|") // serial number
+ .any()
+ .number("xxxx") // checksum
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN2 = new PatternBuilder()
+ .text("$$") // header
+ .number("xx") // length
+ .number("(d+)|") // imei
+ .expression("(..)") // alarm type
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .number("(dd)(dd)(dd)|") // time (hhmmss)
+ .expression("([AV])|") // validity
+ .number("(d+)(dd.d+)|") // latitude
+ .expression("([NS])|")
+ .number("(d+)(dd.d+)|") // longitude
+ .expression("([EW])|")
+ .number("(d+.d+)?|") // speed
+ .number("(d+)?|") // course
+ .number("(d+.d+)|") // hdop
+ .number("(d+)|") // io status
+ .number("d") // charged
+ .number("(dd)") // battery
+ .number("(dd)|") // external power
+ .number("(d+)|") // adc
+ .number("(xxxx)") // lac
+ .number("(xxxx)|") // cid
+ .number("(d+)|") // temperature
+ .number("(d+.d+)|") // odometer
+ .number("d+|") // serial number
+ .number("xxxx") // checksum
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN3 = new PatternBuilder()
+ .text("$$") // header
+ .number("xx") // length
+ .number("(d+)|") // imei
+ .expression("(..)") // alarm type
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number("(xxxx)") // io status
+ .expression("[01]") // charging
+ .number("(dd)") // battery
+ .number("(dd)") // external power
+ .number("(dddd)") // adc 1
+ .number("(dddd)") // adc 2
+ .number("(ddd)") // temperature 1
+ .number("(ddd)") // temperature 2
+ .number("(xxxx)") // lac
+ .number("(xxxx)") // cid
+ .expression("([AV])") // validity
+ .number("(dd)") // satellites
+ .number("(ddd)") // course
+ .number("(ddd)") // speed
+ .number("(dd.d)") // pdop
+ .number("(d{7})") // odometer
+ .number("(dd)(dd.dddd)([NS])") // latitude
+ .number("(ddd)(dd.dddd)([EW])") // longitude
+ .number("dddd") // serial number
+ .number("xxxx") // checksum
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN4 = new PatternBuilder()
+ .text("$$") // header
+ .number("dddd") // length
+ .number("(xx)") // type
+ .number("(d+)|") // imei
+ .number("(x{8})") // status
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number("(dd)") // battery
+ .number("(dd)") // external power
+ .number("(dddd)") // adc 1
+ .groupBegin()
+ .groupBegin()
+ .number("(dddd)") // adc 2
+ .number("(dddd)") // adc 3
+ .number("(dddd)") // adc 4
+ .groupEnd("?")
+ .number("(dddd)") // temperature 1
+ .number("(dddd)?") // temperature 2
+ .groupEnd("?")
+ .number("(xxxx)") // lac
+ .number("(xxxx)") // cid
+ .groupBegin()
+ .number("(dd)") // mcc
+ .number("(ddd)") // mnc
+ .groupEnd("?")
+ .number("(dd)") // satellites
+ .number("(dd)") // gsm (rssi)
+ .number("(ddd)") // course
+ .number("(ddd)") // speed
+ .number("(dd.d)") // hdop
+ .number("(d{7})") // odometer
+ .number("(dd)(dd.dddd)([NS])") // latitude
+ .number("(ddd)(dd.dddd)([EW])") // longitude
+ .number("dddd") // serial number
+ .number("xx") // checksum
+ .any()
+ .compile();
+
+ private String decodeAlarm123(int value) {
+ switch (value) {
+ case 0x01:
+ return Position.ALARM_SOS;
+ case 0x10:
+ return Position.ALARM_LOW_BATTERY;
+ case 0x11:
+ return Position.ALARM_OVERSPEED;
+ case 0x30:
+ return Position.ALARM_PARKING;
+ case 0x42:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 0x43:
+ return Position.ALARM_GEOFENCE_ENTER;
+ default:
+ return null;
+ }
+ }
+
+ private String decodeAlarm4(int value) {
+ switch (value) {
+ case 0x01:
+ return Position.ALARM_SOS;
+ case 0x02:
+ return Position.ALARM_OVERSPEED;
+ case 0x04:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 0x05:
+ return Position.ALARM_GEOFENCE_ENTER;
+ case 0x40:
+ return Position.ALARM_SHOCK;
+ case 0x42:
+ return Position.ALARM_ACCELERATION;
+ case 0x43:
+ return Position.ALARM_BRAKING;
+ default:
+ return null;
+ }
+ }
+
+ private boolean decode12(Position position, Parser parser, Pattern pattern) {
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ALARM, decodeAlarm123(Short.parseShort(parser.next(), 16)));
+ }
+ DateBuilder dateBuilder = new DateBuilder();
+ int year = 0, month = 0, day = 0;
+ if (pattern == PATTERN2) {
+ day = parser.nextInt(0);
+ month = parser.nextInt(0);
+ year = parser.nextInt(0);
+ }
+ dateBuilder.setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ if (pattern == PATTERN1) {
+ day = parser.nextInt(0);
+ month = parser.nextInt(0);
+ year = parser.nextInt(0);
+ }
+ if (year == 0) {
+ return false; // ignore invalid data
+ }
+ dateBuilder.setDate(year, month, day);
+ position.setTime(dateBuilder.getDate());
+
+ if (pattern == PATTERN1) {
+ position.set(Position.KEY_PDOP, parser.nextDouble());
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.set(Position.KEY_VDOP, parser.nextDouble());
+ } else {
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ }
+
+ int io = parser.nextBinInt();
+ position.set(Position.KEY_STATUS, io);
+ if (pattern == PATTERN1) {
+ position.set(Position.KEY_ALARM, BitUtil.check(io, 0) ? Position.ALARM_SOS : null);
+ position.set(Position.PREFIX_IN + 3, BitUtil.check(io, 4));
+ position.set(Position.PREFIX_IN + 4, BitUtil.check(io, 5));
+ position.set(Position.PREFIX_IN + 1, BitUtil.check(io, 6));
+ position.set(Position.PREFIX_IN + 2, BitUtil.check(io, 7));
+ position.set(Position.PREFIX_OUT + 1, BitUtil.check(io, 8));
+ position.set(Position.PREFIX_OUT + 2, BitUtil.check(io, 9));
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0) * 0.01);
+ } else {
+ position.set(Position.KEY_ANTENNA, BitUtil.check(io, 0));
+ position.set(Position.KEY_CHARGE, BitUtil.check(io, 1));
+ for (int i = 1; i <= 6; i++) {
+ position.set(Position.PREFIX_IN + i, BitUtil.check(io, 1 + i));
+ }
+ for (int i = 1; i <= 4; i++) {
+ position.set(Position.PREFIX_OUT + i, BitUtil.check(io, 7 + i));
+ }
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0) * 0.1);
+ }
+
+ position.set(Position.KEY_POWER, parser.nextDouble(0));
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+
+ int lac = parser.nextHexInt(0);
+ int cid = parser.nextHexInt(0);
+ if (lac != 0 && cid != 0) {
+ position.setNetwork(new Network(CellTower.fromLacCid(lac, cid)));
+ }
+
+ position.set(Position.PREFIX_TEMP + 1, parser.next());
+ position.set(Position.KEY_ODOMETER, parser.nextDouble(0) * 1000);
+
+ return true;
+ }
+
+ private boolean decode3(Position position, Parser parser) {
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ALARM, decodeAlarm123(Short.parseShort(parser.next(), 16)));
+ }
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.set(Position.PREFIX_IO + 1, parser.next());
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0) * 0.1);
+ position.set(Position.KEY_POWER, parser.nextDouble(0));
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+ position.set(Position.PREFIX_TEMP + 1, parser.next());
+ position.set(Position.PREFIX_TEMP + 2, parser.next());
+
+ position.setNetwork(new Network(
+ CellTower.fromLacCid(parser.nextHexInt(0), parser.nextHexInt(0))));
+
+ position.setValid(parser.next().equals("A"));
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.setCourse(parser.nextDouble(0));
+ position.setSpeed(parser.nextDouble(0));
+ position.set(Position.KEY_PDOP, parser.nextDouble());
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0) * 1000);
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ return true;
+ }
+
+ private boolean decode4(Position position, Parser parser) {
+
+ long status = parser.nextHexLong();
+
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 32 - 1) ? Position.ALARM_SOS : null);
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 32 - 2));
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 32 - 3) ? Position.ALARM_OVERSPEED : null);
+ position.set(Position.KEY_CHARGE, BitUtil.check(status, 32 - 4));
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 32 - 5) ? Position.ALARM_GEOFENCE_EXIT : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 32 - 6) ? Position.ALARM_GEOFENCE_ENTER : null);
+ position.set(Position.PREFIX_OUT + 1, BitUtil.check(status, 32 - 9));
+ position.set(Position.PREFIX_OUT + 2, BitUtil.check(status, 32 - 10));
+ position.set(Position.PREFIX_OUT + 3, BitUtil.check(status, 32 - 11));
+ position.set(Position.PREFIX_OUT + 4, BitUtil.check(status, 32 - 12));
+ position.set(Position.PREFIX_IN + 2, BitUtil.check(status, 32 - 13));
+ position.set(Position.PREFIX_IN + 3, BitUtil.check(status, 32 - 14));
+ position.set(Position.PREFIX_IN + 4, BitUtil.check(status, 32 - 15));
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 32 - 16) ? Position.ALARM_SHOCK : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 32 - 18) ? Position.ALARM_LOW_BATTERY : null);
+ position.set(Position.KEY_ALARM, BitUtil.check(status, 32 - 22) ? Position.ALARM_JAMMING : null);
+
+
+ position.setTime(parser.nextDateTime());
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble() * 0.1);
+ position.set(Position.KEY_POWER, parser.nextDouble());
+
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+ position.set(Position.PREFIX_ADC + 3, parser.next());
+ position.set(Position.PREFIX_ADC + 4, parser.next());
+ position.set(Position.PREFIX_TEMP + 1, parser.next());
+
+ if (parser.hasNext()) {
+ position.set(Position.PREFIX_TEMP + 2, parser.next());
+ position.setValid(BitUtil.check(status, 32 - 20));
+ } else {
+ position.setValid(BitUtil.check(status, 32 - 18));
+ }
+
+ int lac = parser.nextHexInt();
+ int cid = parser.nextHexInt();
+ CellTower cellTower;
+ if (parser.hasNext(2)) {
+ int mnc = parser.nextInt();
+ int mcc = parser.nextInt();
+ cellTower = CellTower.from(mcc, mnc, lac, cid);
+ } else {
+ cellTower = CellTower.fromLacCid(lac, cid);
+ }
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ cellTower.setSignalStrength(parser.nextInt());
+ position.setNetwork(new Network(cellTower));
+
+ position.setCourse(parser.nextDouble());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.set(Position.KEY_ODOMETER, parser.nextInt() * 1000);
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ return true;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ Pattern pattern = PATTERN3;
+ if (sentence.charAt(2) == '0') {
+ pattern = PATTERN4;
+ } else if (sentence.contains("$GPRMC")) {
+ pattern = PATTERN1;
+ } else {
+ int index = sentence.indexOf('|');
+ if (index != -1 && sentence.indexOf('|', index + 1) != -1) {
+ pattern = PATTERN2;
+ }
+ }
+
+ Parser parser = new Parser(pattern, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ if (pattern == PATTERN4) {
+ position.set(Position.KEY_ALARM, decodeAlarm4(parser.nextHexInt()));
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ boolean result;
+ if (pattern == PATTERN1 || pattern == PATTERN2) {
+ result = decode12(position, parser, pattern);
+ } else if (pattern == PATTERN3) {
+ result = decode3(position, parser);
+ } else {
+ result = decode4(position, parser);
+ }
+
+ if (channel != null) {
+ if (pattern == PATTERN4) {
+ String response = "$$0014AA" + sentence.substring(sentence.length() - 6, sentence.length() - 2);
+ response += String.format("%02X", Checksum.xor(response)).toUpperCase();
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ } else {
+ channel.writeAndFlush(new NetworkMessage("ACK OK\r\n", remoteAddress));
+ }
+ }
+
+ return result ? position : null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TotemProtocolEncoder.java b/src/main/java/org/traccar/protocol/TotemProtocolEncoder.java
new file mode 100644
index 000000000..b5049859d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TotemProtocolEncoder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015 Irving Gonzalez
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class TotemProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ initDevicePassword(command, "000000");
+
+ switch (command.getType()) {
+ // Assuming PIN 8 (Output C) is the power wire, like manual says but it can be PIN 5,7,8
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, "*{%s},025,C,1#", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, "*{%s},025,C,0#", Command.KEY_DEVICE_PASSWORD);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tr20Protocol.java b/src/main/java/org/traccar/protocol/Tr20Protocol.java
new file mode 100644
index 000000000..3eee9d9c3
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tr20Protocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Tr20Protocol extends BaseProtocol {
+
+ public Tr20Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Tr20ProtocolDecoder(Tr20Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tr20ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Tr20ProtocolDecoder.java
new file mode 100644
index 000000000..c2e6c381f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tr20ProtocolDecoder.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Tr20ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Tr20ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_PING = new PatternBuilder()
+ .text("%%")
+ .expression("[^,]+,")
+ .number("(d+)")
+ .compile();
+
+ private static final Pattern PATTERN_DATA = new PatternBuilder()
+ .text("%%")
+ .expression("([^,]+),") // id
+ .expression("([AL]),") // validity
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([NS])")
+ .number("(dd)(dd.d+)") // latitude
+ .expression("([EW])")
+ .number("(ddd)(dd.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(?:NA|[FC]?(-?d+)),") // temperature
+ .number("(x{8}),") // status
+ .number("(d+)") // event
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN_PING, (String) msg);
+ if (parser.matches()) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(
+ "&&" + parser.next() + "\r\n", remoteAddress)); // keep-alive response
+ }
+ return null;
+ }
+
+ parser = new Parser(PATTERN_DATA, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.next().equals("A"));
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setCourse(parser.nextDouble());
+
+ position.set(Position.PREFIX_TEMP + 1, parser.nextInt());
+ position.set(Position.KEY_STATUS, parser.nextHexLong());
+ position.set(Position.KEY_EVENT, parser.nextInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tr900Protocol.java b/src/main/java/org/traccar/protocol/Tr900Protocol.java
new file mode 100644
index 000000000..b70521b35
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tr900Protocol.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Tr900Protocol extends BaseProtocol {
+
+ public Tr900Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Tr900ProtocolDecoder(Tr900Protocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Tr900ProtocolDecoder(Tr900Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tr900ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Tr900ProtocolDecoder.java
new file mode 100644
index 000000000..319194c21
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tr900ProtocolDecoder.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2015 -2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Tr900ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Tr900ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number(">(d+),") // id
+ .number("d+,") // period
+ .number("(d),") // fix
+ .number("(dd)(dd)(dd),") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([EW])")
+ .number("(ddd)(dd.d+),") // longitude
+ .expression("([NS])")
+ .number("(dd)(dd.d+),") // latitude
+ .expression("[^,]*,") // command
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(d+),") // gsm
+ .number("(d+),") // event
+ .number("(d+)-") // adc
+ .number("(d+),") // battery
+ .number("d+,") // impulses
+ .number("(d+),") // input
+ .number("(d+)") // status
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(parser.nextInt(0) == 1);
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG_MIN));
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_RSSI, parser.nextDouble());
+ position.set(Position.KEY_EVENT, parser.nextInt(0));
+ position.set(Position.PREFIX_ADC + 1, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY, parser.nextInt(0));
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_STATUS, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TrackboxProtocol.java b/src/main/java/org/traccar/protocol/TrackboxProtocol.java
new file mode 100644
index 000000000..5da5abd64
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TrackboxProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TrackboxProtocol extends BaseProtocol {
+
+ public TrackboxProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new TrackboxProtocolDecoder(TrackboxProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TrackboxProtocolDecoder.java b/src/main/java/org/traccar/protocol/TrackboxProtocolDecoder.java
new file mode 100644
index 000000000..db8022738
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TrackboxProtocolDecoder.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class TrackboxProtocolDecoder extends BaseProtocolDecoder {
+
+ public TrackboxProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(dd)(dd)(dd).(ddd),") // time (hhmmss.sss)
+ .number("(dd)(dd.dddd)([NS]),") // latitude
+ .number("(ddd)(dd.dddd)([EW]),") // longitude
+ .number("(d+.d),") // hdop
+ .number("(-?d+.?d*),") // altitude
+ .number("(d),") // fix type
+ .number("(d+.d+),") // course
+ .number("d+.d+,") // speed (kph)
+ .number("(d+.d+),") // speed (knots)
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(d+)") // satellites
+ .compile();
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress) {
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage("=OK=\r\n", remoteAddress));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("a=connect")) {
+ String id = sentence.substring(sentence.indexOf("i=") + 2);
+ if (getDeviceSession(channel, remoteAddress, id) != null) {
+ sendResponse(channel, remoteAddress);
+ }
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+ sendResponse(channel, remoteAddress);
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ position.setAltitude(parser.nextDouble(0));
+
+ int fix = parser.nextInt(0);
+ position.set(Position.KEY_GPS, fix);
+ position.setValid(fix > 0);
+
+ position.setCourse(parser.nextDouble(0));
+ position.setSpeed(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TrakMateProtocol.java b/src/main/java/org/traccar/protocol/TrakMateProtocol.java
new file mode 100644
index 000000000..bda5df10f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TrakMateProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TrakMateProtocol extends BaseProtocol {
+
+ public TrakMateProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '#'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new TrakMateProtocolDecoder(TrakMateProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TrakMateProtocolDecoder.java b/src/main/java/org/traccar/protocol/TrakMateProtocolDecoder.java
new file mode 100644
index 000000000..4d5cb18f5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TrakMateProtocolDecoder.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class TrakMateProtocolDecoder extends BaseProtocolDecoder {
+
+ public TrakMateProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_SRT = new PatternBuilder()
+ .text("^TMSRT|")
+ .expression("([^ ]+)|") // uid
+ .number("(d+.d+)|") // latitude
+ .number("(d+.d+)|") // longitude
+ .number("(dd)(dd)(dd)|") // time (hhmmss)
+ .number("(dd)(dd)(dd)|") // date (ddmmyy)
+ .number("(d+.d+)|") // software ver
+ .number("(d+.d+)|") // Hardware ver
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_PER = new PatternBuilder()
+ .text("^TM")
+ .expression("...|") // type
+ .expression("([^ ]+)|") // uid
+ .number("(d+)|") // seq
+ .number("(d+.d+)|") // latitude
+ .number("(d+.d+)|") // longitude
+ .number("(dd)(dd)(dd)|") // time (hhmmss)
+ .number("(dd)(dd)(dd)|") // date (ddmmyy)
+ .number("(d+.d+)|") // speed
+ .number("(d+.d+)|") // heading
+ .number("(d+)|").optional() // satellites
+ .number("([01])|") // ignition
+ .groupBegin()
+ .number("(d+)|") // dop1
+ .number("(d+)|") // dop2
+ .number("(d+.d+)|") // analog
+ .number("(d+.d+)|") // internal battery
+ .or()
+ .number("-?d+ -?d+ -?d+|") // accelerometer
+ .number("([01])|") // movement
+ .groupEnd()
+ .number("(d+.d+)|") // vehicle battery
+ .number("(d+.d+)|") // gps odometer
+ .number("(d+.d+)|").optional() // pulse odometer
+ .number("([01])|") // main power status
+ .number("([01])|") // gps data validity
+ .number("([01])|") // live or cache
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_ALT = new PatternBuilder()
+ .text("^TMALT|")
+ .expression("([^ ]+)|") // uid
+ .number("(d+)|") // seq
+ .number("(d+)|") // Alert type
+ .number("(d+)|") // Alert status
+ .number("(d+.d+)|") // latitude
+ .number("(d+.d+)|") // longitude
+ .number("(dd)(dd)(dd)|") // time (hhmmss)
+ .number("(dd)(dd)(dd)|") // date (ddmmyy)
+ .number("(d+.d+)|") // speed
+ .number("(d+.d+)|") // heading
+ .any()
+ .compile();
+
+ private String decodeAlarm(int value) {
+ switch (value) {
+ case 1:
+ return Position.ALARM_SOS;
+ case 3:
+ return Position.ALARM_GEOFENCE;
+ case 4:
+ return Position.ALARM_POWER_CUT;
+ default:
+ return null;
+ }
+ }
+
+ private Object decodeSrt(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_SRT, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ position.set(Position.KEY_VERSION_FW, parser.next());
+ position.set(Position.KEY_VERSION_HW, parser.next());
+
+ return position;
+ }
+
+ private Object decodeAlt(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN_ALT, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ parser.next(); // seq
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.nextInt()));
+ parser.next(); // alert status or data
+
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ position.setSpeed(parser.nextDouble());
+ position.setCourse(parser.nextDouble());
+
+ return position;
+ }
+
+ private Object decodePer(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN_PER, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ parser.next(); // seq
+
+ position.setLatitude(parser.nextDouble());
+ position.setLongitude(parser.nextDouble());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ position.setSpeed(parser.nextDouble());
+ position.setCourse(parser.nextDouble());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_IGNITION, parser.nextInt() > 0);
+
+ if (parser.hasNext(4)) {
+ position.set("dop1", parser.nextInt());
+ position.set("dop2", parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ }
+
+ if (parser.hasNext()) {
+ position.set(Position.KEY_MOTION, parser.nextInt(0) > 0);
+ }
+
+ position.set(Position.KEY_POWER, parser.nextDouble());
+ position.set(Position.KEY_ODOMETER, parser.nextDouble());
+ position.set("pulseOdometer", parser.nextDouble());
+ position.set(Position.KEY_STATUS, parser.nextInt());
+
+ position.setValid(parser.nextInt() > 0);
+
+ position.set(Position.KEY_ARCHIVE, parser.nextInt() > 0);
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ int typeIndex = sentence.indexOf("^TM");
+ if (typeIndex < 0) {
+ return null;
+ }
+
+ String type = sentence.substring(typeIndex + 3, typeIndex + 6);
+ switch (type) {
+ case "ALT":
+ return decodeAlt(channel, remoteAddress, sentence);
+ case "SRT":
+ return decodeSrt(channel, remoteAddress, sentence);
+ default:
+ return decodePer(channel, remoteAddress, sentence);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TramigoFrameDecoder.java b/src/main/java/org/traccar/protocol/TramigoFrameDecoder.java
new file mode 100644
index 000000000..aaaaccb60
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TramigoFrameDecoder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class TramigoFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 20) {
+ return null;
+ }
+
+ int length;
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0x80) {
+ length = buf.getUnsignedShortLE(buf.readerIndex() + 6);
+ } else {
+ length = buf.getUnsignedShort(buf.readerIndex() + 6);
+ }
+
+ if (length >= buf.readableBytes()) {
+ return buf.readRetainedSlice(length);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TramigoProtocol.java b/src/main/java/org/traccar/protocol/TramigoProtocol.java
new file mode 100644
index 000000000..f683ccc5d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TramigoProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TramigoProtocol extends BaseProtocol {
+
+ public TramigoProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new TramigoFrameDecoder());
+ pipeline.addLast(new TramigoProtocolDecoder(TramigoProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TramigoProtocolDecoder.java b/src/main/java/org/traccar/protocol/TramigoProtocolDecoder.java
new file mode 100644
index 000000000..e42e2f670
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TramigoProtocolDecoder.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TramigoProtocolDecoder extends BaseProtocolDecoder {
+
+ public TramigoProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ public static final int MSG_COMPACT = 0x0100;
+ public static final int MSG_FULL = 0x00FE;
+
+ private static final String[] DIRECTIONS = new String[] {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ int protocol = buf.readUnsignedByte();
+ boolean legacy = protocol == 0x80;
+
+ buf.readUnsignedByte(); // version id
+ int index = legacy ? buf.readUnsignedShort() : buf.readUnsignedShortLE();
+ int type = legacy ? buf.readUnsignedShort() : buf.readUnsignedShortLE();
+ buf.readUnsignedShort(); // length
+ buf.readUnsignedShort(); // mask
+ buf.readUnsignedShort(); // checksum
+ long id = legacy ? buf.readUnsignedInt() : buf.readUnsignedIntLE();
+ buf.readUnsignedInt(); // time
+
+ Position position = new Position(getProtocolName());
+ position.set(Position.KEY_INDEX, index);
+ position.setValid(true);
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, String.valueOf(id));
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (protocol == 0x01 && (type == MSG_COMPACT || type == MSG_FULL)) {
+
+ // need to send ack?
+
+ buf.readUnsignedShortLE(); // report trigger
+ buf.readUnsignedShortLE(); // state flag
+
+ position.setLatitude(buf.readUnsignedIntLE() * 0.0000001);
+ position.setLongitude(buf.readUnsignedIntLE() * 0.0000001);
+
+ position.set(Position.KEY_RSSI, buf.readUnsignedShortLE());
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedShortLE());
+ position.set(Position.KEY_SATELLITES_VISIBLE, buf.readUnsignedShortLE());
+ position.set("gpsAntennaStatus", buf.readUnsignedShortLE());
+
+ position.setSpeed(buf.readUnsignedShortLE() * 0.194384);
+ position.setCourse(buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedIntLE());
+
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShortLE());
+
+ position.set(Position.KEY_CHARGE, buf.readUnsignedShortLE());
+
+ position.setTime(new Date(buf.readUnsignedIntLE() * 1000));
+
+ // parse other data
+
+ return position;
+
+ } else if (legacy) {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(
+ Unpooled.copiedBuffer("gprs,ack," + index, StandardCharsets.US_ASCII), remoteAddress));
+ }
+
+ String sentence = buf.toString(StandardCharsets.US_ASCII);
+
+ Pattern pattern = Pattern.compile("(-?\\d+\\.\\d+), (-?\\d+\\.\\d+)");
+ Matcher matcher = pattern.matcher(sentence);
+ if (!matcher.find()) {
+ return null;
+ }
+ position.setLatitude(Double.parseDouble(matcher.group(1)));
+ position.setLongitude(Double.parseDouble(matcher.group(2)));
+
+ pattern = Pattern.compile("([NSWE]{1,2}) with speed (\\d+) km/h");
+ matcher = pattern.matcher(sentence);
+ if (matcher.find()) {
+ for (int i = 0; i < DIRECTIONS.length; i++) {
+ if (matcher.group(1).equals(DIRECTIONS[i])) {
+ position.setCourse(i * 45.0);
+ break;
+ }
+ }
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(matcher.group(2))));
+ }
+
+ pattern = Pattern.compile("(\\d{1,2}:\\d{2}(:\\d{2})? \\w{3} \\d{1,2})");
+ matcher = pattern.matcher(sentence);
+ if (!matcher.find()) {
+ return null;
+ }
+ DateFormat dateFormat = new SimpleDateFormat(
+ matcher.group(2) != null ? "HH:mm:ss MMM d yyyy" : "HH:mm MMM d yyyy", Locale.ENGLISH);
+ position.setTime(DateUtil.correctYear(
+ dateFormat.parse(matcher.group(1) + " " + Calendar.getInstance().get(Calendar.YEAR))));
+
+ if (sentence.contains("Ignition on detected")) {
+ position.set(Position.KEY_IGNITION, true);
+ } else if (sentence.contains("Ignition off detected")) {
+ position.set(Position.KEY_IGNITION, false);
+ }
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TrvProtocol.java b/src/main/java/org/traccar/protocol/TrvProtocol.java
new file mode 100644
index 000000000..99a164cf1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TrvProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TrvProtocol extends BaseProtocol {
+
+ public TrvProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '#'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new TrvProtocolDecoder(TrvProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TrvProtocolDecoder.java b/src/main/java/org/traccar/protocol/TrvProtocolDecoder.java
new file mode 100644
index 000000000..b63385187
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TrvProtocolDecoder.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+import org.traccar.model.WifiAccessPoint;
+
+import java.net.SocketAddress;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class TrvProtocolDecoder extends BaseProtocolDecoder {
+
+ public TrvProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("[A-Z]{2,3}")
+ .number("APdd")
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .expression("([AV])") // validity
+ .number("(dd)(dd.d+)") // latitude
+ .expression("([NS])")
+ .number("(ddd)(dd.d+)") // longitude
+ .expression("([EW])")
+ .number("(ddd.d)") // speed
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number("([d.]{6})") // course
+ .number("(ddd)") // gsm
+ .number("(ddd)") // satellites
+ .number("(ddd)") // battery
+ .number("(d)") // acc
+ .number("(dd)") // arm status
+ .number("(dd),") // working mode
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .number("(d+),") // lac
+ .number("(d+)") // cell
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_HEATRBEAT = new PatternBuilder()
+ .expression("[A-Z]{2,3}")
+ .text("CP01,")
+ .number("(ddd)") // gsm
+ .number("(ddd)") // gps
+ .number("(ddd)") // battery
+ .number("(d)") // acc
+ .number("(dd)") // arm status
+ .number("(dd)") // working mode
+ .groupBegin()
+ .number("(ddd)") // interval
+ .number("d") // vibration alarm
+ .number("ddd") // vibration sensitivity
+ .number("d") // automatic arm
+ .number("dddd") // automatic arm time
+ .number("(d)") // blocked
+ .number("(d)") // power status
+ .number("(d)") // movement status
+ .groupEnd("?")
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_LBS = new PatternBuilder()
+ .expression("[A-Z]{2,3}")
+ .text("AP02,")
+ .expression("[^,]+,") // language
+ .number("[01],") // reply
+ .number("d+,") // cell count
+ .number("(d+),") // mcc
+ .number("(d+),") // mnc
+ .expression("(")
+ .groupBegin()
+ .number("d+|") // lac
+ .number("d+|") // cid
+ .number("d+,") // rssi
+ .groupEnd("+")
+ .expression(")")
+ .number("d+,") // wifi count
+ .expression("(.*)") // wifi
+ .compile();
+
+ private Boolean decodeOptionalValue(Parser parser, int activeValue) {
+ int value = parser.nextInt();
+ if (value != 0) {
+ return value == activeValue;
+ }
+ return null;
+ }
+
+ private void decodeCommon(Position position, Parser parser) {
+
+ position.set(Position.KEY_RSSI, parser.nextInt());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_BATTERY, parser.nextInt());
+ position.set(Position.KEY_IGNITION, decodeOptionalValue(parser, 1));
+ position.set(Position.KEY_ARMED, decodeOptionalValue(parser, 1));
+
+ int mode = parser.nextInt();
+ if (mode != 0) {
+ position.set("mode", mode);
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ String id = sentence.startsWith("TRV") ? sentence.substring(0, 3) : sentence.substring(0, 2);
+ String type = sentence.substring(id.length(), id.length() + 4);
+
+ if (channel != null) {
+ String responseHeader = id + (char) (type.charAt(0) + 1) + type.substring(1);
+ if (type.equals("AP00") && id.equals("IW")) {
+ String time = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+ channel.writeAndFlush(new NetworkMessage(responseHeader + "," + time + ",0#", remoteAddress));
+ } else if (type.equals("AP14")) {
+ channel.writeAndFlush(new NetworkMessage(responseHeader + ",0.000,0.000#", remoteAddress));
+ } else {
+ channel.writeAndFlush(new NetworkMessage(responseHeader + "#", remoteAddress));
+ }
+ }
+
+ if (type.equals("AP00")) {
+ getDeviceSession(channel, remoteAddress, sentence.substring(id.length() + type.length()));
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (type.equals("CP01")) {
+
+ Parser parser = new Parser(PATTERN_HEATRBEAT, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ decodeCommon(position, parser);
+
+ if (parser.hasNext(3)) {
+ position.set(Position.KEY_BLOCKED, decodeOptionalValue(parser, 2));
+ position.set(Position.KEY_CHARGE, decodeOptionalValue(parser, 1));
+ position.set(Position.KEY_MOTION, decodeOptionalValue(parser, 1));
+ }
+
+ return position;
+
+ } else if (type.equals("AP01") || type.equals("AP10")) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+
+ dateBuilder.setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ position.setCourse(parser.nextDouble());
+
+ decodeCommon(position, parser);
+
+ position.setNetwork(new Network(CellTower.from(
+ parser.nextInt(), parser.nextInt(), parser.nextInt(), parser.nextInt())));
+
+ return position;
+
+ } else if (type.equals("AP02")) {
+
+ Parser parser = new Parser(PATTERN_LBS, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ int mcc = parser.nextInt();
+ int mnc = parser.nextInt();
+
+ Network network = new Network();
+
+ for (String cell : parser.next().split(",")) {
+ if (!cell.isEmpty()) {
+ String[] values = cell.split("\\|");
+ network.addCellTower(CellTower.from(
+ mcc, mnc,
+ Integer.parseInt(values[0]),
+ Integer.parseInt(values[1]),
+ Integer.parseInt(values[2])));
+ }
+ }
+
+ for (String wifi : parser.next().split("&")) {
+ if (!wifi.isEmpty()) {
+ String[] values = wifi.split("\\|");
+ network.addWifiAccessPoint(WifiAccessPoint.from(values[1], Integer.parseInt(values[2])));
+ }
+ }
+
+ position.setNetwork(network);
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tt8850Protocol.java b/src/main/java/org/traccar/protocol/Tt8850Protocol.java
new file mode 100644
index 000000000..66a13da9e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tt8850Protocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Tt8850Protocol extends BaseProtocol {
+
+ public Tt8850Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "$"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Tt8850ProtocolDecoder(Tt8850Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Tt8850ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Tt8850ProtocolDecoder.java
new file mode 100644
index 000000000..1010528c4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Tt8850ProtocolDecoder.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Tt8850ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Tt8850ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .binary("0004,")
+ .number("xxxx,")
+ .expression("[01],")
+ .expression("GT...,")
+ .number("(?:[0-9A-Z]{2}xxxx)?,") // protocol version
+ .expression("([^,]+),") // imei
+ .any()
+ .number("(d{1,2})?,") // gps accuracy
+ .number("(d{1,3}.d)?,") // speed
+ .number("(d{1,3})?,") // course
+ .number("(-?d{1,5}.d)?,") // altitude
+ .number("(-?d{1,3}.d{6}),") // longitude
+ .number("(-?d{1,2}.d{6}),") // latitude
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(0ddd)?,") // mcc
+ .number("(0ddd)?,") // mnc
+ .number("(xxxx)?,") // lac
+ .number("(xxxx)?,") // cell
+ .any()
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(xxxx)")
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setValid(true);
+ position.setAccuracy(parser.nextInt(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setLatitude(parser.nextDouble(0));
+
+ position.setTime(parser.nextDateTime());
+
+ if (parser.hasNext(4)) {
+ position.setNetwork(new Network(
+ CellTower.from(parser.nextInt(0), parser.nextInt(0), parser.nextHexInt(0), parser.nextHexInt(0))));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TytanProtocol.java b/src/main/java/org/traccar/protocol/TytanProtocol.java
new file mode 100644
index 000000000..32e9acae1
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TytanProtocol.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class TytanProtocol extends BaseProtocol {
+
+ public TytanProtocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new TytanProtocolDecoder(TytanProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TytanProtocolDecoder.java b/src/main/java/org/traccar/protocol/TytanProtocolDecoder.java
new file mode 100644
index 000000000..93d3a63d2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TytanProtocolDecoder.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+public class TytanProtocolDecoder extends BaseProtocolDecoder {
+
+ public TytanProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private void decodeExtraData(Position position, ByteBuf buf, int end) {
+ while (buf.readerIndex() < end) {
+
+ int type = buf.readUnsignedByte();
+ int length = buf.readUnsignedByte();
+ if (length == 255) {
+ length += buf.readUnsignedByte();
+ }
+
+ int n;
+
+ switch (type) {
+ case 2:
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedMedium());
+ break;
+ case 5:
+ position.set(Position.KEY_INPUT, buf.readUnsignedByte());
+ break;
+ case 6:
+ n = buf.readUnsignedByte() >> 4;
+ if (n < 2) {
+ position.set(Position.PREFIX_ADC + n, buf.readFloat());
+ } else {
+ position.set("di" + (n - 2), buf.readFloat());
+ }
+ break;
+ case 7:
+ int alarm = buf.readUnsignedByte();
+ buf.readUnsignedByte();
+ if (BitUtil.check(alarm, 5)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_GENERAL);
+ }
+ break;
+ case 8:
+ position.set("antihijack", buf.readUnsignedByte());
+ break;
+ case 9:
+ position.set("unauthorized", ByteBufUtil.hexDump(buf.readSlice(8)));
+ break;
+ case 10:
+ position.set("authorized", ByteBufUtil.hexDump(buf.readSlice(8)));
+ break;
+ case 24:
+ for (int i = 0; i < length / 2; i++) {
+ position.set(Position.PREFIX_TEMP + buf.readUnsignedByte(), buf.readByte());
+ }
+ break;
+ case 28:
+ position.set(Position.KEY_AXLE_WEIGHT, buf.readUnsignedShort());
+ buf.readUnsignedByte();
+ break;
+ case 90:
+ position.set(Position.KEY_POWER, buf.readFloat());
+ break;
+ case 101:
+ position.set(Position.KEY_OBD_SPEED, buf.readUnsignedByte());
+ break;
+ case 102:
+ position.set(Position.KEY_RPM, buf.readUnsignedByte() * 50);
+ break;
+ case 107:
+ int fuel = buf.readUnsignedShort();
+ int fuelFormat = fuel >> 14;
+ if (fuelFormat == 1) {
+ position.set("fuelValue", (fuel & 0x3fff) * 0.4 + "%");
+ } else if (fuelFormat == 2) {
+ position.set("fuelValue", (fuel & 0x3fff) * 0.5 + " l");
+ } else if (fuelFormat == 3) {
+ position.set("fuelValue", (fuel & 0x3fff) * -0.5 + " l");
+ }
+ break;
+ case 108:
+ position.set(Position.KEY_OBD_ODOMETER, buf.readUnsignedInt() * 5);
+ break;
+ case 150:
+ position.set(Position.KEY_DOOR, buf.readUnsignedByte());
+ break;
+ default:
+ buf.skipBytes(length);
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.readUnsignedByte(); // protocol
+ buf.readUnsignedShort(); // length
+ int index = buf.readUnsignedByte() >> 3;
+
+ if (channel != null) {
+ ByteBuf response = Unpooled.copiedBuffer("^" + index, StandardCharsets.US_ASCII);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+
+ String id = String.valueOf(buf.readUnsignedInt());
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ List<Position> positions = new LinkedList<>();
+
+ while (buf.readableBytes() > 2) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ int end = buf.readerIndex() + buf.readUnsignedByte();
+
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+
+ int flags = buf.readUnsignedByte();
+ position.set(Position.KEY_SATELLITES, BitUtil.from(flags, 2));
+ position.setValid(BitUtil.to(flags, 2) > 0);
+
+ // Latitude
+ double lat = buf.readUnsignedMedium();
+ lat = lat * -180 / 16777216 + 90;
+ position.setLatitude(lat);
+
+ // Longitude
+ double lon = buf.readUnsignedMedium();
+ lon = lon * 360 / 16777216 - 180;
+ position.setLongitude(lon);
+
+ // Status
+ flags = buf.readUnsignedByte();
+ position.set(Position.KEY_IGNITION, BitUtil.check(flags, 0));
+ position.set(Position.KEY_RSSI, BitUtil.between(flags, 2, 5));
+ position.setCourse((BitUtil.from(flags, 5) * 45 + 180) % 360);
+
+ // Speed
+ int speed = buf.readUnsignedByte();
+ if (speed < 250) {
+ position.setSpeed(UnitsConverter.knotsFromKph(speed));
+ }
+
+ decodeExtraData(position, buf, end);
+
+ positions.add(position);
+ }
+
+ return positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TzoneProtocol.java b/src/main/java/org/traccar/protocol/TzoneProtocol.java
new file mode 100644
index 000000000..6e855d138
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TzoneProtocol.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+
+public class TzoneProtocol extends BaseProtocol {
+
+ public TzoneProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(256, 2, 2, 2, 0));
+ pipeline.addLast(new TzoneProtocolDecoder(TzoneProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/TzoneProtocolDecoder.java b/src/main/java/org/traccar/protocol/TzoneProtocolDecoder.java
new file mode 100644
index 000000000..87b44a4b2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/TzoneProtocolDecoder.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+
+public class TzoneProtocolDecoder extends BaseProtocolDecoder {
+
+ public TzoneProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private String decodeAlarm(Short value) {
+ switch (value) {
+ case 0x01:
+ return Position.ALARM_SOS;
+ case 0x10:
+ return Position.ALARM_LOW_BATTERY;
+ case 0x11:
+ return Position.ALARM_OVERSPEED;
+ case 0x14:
+ return Position.ALARM_BRAKING;
+ case 0x15:
+ return Position.ALARM_ACCELERATION;
+ case 0x30:
+ return Position.ALARM_PARKING;
+ case 0x42:
+ return Position.ALARM_GEOFENCE_EXIT;
+ case 0x43:
+ return Position.ALARM_GEOFENCE_ENTER;
+ default:
+ return null;
+ }
+ }
+
+ private boolean decodeGps(Position position, ByteBuf buf, int hardware) {
+
+ int blockLength = buf.readUnsignedShort();
+ int blockEnd = buf.readerIndex() + blockLength;
+
+ if (blockLength < 22) {
+ return false;
+ }
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+
+ double lat;
+ double lon;
+
+ if (hardware == 0x10A || hardware == 0x10B) {
+ lat = buf.readUnsignedInt() / 600000.0;
+ lon = buf.readUnsignedInt() / 600000.0;
+ } else {
+ lat = buf.readUnsignedInt() / 100000.0 / 60.0;
+ lon = buf.readUnsignedInt() / 100000.0 / 60.0;
+ }
+
+ position.setFixTime(new DateBuilder()
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte()).getDate());
+
+ position.setSpeed(buf.readUnsignedShort() * 0.01);
+
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedMedium());
+
+ int flags = buf.readUnsignedShort();
+ position.setCourse(BitUtil.to(flags, 9));
+ if (!BitUtil.check(flags, 10)) {
+ lat = -lat;
+ }
+ position.setLatitude(lat);
+ if (BitUtil.check(flags, 9)) {
+ lon = -lon;
+ }
+ position.setLongitude(lon);
+ position.setValid(BitUtil.check(flags, 11));
+
+ buf.readerIndex(blockEnd);
+
+ return true;
+ }
+
+ private void decodeCards(Position position, ByteBuf buf) {
+
+ int index = 1;
+ for (int i = 0; i < 4; i++) {
+
+ int blockLength = buf.readUnsignedShort();
+ int blockEnd = buf.readerIndex() + blockLength;
+
+ if (blockLength > 0) {
+
+ int count = buf.readUnsignedByte();
+ for (int j = 0; j < count; j++) {
+
+ int length = buf.readUnsignedByte();
+
+ boolean odd = length % 2 != 0;
+ if (odd) {
+ length += 1;
+ }
+
+ String num = ByteBufUtil.hexDump(buf.readSlice(length / 2));
+
+ if (odd) {
+ num = num.substring(1);
+ }
+
+ position.set("card" + index, num);
+ }
+ }
+
+ buf.readerIndex(blockEnd);
+ }
+
+ }
+
+ private void decodePassengers(Position position, ByteBuf buf) {
+
+ int blockLength = buf.readUnsignedShort();
+ int blockEnd = buf.readerIndex() + blockLength;
+
+ if (blockLength > 0) {
+
+ position.set("passengersOn", buf.readUnsignedMedium());
+ position.set("passengersOff", buf.readUnsignedMedium());
+
+ }
+
+ buf.readerIndex(blockEnd);
+
+ }
+
+ private void decodeTags(Position position, ByteBuf buf) {
+
+ int blockLength = buf.readUnsignedShort();
+ int blockEnd = buf.readerIndex() + blockLength;
+
+ if (blockLength > 0) {
+
+ buf.readUnsignedByte(); // tag type
+
+ int count = buf.readUnsignedByte();
+ int tagLength = buf.readUnsignedByte();
+
+ for (int i = 1; i <= count; i++) {
+ int tagEnd = buf.readerIndex() + tagLength;
+
+ buf.readUnsignedByte(); // status
+ buf.readUnsignedShortLE(); // battery voltage
+
+ position.set(Position.PREFIX_TEMP + i, (buf.readShortLE() & 0x3fff) * 0.1);
+
+ buf.readUnsignedByte(); // humidity
+ buf.readUnsignedByte(); // rssi
+
+ buf.readerIndex(tagEnd);
+ }
+
+ }
+
+ buf.readerIndex(blockEnd);
+
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(2); // header
+ buf.readUnsignedShort(); // length
+ if (buf.readUnsignedShort() != 0x2424) {
+ return null;
+ }
+ int hardware = buf.readUnsignedShort();
+ long firmware = buf.readUnsignedInt();
+
+ String imei = ByteBufUtil.hexDump(buf.readSlice(8)).substring(1);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_VERSION_HW, hardware);
+ position.set(Position.KEY_VERSION_FW, firmware);
+
+ position.setDeviceTime(new DateBuilder()
+ .setDate(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte())
+ .setTime(buf.readUnsignedByte(), buf.readUnsignedByte(), buf.readUnsignedByte()).getDate());
+
+ // GPS info
+
+ if (hardware == 0x406 || !decodeGps(position, buf, hardware)) {
+
+ getLastLocation(position, position.getDeviceTime());
+
+ }
+
+ // LBS info
+
+ int blockLength = buf.readUnsignedShort();
+ int blockEnd = buf.readerIndex() + blockLength;
+
+ if (blockLength > 0 && (hardware == 0x10A || hardware == 0x10B || hardware == 0x406)) {
+ position.setNetwork(new Network(
+ CellTower.fromLacCid(buf.readUnsignedShort(), buf.readUnsignedShort())));
+ }
+
+ buf.readerIndex(blockEnd);
+
+ // Status info
+
+ blockLength = buf.readUnsignedShort();
+ blockEnd = buf.readerIndex() + blockLength;
+
+ if (blockLength >= 13) {
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte()));
+ position.set("terminalInfo", buf.readUnsignedByte());
+
+ int status = buf.readUnsignedByte();
+ position.set(Position.PREFIX_OUT + 1, BitUtil.check(status, 0));
+ position.set(Position.PREFIX_OUT + 2, BitUtil.check(status, 1));
+ status = buf.readUnsignedByte();
+ position.set(Position.PREFIX_IN + 1, BitUtil.check(status, 4));
+ if (BitUtil.check(status, 0)) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ }
+
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ position.set("gsmStatus", buf.readUnsignedByte());
+ position.set(Position.KEY_BATTERY, buf.readUnsignedShort());
+ position.set(Position.KEY_POWER, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 1, buf.readUnsignedShort());
+ position.set(Position.PREFIX_ADC + 2, buf.readUnsignedShort());
+ }
+
+ if (blockLength >= 15) {
+ position.set(Position.PREFIX_TEMP + 1, buf.readUnsignedShort());
+ }
+
+ buf.readerIndex(blockEnd);
+
+ if (hardware == 0x10B) {
+
+ decodeCards(position, buf);
+
+ buf.skipBytes(buf.readUnsignedShort()); // temperature
+ buf.skipBytes(buf.readUnsignedShort()); // lock
+
+ decodePassengers(position, buf);
+
+ }
+
+ if (hardware == 0x406) {
+
+ decodeTags(position, buf);
+
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/UlbotechFrameDecoder.java b/src/main/java/org/traccar/protocol/UlbotechFrameDecoder.java
new file mode 100644
index 000000000..f141dc9b7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/UlbotechFrameDecoder.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+public class UlbotechFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 2) {
+ return null;
+ }
+
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0xF8) {
+
+ int index = buf.indexOf(buf.readerIndex() + 1, buf.writerIndex(), (byte) 0xF8);
+ if (index != -1) {
+ ByteBuf result = Unpooled.buffer(index + 1 - buf.readerIndex());
+
+ while (buf.readerIndex() <= index) {
+ int b = buf.readUnsignedByte();
+ if (b == 0xF7) {
+ int ext = buf.readUnsignedByte();
+ if (ext == 0x00) {
+ result.writeByte(0xF7);
+ } else if (ext == 0x0F) {
+ result.writeByte(0xF8);
+ }
+ } else {
+ result.writeByte(b);
+ }
+ }
+
+ return result;
+ }
+
+ } else {
+
+ int index = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '#');
+ if (index != -1) {
+ return buf.readRetainedSlice(index + 1 - buf.readerIndex());
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/UlbotechProtocol.java b/src/main/java/org/traccar/protocol/UlbotechProtocol.java
new file mode 100644
index 000000000..b99ec1cc6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/UlbotechProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class UlbotechProtocol extends BaseProtocol {
+
+ public UlbotechProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new UlbotechFrameDecoder());
+ pipeline.addLast(new UlbotechProtocolDecoder(UlbotechProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/UlbotechProtocolDecoder.java b/src/main/java/org/traccar/protocol/UlbotechProtocolDecoder.java
new file mode 100644
index 000000000..0a2a59e23
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/UlbotechProtocolDecoder.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Checksum;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.ObdDecoder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class UlbotechProtocolDecoder extends BaseProtocolDecoder {
+
+ public UlbotechProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final short DATA_GPS = 0x01;
+ private static final short DATA_LBS = 0x02;
+ private static final short DATA_STATUS = 0x03;
+ private static final short DATA_ODOMETER = 0x04;
+ private static final short DATA_ADC = 0x05;
+ private static final short DATA_GEOFENCE = 0x06;
+ private static final short DATA_OBD2 = 0x07;
+ private static final short DATA_FUEL = 0x08;
+ private static final short DATA_OBD2_ALARM = 0x09;
+ private static final short DATA_HARSH_DRIVER = 0x0A;
+ private static final short DATA_CANBUS = 0x0B;
+ private static final short DATA_J1708 = 0x0C;
+ private static final short DATA_VIN = 0x0D;
+ private static final short DATA_RFID = 0x0E;
+ private static final short DATA_EVENT = 0x10;
+
+ private void decodeObd(Position position, ByteBuf buf, int length) {
+
+ int end = buf.readerIndex() + length;
+
+ while (buf.readerIndex() < end) {
+ int parameterLength = buf.getUnsignedByte(buf.readerIndex()) >> 4;
+ int mode = buf.readUnsignedByte() & 0x0F;
+ position.add(ObdDecoder.decode(mode, ByteBufUtil.hexDump(buf.readSlice(parameterLength - 1))));
+ }
+ }
+
+ private void decodeJ1708(Position position, ByteBuf buf, int length) {
+
+ int end = buf.readerIndex() + length;
+
+ while (buf.readerIndex() < end) {
+ int mark = buf.readUnsignedByte();
+ int len = BitUtil.between(mark, 0, 6);
+ int type = BitUtil.between(mark, 6, 8);
+ int id = buf.readUnsignedByte();
+ if (type == 3) {
+ id += 256;
+ }
+ String value = ByteBufUtil.hexDump(buf.readSlice(len - 1));
+ if (type == 2 || type == 3) {
+ position.set("pid" + id, value);
+ }
+ }
+ }
+
+ private void decodeDriverBehavior(Position position, ByteBuf buf) {
+
+ int value = buf.readUnsignedByte();
+
+ if (BitUtil.check(value, 0)) {
+ position.set("rapidAcceleration", true);
+ }
+ if (BitUtil.check(value, 1)) {
+ position.set("roughBraking", true);
+ }
+ if (BitUtil.check(value, 2)) {
+ position.set("harshCourse", true);
+ }
+ if (BitUtil.check(value, 3)) {
+ position.set("noWarmUp", true);
+ }
+ if (BitUtil.check(value, 4)) {
+ position.set("longIdle", true);
+ }
+ if (BitUtil.check(value, 5)) {
+ position.set("fatigueDriving", true);
+ }
+ if (BitUtil.check(value, 6)) {
+ position.set("roughTerrain", true);
+ }
+ if (BitUtil.check(value, 7)) {
+ position.set("highRpm", true);
+ }
+ }
+
+ private String decodeAlarm(int alarm) {
+ if (BitUtil.check(alarm, 0)) {
+ return Position.ALARM_POWER_OFF;
+ }
+ if (BitUtil.check(alarm, 1)) {
+ return Position.ALARM_MOVEMENT;
+ }
+ if (BitUtil.check(alarm, 2)) {
+ return Position.ALARM_OVERSPEED;
+ }
+ if (BitUtil.check(alarm, 4)) {
+ return Position.ALARM_GEOFENCE;
+ }
+ if (BitUtil.check(alarm, 10)) {
+ return Position.ALARM_SOS;
+ }
+ return null;
+ }
+
+ private void decodeAdc(Position position, ByteBuf buf, int length) {
+ for (int i = 0; i < length / 2; i++) {
+ int value = buf.readUnsignedShort();
+ int id = BitUtil.from(value, 12);
+ value = BitUtil.to(value, 12);
+ switch (id) {
+ case 0:
+ position.set(Position.KEY_POWER, value * (100 + 10) / 4096.0 - 10);
+ break;
+ case 1:
+ position.set(Position.PREFIX_TEMP + 1, value * (125 + 55) / 4096.0 - 55);
+ break;
+ case 2:
+ position.set(Position.KEY_BATTERY, value * (100 + 10) / 4096.0 - 10);
+ break;
+ case 3:
+ position.set(Position.PREFIX_ADC + 1, value * (100 + 10) / 4096.0 - 10);
+ break;
+ default:
+ position.set(Position.PREFIX_IO + id, value);
+ break;
+ }
+ }
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("*TS")
+ .number("dd,") // protocol version
+ .number("(d{15}),") // device id
+ .number("(dd)(dd)(dd)") // time
+ .number("(dd)(dd)(dd),") // date
+ .expression("([^#]+)") // command
+ .text("#")
+ .compile();
+
+ private Object decodeText(Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0))
+ .setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ getLastLocation(position, dateBuilder.getDate());
+
+ position.set(Position.KEY_RESULT, parser.next());
+
+ return position;
+ }
+
+ private Object decodeBinary(Channel channel, SocketAddress remoteAddress, ByteBuf buf) {
+
+ buf.readUnsignedByte(); // header
+ buf.readUnsignedByte(); // version
+ buf.readUnsignedByte(); // type
+
+ String imei = ByteBufUtil.hexDump(buf.readSlice(8)).substring(1);
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ if (deviceSession.getTimeZone() == null) {
+ deviceSession.setTimeZone(getTimeZone(deviceSession.getDeviceId()));
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ long seconds = buf.readUnsignedInt() & 0x7fffffffL;
+ seconds += 946684800L; // 2000-01-01 00:00
+ seconds -= deviceSession.getTimeZone().getRawOffset() / 1000;
+ Date time = new Date(seconds * 1000);
+
+ boolean hasLocation = false;
+
+ while (buf.readableBytes() > 3) {
+
+ int type = buf.readUnsignedByte();
+ int length = type == DATA_CANBUS ? buf.readUnsignedShort() : buf.readUnsignedByte();
+
+ switch (type) {
+
+ case DATA_GPS:
+ hasLocation = true;
+ position.setValid(true);
+ position.setLatitude(buf.readInt() / 1000000.0);
+ position.setLongitude(buf.readInt() / 1000000.0);
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+ position.setCourse(buf.readUnsignedShort());
+ position.set(Position.KEY_HDOP, buf.readUnsignedShort());
+ break;
+
+ case DATA_LBS:
+ if (length == 11) {
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedShort(),
+ buf.readUnsignedShort(), buf.readUnsignedInt(), -buf.readUnsignedByte())));
+ } else {
+ position.setNetwork(new Network(CellTower.from(
+ buf.readUnsignedShort(), buf.readUnsignedShort(),
+ buf.readUnsignedShort(), buf.readUnsignedShort(), -buf.readUnsignedByte())));
+ }
+ if (length > 9 && length != 11) {
+ buf.skipBytes(length - 9);
+ }
+ break;
+
+ case DATA_STATUS:
+ int status = buf.readUnsignedShort();
+ position.set(Position.KEY_IGNITION, BitUtil.check(status, 9));
+ position.set(Position.KEY_STATUS, status);
+ position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedShort()));
+ break;
+
+ case DATA_ODOMETER:
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt());
+ break;
+
+ case DATA_ADC:
+ decodeAdc(position, buf, length);
+ break;
+
+ case DATA_GEOFENCE:
+ position.set("geofenceIn", buf.readUnsignedInt());
+ position.set("geofenceAlarm", buf.readUnsignedInt());
+ break;
+
+ case DATA_OBD2:
+ decodeObd(position, buf, length);
+ break;
+
+ case DATA_FUEL:
+ position.set(Position.KEY_FUEL_CONSUMPTION, buf.readUnsignedInt() / 10000.0);
+ break;
+
+ case DATA_OBD2_ALARM:
+ decodeObd(position, buf, length);
+ break;
+
+ case DATA_HARSH_DRIVER:
+ decodeDriverBehavior(position, buf);
+ break;
+
+ case DATA_CANBUS:
+ position.set("can", ByteBufUtil.hexDump(buf.readSlice(length)));
+ break;
+
+ case DATA_J1708:
+ decodeJ1708(position, buf, length);
+ break;
+
+ case DATA_VIN:
+ position.set(Position.KEY_VIN, buf.readSlice(length).toString(StandardCharsets.US_ASCII));
+ break;
+
+ case DATA_RFID:
+ position.set(Position.KEY_DRIVER_UNIQUE_ID,
+ buf.readSlice(length - 1).toString(StandardCharsets.US_ASCII));
+ position.set("authorized", buf.readUnsignedByte() != 0);
+ break;
+
+ case DATA_EVENT:
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+ if (length > 1) {
+ position.set("eventMask", buf.readUnsignedInt());
+ }
+ break;
+
+ default:
+ buf.skipBytes(length);
+ break;
+ }
+ }
+
+ if (!hasLocation) {
+ getLastLocation(position, time);
+ } else {
+ position.setTime(time);
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0xF8) {
+
+ if (channel != null) {
+ ByteBuf response = Unpooled.buffer();
+ response.writeByte(0xF8);
+ response.writeByte(DATA_GPS);
+ response.writeByte(0xFE);
+ response.writeShort(buf.getShort(response.writerIndex() - 1 - 2));
+ response.writeShort(Checksum.crc16(Checksum.CRC16_XMODEM, response.nioBuffer(1, 4)));
+ response.writeByte(0xF8);
+ channel.writeAndFlush(new NetworkMessage(response, remoteAddress));
+ }
+
+ return decodeBinary(channel, remoteAddress, buf);
+ } else {
+
+ if (channel != null) {
+ channel.writeAndFlush(new NetworkMessage(Unpooled.copiedBuffer(String.format("*TS01,ACK:%04X#",
+ Checksum.crc16(Checksum.CRC16_XMODEM, buf.nioBuffer(1, buf.writerIndex() - 2))),
+ StandardCharsets.US_ASCII), remoteAddress));
+ }
+
+ return decodeText(channel, remoteAddress, buf.toString(StandardCharsets.US_ASCII));
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/UproProtocol.java b/src/main/java/org/traccar/protocol/UproProtocol.java
new file mode 100644
index 000000000..4e60ffeb6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/UproProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class UproProtocol extends BaseProtocol {
+
+ public UproProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '#'));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new UproProtocolDecoder(UproProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/UproProtocolDecoder.java b/src/main/java/org/traccar/protocol/UproProtocolDecoder.java
new file mode 100644
index 000000000..dc7a9200d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/UproProtocolDecoder.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+public class UproProtocolDecoder extends BaseProtocolDecoder {
+
+ public UproProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_HEADER = new PatternBuilder()
+ .text("*")
+ .expression("(..20)") // head
+ .expression("([01])") // ack
+ .number("(d+),") // device id
+ .expression("(.)") // type
+ .expression("(.)") // subtype
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_LOCATION = new PatternBuilder()
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .number("(dd)(dd)(dddd)") // latitude
+ .number("(ddd)(dd)(dddd)") // longitude
+ .number("(d)") // flags
+ .number("(dd)") // speed
+ .number("(dd)") // course
+ .number("(dd)(dd)(dd)") // date (ddmmyy)
+ .compile();
+
+ private void decodeLocation(Position position, String data) {
+ Parser parser = new Parser(PATTERN_LOCATION, data);
+ if (parser.matches()) {
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(true);
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_MIN_MIN));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_MIN_MIN));
+
+ int flags = parser.nextInt(0);
+ position.setValid(BitUtil.check(flags, 0));
+ if (!BitUtil.check(flags, 1)) {
+ position.setLatitude(-position.getLatitude());
+ }
+ if (!BitUtil.check(flags, 2)) {
+ position.setLongitude(-position.getLongitude());
+ }
+
+ position.setSpeed(parser.nextInt(0) * 2);
+ position.setCourse(parser.nextInt(0) * 10);
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (buf.getByte(buf.readerIndex()) != '*') {
+ return null;
+ }
+
+ int headerIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '&');
+ if (headerIndex < 0) {
+ headerIndex = buf.writerIndex();
+ }
+ String header = buf.readSlice(headerIndex - buf.readerIndex()).toString(StandardCharsets.US_ASCII);
+
+ Parser parser = new Parser(PATTERN_HEADER, header);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String head = parser.next();
+ boolean reply = parser.next().equals("1");
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ String type = parser.next();
+ String subtype = parser.next();
+
+ if (reply && channel != null) {
+ channel.writeAndFlush(new NetworkMessage("*" + head + "Y" + type + subtype + "#", remoteAddress));
+ }
+
+ while (buf.isReadable()) {
+
+ buf.readByte(); // skip delimiter
+
+ byte dataType = buf.readByte();
+
+ int delimiterIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '&');
+ if (delimiterIndex < 0) {
+ delimiterIndex = buf.writerIndex();
+ }
+
+ ByteBuf data = buf.readSlice(delimiterIndex - buf.readerIndex());
+
+ switch (dataType) {
+ case 'A':
+ decodeLocation(position, data.toString(StandardCharsets.US_ASCII));
+ break;
+ case 'B':
+ position.set(Position.KEY_STATUS, data.toString(StandardCharsets.US_ASCII));
+ break;
+ case 'C':
+ long odometer = 0;
+ while (data.isReadable()) {
+ odometer <<= 4;
+ odometer += data.readByte() - (byte) '0';
+ }
+ position.set(Position.KEY_ODOMETER, odometer * 2 * 1852 / 3600);
+ break;
+ case 'F':
+ position.setSpeed(
+ Integer.parseInt(data.readSlice(4).toString(StandardCharsets.US_ASCII)) * 0.1);
+ break;
+ case 'K':
+ position.set("statusExtended", data.toString(StandardCharsets.US_ASCII));
+ break;
+ case 'P':
+ if (data.readableBytes() >= 16) {
+ position.setNetwork(new Network(CellTower.from(
+ Integer.parseInt(data.readSlice(4).toString(StandardCharsets.US_ASCII)),
+ Integer.parseInt(data.readSlice(4).toString(StandardCharsets.US_ASCII)),
+ Integer.parseInt(data.readSlice(4).toString(StandardCharsets.US_ASCII), 16),
+ Integer.parseInt(data.readSlice(4).toString(StandardCharsets.US_ASCII), 16))));
+ }
+ break;
+ case 'Q':
+ position.set("obdPid", ByteBufUtil.hexDump(data));
+ break;
+ case 'R':
+ if (head.startsWith("HQ")) {
+ position.set(Position.KEY_RSSI,
+ Integer.parseInt(data.readSlice(2).toString(StandardCharsets.US_ASCII)));
+ position.set(Position.KEY_SATELLITES,
+ Integer.parseInt(data.readSlice(2).toString(StandardCharsets.US_ASCII)));
+ } else {
+ position.set("odbTravel", ByteBufUtil.hexDump(data));
+ }
+ break;
+ case 'S':
+ position.set("obdTraffic", ByteBufUtil.hexDump(data));
+ break;
+ case 'T':
+ position.set(Position.KEY_BATTERY_LEVEL,
+ Integer.parseInt(data.readSlice(2).toString(StandardCharsets.US_ASCII)));
+ break;
+ case 'V':
+ position.set(Position.KEY_POWER,
+ Integer.parseInt(data.readSlice(4).toString(StandardCharsets.US_ASCII)) * 0.1);
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ if (position.getLatitude() != 0 && position.getLongitude() != 0) {
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/V680Protocol.java b/src/main/java/org/traccar/protocol/V680Protocol.java
new file mode 100644
index 000000000..dc0922cd4
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/V680Protocol.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class V680Protocol extends BaseProtocol {
+
+ public V680Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "##"));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new V680ProtocolDecoder(V680Protocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new V680ProtocolDecoder(V680Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/V680ProtocolDecoder.java b/src/main/java/org/traccar/protocol/V680ProtocolDecoder.java
new file mode 100644
index 000000000..0342404a6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/V680ProtocolDecoder.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class V680ProtocolDecoder extends BaseProtocolDecoder {
+
+ public V680ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .groupBegin()
+ .number("#(d+)#") // imei
+ .expression("([^#]*)#") // user
+ .groupEnd("?")
+ .number("(d+)#") // fix
+ .expression("([^#]+)#") // password
+ .expression("([^#]+)#") // event
+ .number("(d+)#") // packet number
+ .expression("([^#]+)?#?") // gsm base station
+ .expression("(?:[^#]+#)?")
+ .number("(d+.d+),([EW]),") // longitude
+ .number("(d+.d+),([NS]),") // latitude
+ .number("(d+.d+),") // speed
+ .number("(d+.?d*)?#") // course
+ .number("(dd)(dd)(dd)#") // date (ddmmyy)
+ .number("(dd)(dd)(dd)") // time (hhmmss)
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ sentence = sentence.trim();
+
+ if (sentence.length() == 16) {
+
+ getDeviceSession(channel, remoteAddress, sentence.substring(1, sentence.length()));
+
+ } else {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession;
+ if (parser.hasNext()) {
+ deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ } else {
+ deviceSession = getDeviceSession(channel, remoteAddress);
+ }
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set("user", parser.next());
+ position.setValid(parser.nextInt(0) > 0);
+ position.set("password", parser.next());
+ position.set(Position.KEY_EVENT, parser.next());
+ position.set("packet", parser.next());
+ position.set("lbsData", parser.next());
+
+ double lon = parser.nextDouble(0);
+ boolean west = parser.next().equals("W");
+ double lat = parser.nextDouble(0);
+ boolean south = parser.next().equals("S");
+
+ if (lat > 90 || lon > 180) {
+ int lonDegrees = (int) (lon * 0.01);
+ lon = (lon - lonDegrees * 100) / 60.0;
+ lon += lonDegrees;
+
+ int latDegrees = (int) (lat * 0.01);
+ lat = (lat - latDegrees * 100) / 60.0;
+ lat += latDegrees;
+ }
+
+ position.setLongitude(west ? -lon : lon);
+ position.setLatitude(south ? -lat : lat);
+
+ position.setSpeed(parser.nextDouble(0));
+ position.setCourse(parser.nextDouble(0));
+
+ int day = parser.nextInt(0);
+ int month = parser.nextInt(0);
+ if (day == 0 && month == 0) {
+ return null; // invalid date
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDate(parser.nextInt(0), month, day)
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ return position;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/VisiontekProtocol.java b/src/main/java/org/traccar/protocol/VisiontekProtocol.java
new file mode 100644
index 000000000..2c6af45a8
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/VisiontekProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class VisiontekProtocol extends BaseProtocol {
+
+ public VisiontekProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, '#'));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new VisiontekProtocolDecoder(VisiontekProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/VisiontekProtocolDecoder.java b/src/main/java/org/traccar/protocol/VisiontekProtocolDecoder.java
new file mode 100644
index 000000000..c4787bda2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/VisiontekProtocolDecoder.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2014 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class VisiontekProtocolDecoder extends BaseProtocolDecoder {
+
+ public VisiontekProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("$1,")
+ .expression("([^,]+),") // identifier
+ .number("(d+),").optional() // imei
+ .number("(dd),(dd),(dd),") // date (dd,mm,yy)
+ .number("(dd),(dd),(dd),") // time (hh,mm,ss)
+ .groupBegin()
+ .number("(dd)(dd).?(d+)([NS]),") // latitude
+ .number("(ddd)(dd).?(d+)([EW]),") // longitude
+ .or()
+ .number("(dd.d+)([NS]),") // latitude
+ .number("(ddd.d+)([EW]),") // longitude
+ .groupEnd()
+ .number("(d+.?d+),") // speed
+ .number("(d+),") // course
+ .groupBegin()
+ .number("(d+),") // altitude
+ .number("(d+),") // satellites
+ .number("(d+),") // odometer
+ .number("([01]),") // ignition
+ .number("([01]),") // input 1
+ .number("([01]),") // input 2
+ .number("([01]),") // immobilizer
+ .number("([01]),") // external battery status
+ .number("(d+),") // gsm
+ .or()
+ .number("(d+.d),") // hdop
+ .number("(d+),") // altitude
+ .number("(d+),") // odometer
+ .number("([01],[01],[01],[01]),") // input
+ .number("([01],[01],[01],[01]),") // output
+ .number("(d+.?d*),") // adc 1
+ .number("(d+.?d*),") // adc 2
+ .groupEnd("?")
+ .any()
+ .expression("([AV])") // validity
+ .number(",(d{10})").optional() // rfid
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next(), parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ if (parser.hasNext(8)) {
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_MIN_MIN_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_MIN_MIN_HEM));
+ }
+ if (parser.hasNext(4)) {
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ }
+
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(
+ parser.next().replace(".", "")) / 10));
+
+ position.setCourse(parser.nextDouble(0));
+
+ if (parser.hasNext(9)) {
+ position.setAltitude(parser.nextDouble(0));
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0) * 1000);
+ position.set(Position.KEY_IGNITION, parser.next().equals("1"));
+ position.set(Position.PREFIX_IO + 1, parser.next());
+ position.set(Position.PREFIX_IO + 2, parser.next());
+ position.set("immobilizer", parser.next());
+ position.set(Position.KEY_CHARGE, parser.next().equals("1"));
+ position.set(Position.KEY_RSSI, parser.nextDouble());
+ }
+
+ if (parser.hasNext(7)) {
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.setAltitude(parser.nextDouble(0));
+ position.set(Position.KEY_ODOMETER, parser.nextInt(0) * 1000);
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+ }
+
+ position.setValid(parser.next().equals("A"));
+
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Vt200FrameDecoder.java b/src/main/java/org/traccar/protocol/Vt200FrameDecoder.java
new file mode 100644
index 000000000..0fd83e715
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Vt200FrameDecoder.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+public class Vt200FrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ')') + 1;
+ if (endIndex > 0) {
+
+ ByteBuf frame = Unpooled.buffer();
+
+ while (buf.readerIndex() < endIndex) {
+ int b = buf.readByte();
+ if (b == '=') {
+ frame.writeByte(buf.readByte() ^ '=');
+ } else {
+ frame.writeByte(b);
+ }
+ }
+
+ return frame;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Vt200Protocol.java b/src/main/java/org/traccar/protocol/Vt200Protocol.java
new file mode 100644
index 000000000..2a9ef6ab5
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Vt200Protocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Vt200Protocol extends BaseProtocol {
+
+ public Vt200Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Vt200FrameDecoder());
+ pipeline.addLast(new Vt200ProtocolDecoder(Vt200Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Vt200ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Vt200ProtocolDecoder.java
new file mode 100644
index 000000000..b1564abd9
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Vt200ProtocolDecoder.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.BcdUtil;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.Date;
+
+public class Vt200ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Vt200ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static double decodeCoordinate(int value) {
+ int degrees = value / 1000000;
+ int minutes = value % 1000000;
+ return degrees + minutes * 0.0001 / 60;
+ }
+
+ protected Date decodeDate(ByteBuf buf) {
+ DateBuilder dateBuilder = new DateBuilder()
+ .setDateReverse(BcdUtil.readInteger(buf, 2), BcdUtil.readInteger(buf, 2), BcdUtil.readInteger(buf, 2))
+ .setTime(BcdUtil.readInteger(buf, 2), BcdUtil.readInteger(buf, 2), BcdUtil.readInteger(buf, 2));
+ return dateBuilder.getDate();
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(1); // header
+
+ String id = ByteBufUtil.hexDump(buf.readSlice(6));
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ int type = buf.readUnsignedShort();
+ buf.readUnsignedShort(); // length
+
+ if (type == 0x2086 || type == 0x2084 || type == 0x2082) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ buf.readUnsignedByte(); // data type
+ buf.readUnsignedShort(); // trip id
+
+ position.setTime(decodeDate(buf));
+
+ position.setLatitude(decodeCoordinate(BcdUtil.readInteger(buf, 8)));
+ position.setLongitude(decodeCoordinate(BcdUtil.readInteger(buf, 9)));
+
+ int flags = buf.readUnsignedByte();
+ position.setValid(BitUtil.check(flags, 0));
+ if (!BitUtil.check(flags, 1)) {
+ position.setLatitude(-position.getLatitude());
+ }
+ if (!BitUtil.check(flags, 2)) {
+ position.setLongitude(-position.getLongitude());
+ }
+
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.setCourse(buf.readUnsignedByte() * 2);
+
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ position.set(Position.KEY_RSSI, buf.readUnsignedByte());
+ position.set(Position.KEY_ODOMETER, buf.readUnsignedInt() * 1000);
+ position.set(Position.KEY_STATUS, buf.readUnsignedInt());
+
+ // additional data
+
+ return position;
+
+ } else if (type == 0x3088) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ buf.readUnsignedShort(); // trip id
+ buf.skipBytes(8); // imei
+ buf.skipBytes(8); // imsi
+
+ position.set("tripStart", decodeDate(buf).getTime());
+ position.set("tripEnd", decodeDate(buf).getTime());
+ position.set("drivingTime", buf.readUnsignedShort());
+
+ position.set(Position.KEY_FUEL_CONSUMPTION, buf.readUnsignedInt());
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedInt());
+
+ position.set("maxSpeed", UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ position.set("maxRpm", buf.readUnsignedShort());
+ position.set("maxTemp", buf.readUnsignedByte() - 40);
+ position.set("hardAccelerationCount", buf.readUnsignedByte());
+ position.set("hardBrakingCount", buf.readUnsignedByte());
+
+ for (String speedType : Arrays.asList("over", "high", "normal", "low")) {
+ position.set(speedType + "SpeedTime", buf.readUnsignedShort());
+ position.set(speedType + "SpeedDistance", buf.readUnsignedInt());
+ position.set(speedType + "SpeedFuel", buf.readUnsignedInt());
+ }
+
+ position.set("idleTime", buf.readUnsignedShort());
+ position.set("idleFuel", buf.readUnsignedInt());
+
+ position.set("hardCorneringCount", buf.readUnsignedByte());
+ position.set("overspeedCount", buf.readUnsignedByte());
+ position.set("overheatCount", buf.readUnsignedShort());
+ position.set("laneChangeCount", buf.readUnsignedByte());
+ position.set("emergencyRefueling", buf.readUnsignedByte());
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/VtfmsFrameDecoder.java b/src/main/java/org/traccar/protocol/VtfmsFrameDecoder.java
new file mode 100644
index 000000000..62a189960
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/VtfmsFrameDecoder.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+public class VtfmsFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ')');
+ if (endIndex > 0) {
+ endIndex += 1 + 3;
+ if (buf.writerIndex() >= endIndex) {
+ return buf.readRetainedSlice(endIndex - buf.readerIndex());
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/VtfmsProtocol.java b/src/main/java/org/traccar/protocol/VtfmsProtocol.java
new file mode 100644
index 000000000..2826a86e6
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/VtfmsProtocol.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import io.netty.handler.codec.string.StringDecoder;
+
+public class VtfmsProtocol extends BaseProtocol {
+
+ public VtfmsProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new VtfmsFrameDecoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new VtfmsProtocolDecoder(VtfmsProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new VtfmsProtocolDecoder(VtfmsProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/VtfmsProtocolDecoder.java b/src/main/java/org/traccar/protocol/VtfmsProtocolDecoder.java
new file mode 100644
index 000000000..17fac4311
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/VtfmsProtocolDecoder.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class VtfmsProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final String[] DIRECTIONS = new String[] {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
+
+ public VtfmsProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("(")
+ .number("(d{15}),") // imei
+ .number("[0-9A-Z]{3}dd,") // packet count
+ .number("(dd),") // packet id
+ .number("[^,]*,") // reserved
+ .number("(d+)?,") // rssi
+ .number("(?:d+)?,") // fix status
+ .number("(d+)?,") // satellites
+ .number("[^,]*,") // reserved
+ .expression("([AV]),") // validity
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(dd)(dd)(dd),") // time (ddmmyy)
+ .number("(-?d+.d+),") // latitude
+ .number("(-?d+.d+),") // longitude
+ .number("(?:(d+)|([NESW]{1,2})),") // course
+ .number("(d+),") // speed
+ .number("(d+),") // hours
+ .number("(d+),") // idle hours
+ .expression("[KNT],") // antenna status
+ .number("(d+),") // odometer
+ .expression("([01]),") // power status
+ .number("(d+.d+),") // power voltage
+ .number("[^,]*,") // reserved
+ .number("(d+)?,") // fuel level
+ .number("(d+.d+)?,") // adc 1
+ .number("[^,]*,") // reserved
+ .number("(d+.d+)?,") // adc 2
+ .expression("([01]),") // di 1
+ .expression("([01]),") // di 2
+ .expression("([01]),") // di 3
+ .expression("([01]),") // di 4
+ .expression("([01]),") // do 1
+ .expression("([01]),") // do 2
+ .expression("([01]),") // do 3
+ .number("[^,]*,") // reserved
+ .number("[^,]*") // reserved
+ .text(")")
+ .number("ddd") // checksum
+ .compile();
+
+ private String decodeAlarm(int value) {
+ switch (value) {
+ case 10:
+ return Position.ALARM_OVERSPEED;
+ case 14:
+ return Position.ALARM_POWER_CUT;
+ case 15:
+ return Position.ALARM_POWER_RESTORED;
+ case 32:
+ return Position.ALARM_BRAKING;
+ case 33:
+ return Position.ALARM_ACCELERATION;
+ default:
+ return null;
+ }
+ }
+
+ private double convertToDegrees(double value) {
+ double degrees = Math.floor(value / 100);
+ return degrees + (value - degrees * 100) / 60;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.set(Position.KEY_ALARM, decodeAlarm(parser.nextInt()));
+ position.set(Position.KEY_RSSI, parser.nextInt());
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+
+ position.setValid(parser.next().equals("A"));
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.HMS_DMY));
+
+ double latitude = parser.nextDouble();
+ double longitude = parser.nextDouble();
+ if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) {
+ position.setLatitude(convertToDegrees(latitude));
+ position.setLongitude(convertToDegrees(longitude));
+ } else {
+ position.setLatitude(latitude);
+ position.setLongitude(longitude);
+ }
+
+ position.setCourse(parser.nextDouble(0));
+ if (parser.hasNext()) {
+ String direction = parser.next();
+ for (int i = 0; i < DIRECTIONS.length; i++) {
+ if (direction.equals(DIRECTIONS[i])) {
+ position.setCourse(i * 45.0);
+ break;
+ }
+ }
+ }
+
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromHours(parser.nextInt()));
+ position.set("idleHours", parser.nextInt());
+ position.set(Position.KEY_ODOMETER, parser.nextInt() * 100);
+ position.set(Position.KEY_CHARGE, parser.next().equals("1"));
+ position.set(Position.KEY_POWER, parser.nextDouble());
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.PREFIX_ADC + 2, parser.nextDouble());
+ position.set(Position.PREFIX_IN + 1, parser.nextInt());
+ position.set(Position.PREFIX_IN + 2, parser.nextInt());
+ position.set(Position.PREFIX_IN + 3, parser.nextInt());
+ position.set(Position.PREFIX_IN + 4, parser.nextInt());
+ position.set(Position.PREFIX_OUT + 1, parser.nextInt());
+ position.set(Position.PREFIX_OUT + 2, parser.nextInt());
+ position.set(Position.PREFIX_OUT + 3, parser.nextInt());
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WatchFrameDecoder.java b/src/main/java/org/traccar/protocol/WatchFrameDecoder.java
new file mode 100644
index 000000000..f99bd52e2
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WatchFrameDecoder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import org.traccar.BaseFrameDecoder;
+
+public class WatchFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ int endIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ']') + 1;
+ if (endIndex > 0) {
+ ByteBuf frame = Unpooled.buffer();
+ while (buf.readerIndex() < endIndex) {
+ byte b1 = buf.readByte();
+ if (b1 == '}') {
+ byte b2 = buf.readByte();
+ switch (b2) {
+ case 0x01:
+ frame.writeByte('}');
+ break;
+ case 0x02:
+ frame.writeByte('[');
+ break;
+ case 0x03:
+ frame.writeByte(']');
+ break;
+ case 0x04:
+ frame.writeByte(',');
+ break;
+ case 0x05:
+ frame.writeByte('*');
+ break;
+ default:
+ throw new IllegalArgumentException(String.format(
+ "unexpected byte at %d: 0x%02x", buf.readerIndex() - 1, b2));
+ }
+ } else {
+ frame.writeByte(b1);
+ }
+ }
+ return frame;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WatchProtocol.java b/src/main/java/org/traccar/protocol/WatchProtocol.java
new file mode 100644
index 000000000..fe285e70d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WatchProtocol.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+public class WatchProtocol extends BaseProtocol {
+
+ public WatchProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_POSITION_PERIODIC,
+ Command.TYPE_SOS_NUMBER,
+ Command.TYPE_ALARM_SOS,
+ Command.TYPE_ALARM_BATTERY,
+ Command.TYPE_REBOOT_DEVICE,
+ Command.TYPE_POWER_OFF,
+ Command.TYPE_ALARM_REMOVE,
+ Command.TYPE_SILENCE_TIME,
+ Command.TYPE_ALARM_CLOCK,
+ Command.TYPE_SET_PHONEBOOK,
+ Command.TYPE_MESSAGE,
+ Command.TYPE_VOICE_MESSAGE,
+ Command.TYPE_SET_TIMEZONE,
+ Command.TYPE_SET_INDICATOR);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new WatchFrameDecoder());
+ pipeline.addLast(new WatchProtocolEncoder());
+ pipeline.addLast(new WatchProtocolDecoder(WatchProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WatchProtocolDecoder.java b/src/main/java/org/traccar/protocol/WatchProtocolDecoder.java
new file mode 100644
index 000000000..70b207e9b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WatchProtocolDecoder.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.BitUtil;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+import org.traccar.model.WifiAccessPoint;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class WatchProtocolDecoder extends BaseProtocolDecoder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WatchProtocolDecoder.class);
+
+ public WatchProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN_POSITION = new PatternBuilder()
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number(" *(-?d+.d+),") // latitude
+ .expression("([NS]),")
+ .number(" *(-?d+.d+),") // longitude
+ .expression("([EW])?,")
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(d+.?d*),") // altitude
+ .number("(d+),") // satellites
+ .number("(d+),") // rssi
+ .number("(d+),") // battery
+ .number("(d+),") // steps
+ .number("d+,") // tumbles
+ .number("(x+),") // status
+ .expression("(.*)") // cell and wifi
+ .compile();
+
+ private void sendResponse(Channel channel, String id, String index, String content) {
+ if (channel != null) {
+ String response;
+ if (index != null) {
+ response = String.format("[%s*%s*%s*%04x*%s]",
+ manufacturer, id, index, content.length(), content);
+ } else {
+ response = String.format("[%s*%s*%04x*%s]",
+ manufacturer, id, content.length(), content);
+ }
+ ByteBuf buf = Unpooled.copiedBuffer(response, StandardCharsets.US_ASCII);
+ channel.writeAndFlush(new NetworkMessage(buf, channel.remoteAddress()));
+ }
+ }
+
+ private String decodeAlarm(int status) {
+ if (BitUtil.check(status, 0)) {
+ return Position.ALARM_LOW_BATTERY;
+ } else if (BitUtil.check(status, 1)) {
+ return Position.ALARM_GEOFENCE_EXIT;
+ } else if (BitUtil.check(status, 2)) {
+ return Position.ALARM_GEOFENCE_ENTER;
+ } else if (BitUtil.check(status, 3)) {
+ return Position.ALARM_OVERSPEED;
+ } else if (BitUtil.check(status, 16)) {
+ return Position.ALARM_SOS;
+ } else if (BitUtil.check(status, 17)) {
+ return Position.ALARM_LOW_BATTERY;
+ } else if (BitUtil.check(status, 18)) {
+ return Position.ALARM_GEOFENCE_EXIT;
+ } else if (BitUtil.check(status, 19)) {
+ return Position.ALARM_GEOFENCE_ENTER;
+ } else if (BitUtil.check(status, 20)) {
+ return Position.ALARM_REMOVING;
+ } else if (BitUtil.check(status, 21)) {
+ return Position.ALARM_FALL_DOWN;
+ }
+ return null;
+ }
+
+ private Position decodePosition(DeviceSession deviceSession, String data) {
+
+ Parser parser = new Parser(PATTERN_POSITION, data);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.DEG_HEM));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt(0));
+ position.set(Position.KEY_RSSI, parser.nextInt(0));
+ position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt(0));
+
+ position.set(Position.KEY_STEPS, parser.nextInt(0));
+
+ int status = parser.nextHexInt(0);
+ position.set(Position.KEY_ALARM, decodeAlarm(status));
+ if (BitUtil.check(status, 4)) {
+ position.set(Position.KEY_MOTION, true);
+ }
+
+ String[] values = parser.next().split(",");
+ int index = 0;
+
+ Network network = new Network();
+
+ int cellCount = Integer.parseInt(values[index++]);
+ index += 1; // timing advance
+ int mcc = Integer.parseInt(values[index++]);
+ int mnc = Integer.parseInt(values[index++]);
+
+ for (int i = 0; i < cellCount; i++) {
+ network.addCellTower(CellTower.from(mcc, mnc,
+ Integer.parseInt(values[index++]), Integer.parseInt(values[index++]),
+ Integer.parseInt(values[index++])));
+ }
+
+ if (index < values.length && !values[index].isEmpty()) {
+ int wifiCount = Integer.parseInt(values[index++]);
+
+ for (int i = 0; i < wifiCount; i++) {
+ index += 1; // wifi name
+ network.addWifiAccessPoint(WifiAccessPoint.from(
+ values[index++], Integer.parseInt(values[index++])));
+ }
+ }
+
+ if (network.getCellTowers() != null || network.getWifiAccessPoints() != null) {
+ position.setNetwork(network);
+ }
+
+ return position;
+ }
+
+ private boolean hasIndex;
+ private String manufacturer;
+
+ public boolean getHasIndex() {
+ return hasIndex;
+ }
+
+ public String getManufacturer() {
+ return manufacturer;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ buf.skipBytes(1); // '[' header
+ manufacturer = buf.readSlice(2).toString(StandardCharsets.US_ASCII);
+ buf.skipBytes(1); // '*' delimiter
+
+ int idIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '*');
+ String id = buf.readSlice(idIndex - buf.readerIndex()).toString(StandardCharsets.US_ASCII);
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, id);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ buf.skipBytes(1); // '*' delimiter
+
+ String index = null;
+ int contentIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '*');
+ if (contentIndex + 5 < buf.writerIndex() && buf.getByte(contentIndex + 5) == '*'
+ && buf.toString(contentIndex + 1, 4, StandardCharsets.US_ASCII).matches("\\p{XDigit}+")) {
+ int indexLength = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) '*') - buf.readerIndex();
+ hasIndex = true;
+ index = buf.readSlice(indexLength).toString(StandardCharsets.US_ASCII);
+ buf.skipBytes(1); // '*' delimiter
+ }
+
+ buf.skipBytes(4); // length
+ buf.skipBytes(1); // '*' delimiter
+
+ buf.writerIndex(buf.writerIndex() - 1); // ']' ignore ending
+
+ contentIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ',');
+ if (contentIndex < 0) {
+ contentIndex = buf.writerIndex();
+ }
+
+ String type = buf.readSlice(contentIndex - buf.readerIndex()).toString(StandardCharsets.US_ASCII);
+
+ if (contentIndex < buf.writerIndex()) {
+ buf.readerIndex(contentIndex + 1);
+ }
+
+ if (type.equals("INIT")) {
+
+ sendResponse(channel, id, index, "INIT,1");
+
+ } else if (type.equals("LK")) {
+
+ sendResponse(channel, id, index, "LK");
+
+ if (buf.isReadable()) {
+ String[] values = buf.toString(StandardCharsets.US_ASCII).split(",");
+ if (values.length >= 3) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(values[2]));
+
+ return position;
+ }
+ }
+
+ } else if (type.equals("UD") || type.equals("UD2") || type.equals("UD3")
+ || type.equals("AL") || type.equals("WT")) {
+
+ Position position = decodePosition(deviceSession, buf.toString(StandardCharsets.US_ASCII));
+
+ if (type.equals("AL")) {
+ if (position != null) {
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ }
+ sendResponse(channel, id, index, "AL");
+ }
+
+ return position;
+
+ } else if (type.equals("TKQ")) {
+
+ sendResponse(channel, id, index, "TKQ");
+
+ } else if (type.equals("PULSE") || type.equals("heart") || type.equals("bphrt")) {
+
+ if (buf.isReadable()) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, new Date());
+
+ String[] values = buf.toString(StandardCharsets.US_ASCII).split(",");
+ int valueIndex = 0;
+
+ if (type.equals("bphrt")) {
+ position.set("pressureHigh", values[valueIndex++]);
+ position.set("pressureLow", values[valueIndex++]);
+ }
+ position.set(Position.KEY_HEART_RATE, Integer.parseInt(values[valueIndex]));
+
+ return position;
+
+ }
+
+ } else if (type.equals("img")) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ int timeIndex = buf.indexOf(buf.readerIndex(), buf.writerIndex(), (byte) ',');
+ buf.readerIndex(timeIndex + 12 + 2);
+ position.set(Position.KEY_IMAGE, Context.getMediaManager().writeFile(id, buf, "jpg"));
+
+ return position;
+
+ } else if (type.equals("TK")) {
+
+ if (buf.readableBytes() == 1) {
+ byte result = buf.readByte();
+ if (result != '1') {
+ LOGGER.warn(type + "," + result);
+ }
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_AUDIO, Context.getMediaManager().writeFile(id, buf, "amr"));
+
+ return position;
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WatchProtocolEncoder.java b/src/main/java/org/traccar/protocol/WatchProtocolEncoder.java
new file mode 100644
index 000000000..264aec81f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WatchProtocolEncoder.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.StringProtocolEncoder;
+import org.traccar.helper.DataConverter;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class WatchProtocolEncoder extends StringProtocolEncoder implements StringProtocolEncoder.ValueFormatter {
+
+ @Override
+ public String formatValue(String key, Object value) {
+ if (key.equals(Command.KEY_TIMEZONE)) {
+ double offset = TimeZone.getTimeZone((String) value).getRawOffset() / 3600000.0;
+ DecimalFormat fmt = new DecimalFormat("+#.##;-#.##", DecimalFormatSymbols.getInstance(Locale.US));
+ return fmt.format(offset);
+ } else if (key.equals(Command.KEY_MESSAGE)) {
+ return DataConverter.printHex(value.toString().getBytes(StandardCharsets.UTF_16BE));
+ } else if (key.equals(Command.KEY_ENABLE)) {
+ return (boolean) value ? "1" : "0";
+ }
+
+ return null;
+ }
+
+ protected ByteBuf formatTextCommand(Channel channel, Command command, String format, String... keys) {
+ String content = formatCommand(command, format, this, keys);
+ ByteBuf buf = Unpooled.copiedBuffer(content, StandardCharsets.US_ASCII);
+
+ return formatBinaryCommand(channel, command, "", buf);
+ }
+
+ protected ByteBuf formatBinaryCommand(Channel channel, Command command, String textPrefix, ByteBuf data) {
+ boolean hasIndex = false;
+ String manufacturer = "CS";
+ if (channel != null) {
+ WatchProtocolDecoder decoder = channel.pipeline().get(WatchProtocolDecoder.class);
+ if (decoder != null) {
+ hasIndex = decoder.getHasIndex();
+ manufacturer = decoder.getManufacturer();
+ }
+ }
+
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeByte('[');
+ buf.writeCharSequence(manufacturer, StandardCharsets.US_ASCII);
+ buf.writeByte('*');
+ buf.writeCharSequence(getUniqueId(command.getDeviceId()), StandardCharsets.US_ASCII);
+ buf.writeByte('*');
+ if (hasIndex) {
+ buf.writeCharSequence("0001", StandardCharsets.US_ASCII);
+ buf.writeByte('*');
+ }
+ buf.writeCharSequence(String.format("%04x", data.readableBytes() + textPrefix.length()),
+ StandardCharsets.US_ASCII);
+ buf.writeByte('*');
+ buf.writeCharSequence(textPrefix, StandardCharsets.US_ASCII);
+ buf.writeBytes(data);
+ buf.writeByte(']');
+
+ return buf;
+ }
+
+ private static Map<Byte, Byte> mapping = new HashMap<>();
+
+ static {
+ mapping.put((byte) 0x7d, (byte) 0x01);
+ mapping.put((byte) 0x5B, (byte) 0x02);
+ mapping.put((byte) 0x5D, (byte) 0x03);
+ mapping.put((byte) 0x2C, (byte) 0x04);
+ mapping.put((byte) 0x2A, (byte) 0x05);
+ }
+
+ private ByteBuf getBinaryData(Command command) {
+ byte[] data = DataConverter.parseHex(command.getString(Command.KEY_DATA));
+
+ int encodedLength = data.length;
+ for (byte b : data) {
+ if (mapping.containsKey(b)) {
+ encodedLength += 1;
+ }
+ }
+
+ int index = 0;
+ byte[] encodedData = new byte[encodedLength];
+
+ for (byte b : data) {
+ Byte replacement = mapping.get(b);
+ if (replacement != null) {
+ encodedData[index] = 0x7D;
+ index += 1;
+ encodedData[index] = replacement;
+ } else {
+ encodedData[index] = b;
+ }
+ index += 1;
+ }
+
+ return Unpooled.copiedBuffer(encodedData);
+ }
+
+ @Override
+ protected Object encodeCommand(Channel channel, Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatTextCommand(channel, command, command.getString(Command.KEY_DATA));
+ case Command.TYPE_POSITION_SINGLE:
+ return formatTextCommand(channel, command, "RG");
+ case Command.TYPE_SOS_NUMBER:
+ return formatTextCommand(channel, command, "SOS{%s},{%s}", Command.KEY_INDEX, Command.KEY_PHONE);
+ case Command.TYPE_ALARM_SOS:
+ return formatTextCommand(channel, command, "SOSSMS,{%s}", Command.KEY_ENABLE);
+ case Command.TYPE_ALARM_BATTERY:
+ return formatTextCommand(channel, command, "LOWBAT,{%s}", Command.KEY_ENABLE);
+ case Command.TYPE_REBOOT_DEVICE:
+ return formatTextCommand(channel, command, "RESET");
+ case Command.TYPE_POWER_OFF:
+ return formatTextCommand(channel, command, "POWEROFF");
+ case Command.TYPE_ALARM_REMOVE:
+ return formatTextCommand(channel, command, "REMOVE,{%s}", Command.KEY_ENABLE);
+ case Command.TYPE_SILENCE_TIME:
+ return formatTextCommand(channel, command, "SILENCETIME,{%s}", Command.KEY_DATA);
+ case Command.TYPE_ALARM_CLOCK:
+ return formatTextCommand(channel, command, "REMIND,{%s}", Command.KEY_DATA);
+ case Command.TYPE_SET_PHONEBOOK:
+ return formatTextCommand(channel, command, "PHB,{%s}", Command.KEY_DATA);
+ case Command.TYPE_MESSAGE:
+ return formatTextCommand(channel, command, "MESSAGE,{%s}", Command.KEY_MESSAGE);
+ case Command.TYPE_VOICE_MESSAGE:
+ return formatBinaryCommand(channel, command, "TK,", getBinaryData(command));
+ case Command.TYPE_POSITION_PERIODIC:
+ return formatTextCommand(channel, command, "UPLOAD,{%s}", Command.KEY_FREQUENCY);
+ case Command.TYPE_SET_TIMEZONE:
+ return formatTextCommand(channel, command, "LZ,,{%s}", Command.KEY_TIMEZONE);
+ case Command.TYPE_SET_INDICATOR:
+ return formatTextCommand(channel, command, "FLOWER,{%s}", Command.KEY_DATA);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WialonProtocol.java b/src/main/java/org/traccar/protocol/WialonProtocol.java
new file mode 100644
index 000000000..06b54dceb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WialonProtocol.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.Context;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+
+import java.nio.charset.StandardCharsets;
+public class WialonProtocol extends BaseProtocol {
+
+ public WialonProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_REBOOT_DEVICE,
+ Command.TYPE_SEND_USSD,
+ Command.TYPE_IDENTIFICATION,
+ Command.TYPE_OUTPUT_CONTROL);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(4 * 1024));
+ pipeline.addLast(new StringEncoder());
+ boolean utf8 = Context.getConfig().getBoolean(getName() + ".utf8");
+ if (utf8) {
+ pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
+ } else {
+ pipeline.addLast(new StringDecoder());
+ }
+ pipeline.addLast(new WialonProtocolEncoder());
+ pipeline.addLast(new WialonProtocolDecoder(WialonProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WialonProtocolDecoder.java b/src/main/java/org/traccar/protocol/WialonProtocolDecoder.java
new file mode 100644
index 000000000..de7073b67
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WialonProtocolDecoder.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class WialonProtocolDecoder extends BaseProtocolDecoder {
+
+ public WialonProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("(dd)(dd)(dd);") // date (ddmmyy)
+ .number("(dd)(dd)(dd);") // time (hhmmss)
+ .number("(dd)(dd.d+);") // latitude
+ .expression("([NS]);")
+ .number("(ddd)(dd.d+);") // longitude
+ .expression("([EW]);")
+ .number("(d+.?d*)?;") // speed
+ .number("(d+.?d*)?;") // course
+ .number("(?:NA|(-?d+.?d*));") // altitude
+ .number("(?:NA|(d+))") // satellites
+ .groupBegin().text(";")
+ .number("(?:NA|(d+.?d*));") // hdop
+ .number("(?:NA|(d+));") // inputs
+ .number("(?:NA|(d+));") // outputs
+ .expression("(?:NA|([^;]*));") // adc
+ .expression("(?:NA|([^;]*));") // ibutton
+ .expression("(?:NA|(.*))") // params
+ .groupEnd("?")
+ .compile();
+
+ private void sendResponse(Channel channel, SocketAddress remoteAddress, String prefix, Integer number) {
+ if (channel != null) {
+ StringBuilder response = new StringBuilder(prefix);
+ if (number != null) {
+ response.append(number);
+ }
+ response.append("\r\n");
+ channel.writeAndFlush(new NetworkMessage(response.toString(), remoteAddress));
+ }
+ }
+
+ private Position decodePosition(Channel channel, SocketAddress remoteAddress, String substring) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, substring);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime(Parser.DateTimeFormat.DMY_HMS));
+
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ if (parser.hasNext()) {
+ int satellites = parser.nextInt(0);
+ position.setValid(satellites >= 3);
+ position.set(Position.KEY_SATELLITES, satellites);
+ }
+
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+
+ if (parser.hasNext()) {
+ String[] values = parser.next().split(",");
+ for (int i = 0; i < values.length; i++) {
+ position.set(Position.PREFIX_ADC + (i + 1), values[i]);
+ }
+ }
+
+ position.set(Position.KEY_DRIVER_UNIQUE_ID, parser.next());
+
+ if (parser.hasNext()) {
+ String[] values = parser.next().split(",");
+ for (String param : values) {
+ Matcher paramParser = Pattern.compile("(.*):[1-3]:(.*)").matcher(param);
+ if (paramParser.matches()) {
+ try {
+ position.set(paramParser.group(1).toLowerCase(), Double.parseDouble(paramParser.group(2)));
+ } catch (NumberFormatException e) {
+ position.set(paramParser.group(1).toLowerCase(), paramParser.group(2));
+ }
+ }
+ }
+ }
+
+ return position;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ if (sentence.startsWith("#L#")) {
+
+ String[] values = sentence.substring(3).split(";");
+
+ String imei = values[0].indexOf('.') >= 0 ? values[1] : values[0];
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession != null) {
+ sendResponse(channel, remoteAddress, "#AL#", 1);
+ }
+
+ } else if (sentence.startsWith("#P#")) {
+
+ sendResponse(channel, remoteAddress, "#AP#", null); // heartbeat
+
+ } else if (sentence.startsWith("#SD#") || sentence.startsWith("#D#")) {
+
+ Position position = decodePosition(
+ channel, remoteAddress, sentence.substring(sentence.indexOf('#', 1) + 1));
+
+ if (position != null) {
+ sendResponse(channel, remoteAddress, "#AD#", 1);
+ return position;
+ }
+
+ } else if (sentence.startsWith("#B#")) {
+
+ String[] messages = sentence.substring(sentence.indexOf('#', 1) + 1).split("\\|");
+ List<Position> positions = new LinkedList<>();
+
+ for (String message : messages) {
+ Position position = decodePosition(channel, remoteAddress, message);
+ if (position != null) {
+ position.set(Position.KEY_ARCHIVE, true);
+ positions.add(position);
+ }
+ }
+
+ sendResponse(channel, remoteAddress, "#AB#", messages.length);
+ if (!positions.isEmpty()) {
+ return positions;
+ }
+
+ } else if (sentence.startsWith("#M#")) {
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+ if (deviceSession != null) {
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ getLastLocation(position, new Date());
+ position.setValid(false);
+ position.set(Position.KEY_RESULT, sentence.substring(sentence.indexOf('#', 1) + 1));
+ sendResponse(channel, remoteAddress, "#AM#", 1);
+ return position;
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WialonProtocolEncoder.java b/src/main/java/org/traccar/protocol/WialonProtocolEncoder.java
new file mode 100644
index 000000000..9ff1631eb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WialonProtocolEncoder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class WialonProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+ switch (command.getType()) {
+ case Command.TYPE_REBOOT_DEVICE:
+ return formatCommand(command, "reboot\r\n");
+ case Command.TYPE_SEND_USSD:
+ return formatCommand(command, "USSD:{%s}\r\n", Command.KEY_PHONE);
+ case Command.TYPE_IDENTIFICATION:
+ return formatCommand(command, "VER?\r\n");
+ case Command.TYPE_OUTPUT_CONTROL:
+ return formatCommand(command, "L{%s}={%s}\r\n", Command.KEY_INDEX, Command.KEY_DATA);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WondexFrameDecoder.java b/src/main/java/org/traccar/protocol/WondexFrameDecoder.java
new file mode 100644
index 000000000..39d83d761
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WondexFrameDecoder.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+import org.traccar.NetworkMessage;
+import org.traccar.helper.BufferUtil;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+public class WondexFrameDecoder extends BaseFrameDecoder {
+
+ private static final int KEEP_ALIVE_LENGTH = 8;
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < KEEP_ALIVE_LENGTH) {
+ return null;
+ }
+
+ if (buf.getUnsignedByte(buf.readerIndex()) == 0xD0) {
+
+ // Send response
+ ByteBuf frame = buf.readRetainedSlice(KEEP_ALIVE_LENGTH);
+ if (channel != null) {
+ frame.retain();
+ channel.writeAndFlush(new NetworkMessage(frame, channel.remoteAddress()));
+ }
+ return frame;
+
+ } else {
+
+ int index = BufferUtil.indexOf("\r\n", buf);
+ if (index != -1) {
+ ByteBuf frame = buf.readRetainedSlice(index - buf.readerIndex());
+ buf.skipBytes(2);
+ return frame;
+ }
+
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WondexProtocol.java b/src/main/java/org/traccar/protocol/WondexProtocol.java
new file mode 100644
index 000000000..8c6283d66
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WondexProtocol.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+import io.netty.handler.codec.string.StringEncoder;
+
+public class WondexProtocol extends BaseProtocol {
+
+ public WondexProtocol() {
+ setTextCommandEncoder(new WondexProtocolEncoder());
+ setSupportedCommands(
+ Command.TYPE_GET_DEVICE_STATUS,
+ Command.TYPE_GET_MODEM_STATUS,
+ Command.TYPE_REBOOT_DEVICE,
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_GET_VERSION,
+ Command.TYPE_IDENTIFICATION);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new WondexFrameDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new WondexProtocolEncoder());
+ pipeline.addLast(new WondexProtocolDecoder(WondexProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new WondexProtocolEncoder());
+ pipeline.addLast(new WondexProtocolDecoder(WondexProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WondexProtocolDecoder.java b/src/main/java/org/traccar/protocol/WondexProtocolDecoder.java
new file mode 100644
index 000000000..b85ae2656
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WondexProtocolDecoder.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class WondexProtocolDecoder extends BaseProtocolDecoder {
+
+ public WondexProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("[^d]*") // header
+ .number("(d+),") // device identifier
+ .number("(dddd)(dd)(dd)") // date (yyyymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("(-?d+.d+),") // longitude
+ .number("(-?d+.d+),") // latitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(-?d+.?d*),") // altitude
+ .number("(d+),") // satellites
+ .number("(d+),?") // event
+ .number("(d+.d+)V,").optional() // battery
+ .number("(d+.d+)?,?") // odometer
+ .number("(d+)?,?") // input
+ .number("(d+.d+)?,?") // adc1
+ .number("(d+.d+)?,?") // adc2
+ .number("(d+)?") // output
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ if (buf.getUnsignedByte(0) == 0xD0) {
+
+ long deviceId = ((Long.reverseBytes(buf.getLong(0))) >> 32) & 0xFFFFFFFFL;
+ getDeviceSession(channel, remoteAddress, String.valueOf(deviceId));
+
+ return null;
+ } else if (buf.toString(StandardCharsets.US_ASCII).startsWith("$OK:")
+ || buf.toString(StandardCharsets.US_ASCII).startsWith("$ERR:")
+ || buf.toString(StandardCharsets.US_ASCII).startsWith("$MSG:")) {
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress);
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+ getLastLocation(position, new Date());
+ position.set(Position.KEY_RESULT, buf.toString(StandardCharsets.US_ASCII));
+
+ return position;
+
+ } else {
+
+ Parser parser = new Parser(PATTERN, buf.toString(StandardCharsets.US_ASCII));
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLongitude(parser.nextDouble(0));
+ position.setLatitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+
+ int satellites = parser.nextInt(0);
+ position.setValid(satellites != 0);
+ position.set(Position.KEY_SATELLITES, satellites);
+
+ position.set(Position.KEY_EVENT, parser.next());
+ position.set(Position.KEY_BATTERY, parser.nextDouble());
+ if (parser.hasNext()) {
+ position.set(Position.KEY_ODOMETER, parser.nextDouble(0) * 1000);
+ }
+ position.set(Position.KEY_INPUT, parser.next());
+ position.set(Position.PREFIX_ADC + 1, parser.next());
+ position.set(Position.PREFIX_ADC + 2, parser.next());
+ position.set(Position.KEY_OUTPUT, parser.next());
+
+ return position;
+
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WondexProtocolEncoder.java b/src/main/java/org/traccar/protocol/WondexProtocolEncoder.java
new file mode 100644
index 000000000..f9e8eeb9b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WondexProtocolEncoder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class WondexProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ initDevicePassword(command, "0000");
+
+ switch (command.getType()) {
+ case Command.TYPE_REBOOT_DEVICE:
+ return formatCommand(command, "$WP+REBOOT={%s}", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_GET_DEVICE_STATUS:
+ return formatCommand(command, "$WP+TEST={%s}", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_GET_MODEM_STATUS:
+ return formatCommand(command, "$WP+GSMINFO={%s}", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_IDENTIFICATION:
+ return formatCommand(command, "$WP+IMEI={%s}", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_POSITION_SINGLE:
+ return formatCommand(command, "$WP+GETLOCATION={%s}", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_GET_VERSION:
+ return formatCommand(command, "$WP+VER={%s}", Command.KEY_DEVICE_PASSWORD);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WristbandProtocol.java b/src/main/java/org/traccar/protocol/WristbandProtocol.java
new file mode 100644
index 000000000..1e5ef2c01
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WristbandProtocol.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class WristbandProtocol extends BaseProtocol {
+
+ public WristbandProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 3, 2, 3, 0));
+ pipeline.addLast(new WristbandProtocolDecoder(WristbandProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/WristbandProtocolDecoder.java b/src/main/java/org/traccar/protocol/WristbandProtocolDecoder.java
new file mode 100644
index 000000000..7f2b0af85
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/WristbandProtocolDecoder.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.CellTower;
+import org.traccar.model.Network;
+import org.traccar.model.Position;
+import org.traccar.model.WifiAccessPoint;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class WristbandProtocolDecoder extends BaseProtocolDecoder {
+
+ public WristbandProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private void sendResponse(
+ Channel channel, String imei, String version, int type, String data) {
+
+ if (channel != null) {
+ String sentence = String.format("YX%s|%s|0|{F%02d#%s}\r\n", imei, version, type, data);
+ ByteBuf response = Unpooled.buffer();
+ if (type != 91) {
+ response.writeBytes(new byte[]{0x00, 0x01, 0x02});
+ response.writeShort(sentence.length());
+ }
+ response.writeCharSequence(sentence, StandardCharsets.US_ASCII);
+ if (type != 91) {
+ response.writeBytes(new byte[]{(byte) 0xFF, (byte) 0xFE, (byte) 0xFC});
+ }
+ channel.writeAndFlush(new NetworkMessage(response, channel.remoteAddress()));
+ }
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("..") // header
+ .number("(d+)|") // imei
+ .number("([vV]d+.d+)|") // version
+ .number("d+|") // model
+ .text("{")
+ .number("F(d+)") // function
+ .groupBegin()
+ .text("#")
+ .expression("(.*)") // data
+ .groupEnd("?")
+ .text("}")
+ .text("\r\n")
+ .compile();
+
+ private Position decodePosition(DeviceSession deviceSession, String sentence) throws ParseException {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ String[] values = sentence.split(",");
+
+ position.setValid(true);
+ position.setLongitude(Double.parseDouble(values[0]));
+ position.setLatitude(Double.parseDouble(values[1]));
+ position.setTime(new SimpleDateFormat("yyyyMMddHHmm").parse(values[2]));
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[3])));
+
+ return position;
+ }
+
+ private Position decodeStatus(DeviceSession deviceSession, String sentence) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(sentence.split(",")[0]));
+
+ return position;
+ }
+
+ private Position decodeNetwork(DeviceSession deviceSession, String sentence, boolean wifi) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ Network network = new Network();
+ String[] fragments = sentence.split("\\|");
+
+ if (wifi) {
+ for (String item : fragments[0].split("_")) {
+ String[] values = item.split(",");
+ network.addWifiAccessPoint(WifiAccessPoint.from(values[0], Integer.parseInt(values[1])));
+ }
+ }
+
+ for (String item : fragments[wifi ? 1 : 0].split(":")) {
+ String[] values = item.split(",");
+ int lac = Integer.parseInt(values[0]);
+ int mnc = Integer.parseInt(values[1]);
+ int mcc = Integer.parseInt(values[2]);
+ int cid = Integer.parseInt(values[3]);
+ int rssi = Integer.parseInt(values[4]);
+ network.addCellTower(CellTower.from(mcc, mnc, lac, cid, rssi));
+ }
+
+ position.setNetwork(network);
+
+ return position;
+ }
+
+ private List<Position> decodeMessage(
+ Channel channel, SocketAddress remoteAddress, String sentence) throws ParseException {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ String imei = parser.next();
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, imei);
+ if (deviceSession == null) {
+ return null;
+ }
+
+ String version = parser.next();
+ int type = parser.nextInt();
+
+ List<Position> positions = new LinkedList<>();
+ String data = parser.next();
+
+ switch (type) {
+ case 90:
+ sendResponse(channel, imei, version, type, getServer(channel, ','));
+ break;
+ case 91:
+ String time = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date());
+ sendResponse(channel, imei, version, type, time + "|" + getServer(channel, ','));
+ break;
+ case 1:
+ positions.add(decodeStatus(deviceSession, data));
+ sendResponse(channel, imei, version, type, data.split(",")[1]);
+ break;
+ case 2:
+ for (String fragment : data.split("\\|")) {
+ positions.add(decodePosition(deviceSession, fragment));
+ }
+ break;
+ case 3:
+ case 4:
+ positions.add(decodeNetwork(deviceSession, data, type == 3));
+ break;
+ case 64:
+ sendResponse(channel, imei, version, type, data);
+ break;
+ default:
+ break;
+ }
+
+ return positions.isEmpty() ? null : positions;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+ buf.skipBytes(3); // header
+ buf.readUnsignedShort(); // length
+
+ String sentence = buf.toString(buf.readerIndex(), buf.readableBytes() - 3, StandardCharsets.US_ASCII);
+
+ buf.skipBytes(3); // footer
+
+ return decodeMessage(channel, remoteAddress, sentence);
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/XexunFrameDecoder.java b/src/main/java/org/traccar/protocol/XexunFrameDecoder.java
new file mode 100644
index 000000000..114e94061
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/XexunFrameDecoder.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseFrameDecoder;
+import org.traccar.helper.BufferUtil;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+
+public class XexunFrameDecoder extends BaseFrameDecoder {
+
+ @Override
+ protected Object decode(
+ ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception {
+
+ if (buf.readableBytes() < 80) {
+ return null;
+ }
+
+ int beginIndex = BufferUtil.indexOf("GPRMC", buf);
+ if (beginIndex == -1) {
+ beginIndex = BufferUtil.indexOf("GNRMC", buf);
+ if (beginIndex == -1) {
+ return null;
+ }
+ }
+
+ int identifierIndex = BufferUtil.indexOf("imei:", buf, beginIndex, buf.writerIndex());
+ if (identifierIndex == -1) {
+ return null;
+ }
+
+ int endIndex = buf.indexOf(identifierIndex, buf.writerIndex(), (byte) ',');
+ if (endIndex == -1) {
+ return null;
+ }
+
+ buf.skipBytes(beginIndex - buf.readerIndex());
+
+ return buf.readRetainedSlice(endIndex - beginIndex + 1);
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/XexunProtocol.java b/src/main/java/org/traccar/protocol/XexunProtocol.java
new file mode 100644
index 000000000..0005270fb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/XexunProtocol.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.Context;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+
+public class XexunProtocol extends BaseProtocol {
+
+ public XexunProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_ENGINE_STOP,
+ Command.TYPE_ENGINE_RESUME);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ boolean full = Context.getConfig().getBoolean(getName() + ".extended");
+ if (full) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024)); // tracker bug \n\r
+ } else {
+ pipeline.addLast(new XexunFrameDecoder());
+ }
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new XexunProtocolEncoder());
+ pipeline.addLast(new XexunProtocolDecoder(XexunProtocol.this, full));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/XexunProtocolDecoder.java b/src/main/java/org/traccar/protocol/XexunProtocolDecoder.java
new file mode 100644
index 000000000..5e2d0c05e
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/XexunProtocolDecoder.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class XexunProtocolDecoder extends BaseProtocolDecoder {
+
+ private final boolean full;
+
+ public XexunProtocolDecoder(Protocol protocol, boolean full) {
+ super(protocol);
+ this.full = full;
+ }
+
+ private static final Pattern PATTERN_BASIC = new PatternBuilder()
+ .expression("G[PN]RMC,")
+ .number("(?:(dd)(dd)(dd))?.?d*,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(d*?)(d?d.d+),([NS]),") // latitude
+ .number("(d*?)(d?d.d+),([EW])?,") // longitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*)?,") // course
+ .number("(?:(dd)(dd)(dd))?,") // date (ddmmyy)
+ .expression("[^*]*").text("*")
+ .number("xx") // checksum
+ .expression("\\r\\n").optional()
+ .expression(",([FL]),") // signal
+ .expression("([^,]*),").optional() // alarm
+ .any()
+ .number("imei:(d+),") // imei
+ .compile();
+
+ private static final Pattern PATTERN_FULL = new PatternBuilder()
+ .any()
+ .number("(d+),") // serial
+ .expression("([^,]+)?,") // phone number
+ .expression(PATTERN_BASIC.pattern())
+ .number("(d+),") // satellites
+ .number("(-?d+.d+)?,") // altitude
+ .number("[FL]:(d+.d+)V") // power
+ .any()
+ .compile();
+
+ private String decodeStatus(Position position, String value) {
+ if (value != null) {
+ switch (value.toLowerCase()) {
+ case "acc on":
+ case "accstart":
+ position.set(Position.KEY_IGNITION, true);
+ break;
+ case "acc off":
+ case "accstop":
+ position.set(Position.KEY_IGNITION, false);
+ break;
+ case "help me!":
+ position.set(Position.KEY_ALARM, Position.ALARM_SOS);
+ break;
+ case "low battery":
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ case "move!":
+ case "moved!":
+ position.set(Position.KEY_ALARM, Position.ALARM_MOVEMENT);
+ break;
+ default:
+ break;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Pattern pattern = PATTERN_BASIC;
+ if (full) {
+ pattern = PATTERN_FULL;
+ }
+
+ Parser parser = new Parser(pattern, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ if (full) {
+ position.set("serial", parser.next());
+ position.set("number", parser.next());
+ }
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ position.setSpeed(convertSpeed(parser.nextDouble(0), "kn"));
+
+ position.setCourse(parser.nextDouble(0));
+
+ dateBuilder.setDateReverse(parser.nextInt(0), parser.nextInt(0), parser.nextInt(0));
+ position.setTime(dateBuilder.getDate());
+
+ position.set("signal", parser.next());
+
+ decodeStatus(position, parser.next());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ if (full) {
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+
+ position.setAltitude(parser.nextDouble(0));
+
+ position.set(Position.KEY_POWER, parser.nextDouble(0));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/XexunProtocolEncoder.java b/src/main/java/org/traccar/protocol/XexunProtocolEncoder.java
new file mode 100644
index 000000000..515cfbbd0
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/XexunProtocolEncoder.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class XexunProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ initDevicePassword(command, "123456");
+
+ switch (command.getType()) {
+ case Command.TYPE_ENGINE_STOP:
+ return formatCommand(command, "powercar{%s} 11", Command.KEY_DEVICE_PASSWORD);
+ case Command.TYPE_ENGINE_RESUME:
+ return formatCommand(command, "powercar{%s} 00", Command.KEY_DEVICE_PASSWORD);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/XirgoProtocol.java b/src/main/java/org/traccar/protocol/XirgoProtocol.java
new file mode 100644
index 000000000..4979fda5d
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/XirgoProtocol.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.CharacterDelimiterFrameDecoder;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+
+public class XirgoProtocol extends BaseProtocol {
+
+ public XirgoProtocol() {
+ setSupportedDataCommands(
+ Command.TYPE_OUTPUT_CONTROL);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new CharacterDelimiterFrameDecoder(1024, "##"));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new XirgoProtocolEncoder());
+ pipeline.addLast(new XirgoProtocolDecoder(XirgoProtocol.this));
+ }
+ });
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new XirgoProtocolEncoder());
+ pipeline.addLast(new XirgoProtocolDecoder(XirgoProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/XirgoProtocolDecoder.java b/src/main/java/org/traccar/protocol/XirgoProtocolDecoder.java
new file mode 100644
index 000000000..6d215e672
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/XirgoProtocolDecoder.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class XirgoProtocolDecoder extends BaseProtocolDecoder {
+
+ private Boolean newFormat;
+ private String form;
+
+ public XirgoProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ form = Context.getConfig().getString(getProtocolName() + ".form");
+ }
+
+ public void setForm(String form) {
+ this.form = form;
+ }
+
+ private static final Pattern PATTERN_OLD = new PatternBuilder()
+ .text("$$")
+ .number("(d+),") // imei
+ .number("(d+),") // event
+ .number("(dddd)/(dd)/(dd),") // date (yyyy/mm/dd)
+ .number("(dd):(dd):(dd),") // time (hh:mm:ss)
+ .number("(-?d+.?d*),") // latitude
+ .number("(-?d+.?d*),") // longitude
+ .number("(-?d+.?d*),") // altitude
+ .number("(d+.?d*),") // speed
+ .number("(d+.?d*),") // course
+ .number("(d+),") // satellites
+ .number("(d+.?d*),") // hdop
+ .number("(d+.d+),") // battery
+ .number("(d+),") // gsm
+ .number("(d+.?d*),") // odometer
+ .number("(d+),") // gps
+ .any()
+ .compile();
+
+ private static final Pattern PATTERN_NEW = new PatternBuilder()
+ .text("$$")
+ .number("(d+),") // imei
+ .number("(d+),") // event
+ .number("(dddd)/(dd)/(dd),") // date (yyyy/mm/dd)
+ .number("(dd):(dd):(dd),") // time (hh:mm:ss)
+ .number("(-?d+.?d*),") // latitude
+ .number("(-?d+.?d*),") // longitude
+ .number("(-?d+.?d*),") // altitude
+ .number("(d+.?d*),") // speed
+ .number("d+.?d*,") // acceleration
+ .number("d+.?d*,") // deceleration
+ .number("d+,")
+ .number("(d+.?d*),") // course
+ .number("(d+),") // satellites
+ .number("(d+.?d*),") // hdop
+ .number("(d+.?d*),") // odometer
+ .number("(d+.?d*),") // fuel consumption
+ .number("(d+.d+),") // battery
+ .number("(d+),") // gsm
+ .number("(d+),") // gps
+ .groupBegin()
+ .number("d,") // reset mode
+ .expression("([01])") // input 1
+ .expression("([01])") // input 1
+ .expression("([01])") // input 1
+ .expression("([01]),") // output 1
+ .number("(d+.?d*),") // adc 1
+ .number("(d+.?d*),") // fuel level
+ .number("d+,") // engine load
+ .number("(d+),") // engine hours
+ .number("(d+),") // oil pressure
+ .number("(d+),") // oil level
+ .number("(-?d+),") // oil temperature
+ .number("(d+),") // coolant pressure
+ .number("(d+),") // coolant level
+ .number("(-?d+)") // coolant temperature
+ .groupEnd("?")
+ .any()
+ .compile();
+
+ private void decodeEvent(Position position, int event) {
+
+ position.set(Position.KEY_EVENT, event);
+
+ switch (event) {
+ case 4001:
+ case 4003:
+ case 6011:
+ case 6013:
+ position.set(Position.KEY_IGNITION, true);
+ break;
+ case 4002:
+ case 4004:
+ case 6012:
+ case 6014:
+ position.set(Position.KEY_IGNITION, false);
+ break;
+ case 4005:
+ position.set(Position.KEY_CHARGE, false);
+ break;
+ case 6002:
+ position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED);
+ break;
+ case 6006:
+ position.set(Position.KEY_ALARM, Position.ALARM_ACCELERATION);
+ break;
+ case 6007:
+ position.set(Position.KEY_ALARM, Position.ALARM_BRAKING);
+ break;
+ case 6008:
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_POWER);
+ break;
+ case 6009:
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_CUT);
+ break;
+ case 6010:
+ position.set(Position.KEY_ALARM, Position.ALARM_POWER_RESTORED);
+ break;
+ case 6016:
+ position.set(Position.KEY_ALARM, Position.ALARM_IDLE);
+ break;
+ case 6017:
+ position.set(Position.KEY_ALARM, Position.ALARM_TOW);
+ break;
+ case 6030:
+ case 6071:
+ position.set(Position.KEY_MOTION, true);
+ break;
+ case 6031:
+ position.set(Position.KEY_MOTION, false);
+ break;
+ case 6032:
+ position.set(Position.KEY_ALARM, Position.ALARM_PARKING);
+ break;
+ case 6090:
+ position.set(Position.KEY_ALARM, Position.ALARM_REMOVING);
+ break;
+ case 6091:
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+ if (form != null) {
+ return decodeCustom(channel, remoteAddress, sentence);
+ } else {
+ return decodeFixed(channel, remoteAddress, sentence);
+ }
+ }
+
+
+ private Object decodeCustom(
+ Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ String[] keys = form.split(",");
+ String[] values = sentence.replace("$$", "").replace("##", "").split(",");
+
+ Position position = new Position(getProtocolName());
+ DateBuilder dateBuilder = new DateBuilder();
+
+ for (int i = 0; i < Math.min(keys.length, values.length); i++) {
+ switch (keys[i]) {
+ case "UID":
+ case "IM":
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, values[i]);
+ if (deviceSession != null) {
+ position.setDeviceId(deviceSession.getDeviceId());
+ }
+ break;
+ case "EV":
+ decodeEvent(position, Integer.parseInt(values[i]));
+ break;
+ case "D":
+ String[] date = values[i].split("/");
+ dateBuilder.setMonth(Integer.parseInt(date[0]));
+ dateBuilder.setDay(Integer.parseInt(date[1]));
+ dateBuilder.setYear(Integer.parseInt(date[2]));
+ break;
+ case "T":
+ String[] time = values[i].split(":");
+ dateBuilder.setHour(Integer.parseInt(time[0]));
+ dateBuilder.setMinute(Integer.parseInt(time[1]));
+ dateBuilder.setSecond(Integer.parseInt(time[2]));
+ break;
+ case "LT":
+ position.setLatitude(Double.parseDouble(values[i]));
+ break;
+ case "LN":
+ position.setLongitude(Double.parseDouble(values[i]));
+ break;
+ case "AL":
+ position.setAltitude(Integer.parseInt(values[i]));
+ break;
+ case "GSPT":
+ position.setSpeed(UnitsConverter.knotsFromKph(Double.parseDouble(values[i])));
+ break;
+ case "HD":
+ if (values[i].contains(".")) {
+ position.setCourse(Double.parseDouble(values[i]));
+ } else {
+ position.setCourse(Integer.parseInt(values[i]) * 0.1);
+ }
+ break;
+ case "SV":
+ position.set(Position.KEY_SATELLITES, Integer.parseInt(values[i]));
+ break;
+ case "BV":
+ position.set(Position.KEY_BATTERY, Double.parseDouble(values[i]));
+ break;
+ case "CQ":
+ position.set(Position.KEY_RSSI, Integer.parseInt(values[i]));
+ break;
+ case "MI":
+ position.set(Position.KEY_ODOMETER, Integer.parseInt(values[i]));
+ break;
+ case "GS":
+ position.setValid(Integer.parseInt(values[i]) == 3);
+ break;
+ case "SI":
+ position.set("iccid", values[i]);
+ break;
+ case "IG":
+ int ignition = Integer.parseInt(values[i]);
+ if (ignition > 0) {
+ position.set(Position.KEY_IGNITION, ignition == 1);
+ }
+ break;
+ case "OT":
+ position.set(Position.KEY_OUTPUT, Integer.parseInt(values[i]));
+ break;
+ default:
+ break;
+ }
+ }
+
+ position.setTime(dateBuilder.getDate());
+
+ return position.getDeviceId() > 0 ? position : null;
+ }
+
+ private Object decodeFixed(
+ Channel channel, SocketAddress remoteAddress, String sentence) {
+
+ Parser parser;
+ if (newFormat == null) {
+ parser = new Parser(PATTERN_NEW, sentence);
+ if (parser.matches()) {
+ newFormat = true;
+ } else {
+ parser = new Parser(PATTERN_OLD, sentence);
+ if (parser.matches()) {
+ newFormat = false;
+ } else {
+ return null;
+ }
+ }
+ } else {
+ if (newFormat) {
+ parser = new Parser(PATTERN_NEW, sentence);
+ } else {
+ parser = new Parser(PATTERN_OLD, sentence);
+ }
+ if (!parser.matches()) {
+ return null;
+ }
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ decodeEvent(position, parser.nextInt());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromMph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ if (newFormat) {
+ position.set(Position.KEY_ODOMETER, UnitsConverter.metersFromMiles(parser.nextDouble(0)));
+ position.set(Position.KEY_FUEL_CONSUMPTION, parser.next());
+ }
+
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0));
+ position.set(Position.KEY_RSSI, parser.nextDouble());
+
+ if (!newFormat) {
+ position.set(Position.KEY_ODOMETER, UnitsConverter.metersFromMiles(parser.nextDouble(0)));
+ }
+
+ position.setValid(parser.nextInt(0) == 1);
+
+ if (newFormat && parser.hasNext(13)) {
+ position.set(Position.PREFIX_IN + 1, parser.nextInt());
+ position.set(Position.PREFIX_IN + 2, parser.nextInt());
+ position.set(Position.PREFIX_IN + 3, parser.nextInt());
+ position.set(Position.PREFIX_OUT + 1, parser.nextInt());
+ position.set(Position.PREFIX_ADC + 1, parser.nextDouble());
+ position.set(Position.KEY_FUEL_LEVEL, parser.nextDouble());
+ position.set(Position.KEY_HOURS, UnitsConverter.msFromHours(parser.nextInt()));
+ position.set("oilPressure", parser.nextInt());
+ position.set("oilLevel", parser.nextInt());
+ position.set("oilTemp", parser.nextInt());
+ position.set("coolantPressure", parser.nextInt());
+ position.set("coolantLevel", parser.nextInt());
+ position.set("coolantTemp", parser.nextInt());
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/XirgoProtocolEncoder.java b/src/main/java/org/traccar/protocol/XirgoProtocolEncoder.java
new file mode 100644
index 000000000..dd5e30cca
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/XirgoProtocolEncoder.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.StringProtocolEncoder;
+import org.traccar.model.Command;
+
+public class XirgoProtocolEncoder extends StringProtocolEncoder {
+
+ @Override
+ protected Object encodeCommand(Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_OUTPUT_CONTROL:
+ return String.format("+XT:7005,%d,1", command.getInteger(Command.KEY_DATA) + 1);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Xrb28Protocol.java b/src/main/java/org/traccar/protocol/Xrb28Protocol.java
new file mode 100644
index 000000000..b1f1c34fb
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Xrb28Protocol.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+import org.traccar.model.Command;
+
+import java.nio.charset.StandardCharsets;
+
+public class Xrb28Protocol extends BaseProtocol {
+
+ public Xrb28Protocol() {
+ setSupportedDataCommands(
+ Command.TYPE_CUSTOM,
+ Command.TYPE_POSITION_SINGLE,
+ Command.TYPE_POSITION_PERIODIC,
+ Command.TYPE_ALARM_ARM,
+ Command.TYPE_ALARM_DISARM);
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder(StandardCharsets.ISO_8859_1));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new Xrb28ProtocolEncoder());
+ pipeline.addLast(new Xrb28ProtocolDecoder(Xrb28Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Xrb28ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Xrb28ProtocolDecoder.java
new file mode 100644
index 000000000..938394d6b
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Xrb28ProtocolDecoder.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.DateBuilder;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.model.Command;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Xrb28ProtocolDecoder extends BaseProtocolDecoder {
+
+ private String pendingCommand;
+
+ public void setPendingCommand(String pendingCommand) {
+ this.pendingCommand = pendingCommand;
+ }
+
+ public Xrb28ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .text("*")
+ .expression("....,")
+ .expression("..,") // vendor
+ .number("d{15},") // imei
+ .expression("..,") // type
+ .number("0,") // reserved
+ .number("(dd)(dd)(dd).d+,") // time (hhmmss)
+ .expression("([AV]),") // validity
+ .number("(dd)(dd.d+),") // latitude
+ .expression("([NS]),")
+ .number("(d{2,3})(dd.d+),") // longitude
+ .expression("([EW]),")
+ .number("(d+),") // satellites
+ .number("(d+.d+),") // hdop
+ .number("(dd)(dd)(dd),") // date (ddmmyy)
+ .number("(-?d+.?d*),") // altitude
+ .expression(".,") // height unit
+ .expression(".#") // mode
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, sentence.substring(9, 24));
+ if (deviceSession == null) {
+ return null;
+ }
+
+ String type = sentence.substring(25, 27);
+ if (channel != null) {
+ if (type.matches("L0|L1|W0|E1")) {
+ channel.write(new NetworkMessage(
+ "\u00ff\u00ff*SCOS" + sentence.substring(5, 27) + "#\n",
+ remoteAddress));
+ } else if (type.equals("R0") && pendingCommand != null) {
+ String command = pendingCommand.equals(Command.TYPE_ALARM_ARM) ? "L1," : "L0,";
+ channel.write(new NetworkMessage(
+ "\u00ff\u00ff*SCOS" + sentence.substring(5, 25) + command + sentence.substring(30) + "\n",
+ remoteAddress));
+ pendingCommand = null;
+ }
+ }
+
+ if (!type.startsWith("D")) {
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ getLastLocation(position, null);
+
+ String payload = sentence.substring(25, sentence.length() - 1);
+
+ int index = 0;
+ String[] values = payload.substring(3).split(",");
+
+ switch (type) {
+ case "Q0":
+ position.set(Position.KEY_BATTERY, Integer.parseInt(values[index++]) * 0.01);
+ position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(values[index++]));
+ position.set(Position.KEY_RSSI, Integer.parseInt(values[index++]));
+ break;
+ case "H0":
+ position.set(Position.KEY_BLOCKED, Integer.parseInt(values[index++]) > 0);
+ position.set(Position.KEY_BATTERY, Integer.parseInt(values[index++]) * 0.01);
+ position.set(Position.KEY_RSSI, Integer.parseInt(values[index++]));
+ position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(values[index++]));
+ break;
+ case "W0":
+ switch (Integer.parseInt(values[index++])) {
+ case 1:
+ position.set(Position.KEY_ALARM, Position.ALARM_MOVEMENT);
+ break;
+ case 2:
+ position.set(Position.KEY_ALARM, Position.ALARM_FALL_DOWN);
+ break;
+ case 3:
+ position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY);
+ break;
+ default:
+ break;
+ }
+ break;
+ case "E0":
+ position.set(Position.KEY_ALARM, Position.ALARM_FAULT);
+ position.set("error", Integer.parseInt(values[index++]));
+ break;
+ case "S1":
+ position.set(Position.KEY_EVENT, Integer.parseInt(values[index++]));
+ break;
+ case "R0":
+ case "L0":
+ case "L1":
+ case "S4":
+ case "S5":
+ case "S6":
+ case "S7":
+ case "V0":
+ case "G0":
+ case "K0":
+ case "I0":
+ case "M0":
+ position.set(Position.KEY_RESULT, payload);
+ break;
+ default:
+ break;
+ }
+
+ return !position.getAttributes().isEmpty() ? position : null;
+
+ } else {
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ DateBuilder dateBuilder = new DateBuilder()
+ .setTime(parser.nextInt(), parser.nextInt(), parser.nextInt());
+
+ position.setValid(parser.next().equals("A"));
+ position.setLatitude(parser.nextCoordinate());
+ position.setLongitude(parser.nextCoordinate());
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_HDOP, parser.nextDouble());
+
+ dateBuilder.setDateReverse(parser.nextInt(), parser.nextInt(), parser.nextInt());
+ position.setTime(dateBuilder.getDate());
+
+ position.setAltitude(parser.nextDouble());
+
+ return position;
+
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Xrb28ProtocolEncoder.java b/src/main/java/org/traccar/protocol/Xrb28ProtocolEncoder.java
new file mode 100644
index 000000000..617639312
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Xrb28ProtocolEncoder.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolEncoder;
+import org.traccar.model.Command;
+
+public class Xrb28ProtocolEncoder extends BaseProtocolEncoder {
+
+ private String formatCommand(Command command, String content) {
+ return String.format("\u00ff\u00ff*SCOS,OM,%s,%s#\n", getUniqueId(command.getDeviceId()), content);
+ }
+
+ @Override
+ protected Object encodeCommand(Channel channel, Command command) {
+
+ switch (command.getType()) {
+ case Command.TYPE_CUSTOM:
+ return formatCommand(command, command.getString(Command.KEY_DATA));
+ case Command.TYPE_POSITION_SINGLE:
+ return formatCommand(command, "D0");
+ case Command.TYPE_POSITION_PERIODIC:
+ return formatCommand(command, "D1," + command.getInteger(Command.KEY_FREQUENCY));
+ case Command.TYPE_ENGINE_STOP:
+ case Command.TYPE_ALARM_DISARM:
+ if (channel != null) {
+ Xrb28ProtocolDecoder decoder = channel.pipeline().get(Xrb28ProtocolDecoder.class);
+ if (decoder != null) {
+ decoder.setPendingCommand(command.getType());
+ }
+ }
+ return formatCommand(command, "R0,0,20,1234," + System.currentTimeMillis() / 1000);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Xt013Protocol.java b/src/main/java/org/traccar/protocol/Xt013Protocol.java
new file mode 100644
index 000000000..ebb3c123f
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Xt013Protocol.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+
+public class Xt013Protocol extends BaseProtocol {
+
+ public Xt013Protocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new Xt013ProtocolDecoder(Xt013Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Xt013ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Xt013ProtocolDecoder.java
new file mode 100644
index 000000000..f49fb9563
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Xt013ProtocolDecoder.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class Xt013ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Xt013ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .number("HI,d+").optional()
+ .text("TK,")
+ .number("(d+),") // imei
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .number("([+-]d+.d+),") // latitude
+ .number("([+-]d+.d+),") // longitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("d+,")
+ .number("(d+),") // altitude
+ .expression("([FL]),") // gps fix
+ .number("d+,")
+ .number("(d+),") // gps level
+ .number("x+,")
+ .number("x+,")
+ .number("(d+),") // gsm level
+ .expression("[^,]*,")
+ .number("(d+.d+),") // battery
+ .number("(d),") // charging
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ Parser parser = new Parser(PATTERN, (String) msg);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLatitude(parser.nextDouble(0));
+ position.setLongitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble(0)));
+ position.setCourse(parser.nextDouble(0));
+ position.setAltitude(parser.nextDouble(0));
+ position.setValid(parser.next().equals("F"));
+
+ position.set(Position.KEY_SATELLITES, parser.nextInt());
+ position.set(Position.KEY_RSSI, parser.nextDouble());
+ position.set(Position.KEY_BATTERY, parser.nextDouble(0));
+ position.set(Position.KEY_CHARGE, parser.next().equals("1"));
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Xt2400Protocol.java b/src/main/java/org/traccar/protocol/Xt2400Protocol.java
new file mode 100644
index 000000000..9427876c8
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Xt2400Protocol.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class Xt2400Protocol extends BaseProtocol {
+
+ public Xt2400Protocol() {
+ addServer(new TrackerServer(true, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new Xt2400ProtocolDecoder(Xt2400Protocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/Xt2400ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Xt2400ProtocolDecoder.java
new file mode 100644
index 000000000..819011a50
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/Xt2400ProtocolDecoder.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.Context;
+import org.traccar.DeviceSession;
+import org.traccar.Protocol;
+import org.traccar.helper.DataConverter;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Xt2400ProtocolDecoder extends BaseProtocolDecoder {
+
+ public Xt2400ProtocolDecoder(Protocol protocol) {
+ super(protocol);
+
+ String config = Context.getConfig().getString(getProtocolName() + ".config");
+ if (config != null) {
+ setConfig(config);
+ }
+ }
+
+ private static final Map<Integer, Integer> TAG_LENGTH_MAP = new HashMap<>();
+
+ static {
+ int[] l1 = {
+ 0x01, 0x02, 0x04, 0x0b, 0x0c, 0x0d, 0x12, 0x13,
+ 0x16, 0x17, 0x1c, 0x1f, 0x23, 0x2c, 0x2d, 0x30,
+ 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
+ 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x40, 0x41,
+ 0x53, 0x66, 0x69, 0x6a, 0x93, 0x94, 0x96
+ };
+ int[] l2 = {
+ 0x05, 0x09, 0x0a, 0x14, 0x15, 0x1d, 0x1e, 0x24,
+ 0x26, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
+ 0x49, 0x57, 0x58, 0x59, 0x5a, 0x6b, 0x6f, 0x7A,
+ 0x7B, 0x7C, 0x7d, 0x7E, 0x7F, 0x80, 0x81, 0x82,
+ 0x83, 0x84, 0x85, 0x86
+ };
+ int[] l4 = {
+ 0x03, 0x06, 0x07, 0x08, 0x0e, 0x0f, 0x10, 0x11,
+ 0x18, 0x19, 0x1a, 0x1b, 0x20, 0x21, 0x22, 0x2e,
+ 0x2f, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50,
+ 0x51, 0x52, 0x54, 0x55, 0x56, 0x5b, 0x5c, 0x5d,
+ 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x68, 0x6e, 0x71,
+ 0x72, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x87,
+ 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d
+ };
+ for (int i : l1) {
+ TAG_LENGTH_MAP.put(i, 1);
+ }
+ for (int i : l2) {
+ TAG_LENGTH_MAP.put(i, 2);
+ }
+ for (int i : l4) {
+ TAG_LENGTH_MAP.put(i, 4);
+ }
+ TAG_LENGTH_MAP.put(0x95, 24);
+ }
+
+ private static int getTagLength(int tag) {
+ Integer length = TAG_LENGTH_MAP.get(tag);
+ if (length == null) {
+ throw new IllegalArgumentException("Unknown tag: " + tag);
+ }
+ return length;
+ }
+
+ private Map<Short, byte[]> formats = new HashMap<>();
+
+ public void setConfig(String configString) {
+ Pattern pattern = Pattern.compile(":wycfg pcr\\[\\d+\\] ([0-9a-fA-F]{2})[0-9a-fA-F]{2}([0-9a-fA-F]+)");
+ Matcher matcher = pattern.matcher(configString);
+ while (matcher.find()) {
+ formats.put(Short.parseShort(matcher.group(1), 16), DataConverter.parseHex(matcher.group(2)));
+ }
+ }
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ ByteBuf buf = (ByteBuf) msg;
+
+ byte[] format = null;
+ if (formats.size() > 1) {
+ format = formats.get(buf.getUnsignedByte(buf.readerIndex()));
+ } else if (!formats.isEmpty()) {
+ format = formats.values().iterator().next();
+ }
+
+ if (format == null) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ for (byte tag : format) {
+ switch (tag) {
+ case 0x03:
+ DeviceSession deviceSession = getDeviceSession(
+ channel, remoteAddress, String.valueOf(buf.readUnsignedInt()));
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+ break;
+ case 0x04:
+ position.set(Position.KEY_EVENT, buf.readUnsignedByte());
+ break;
+ case 0x05:
+ position.set(Position.KEY_INDEX, buf.readUnsignedShort());
+ break;
+ case 0x06:
+ position.setTime(new Date(buf.readUnsignedInt() * 1000));
+ break;
+ case 0x07:
+ position.setLatitude(buf.readInt() * 0.000001);
+ break;
+ case 0x08:
+ position.setLongitude(buf.readInt() * 0.000001);
+ break;
+ case 0x09:
+ position.setAltitude(buf.readShort() * 0.1);
+ break;
+ case 0x0a:
+ position.setCourse(buf.readShort() * 0.1);
+ break;
+ case 0x0b:
+ position.setSpeed(UnitsConverter.knotsFromKph(buf.readUnsignedByte()));
+ break;
+ case 0x10:
+ position.set(Position.KEY_ODOMETER_TRIP, buf.readUnsignedInt());
+ break;
+ case 0x12:
+ position.set(Position.KEY_HDOP, buf.readUnsignedByte() * 0.1);
+ break;
+ case 0x13:
+ position.set(Position.KEY_SATELLITES, buf.readUnsignedByte());
+ break;
+ case 0x14:
+ position.set(Position.KEY_RSSI, buf.readShort());
+ break;
+ case 0x16:
+ position.set(Position.KEY_BATTERY, buf.readUnsignedByte() * 0.1);
+ break;
+ case 0x17:
+ position.set(Position.KEY_POWER, buf.readUnsignedByte() * 0.1);
+ break;
+ case 0x57:
+ position.set(Position.KEY_OBD_SPEED, UnitsConverter.knotsFromKph(buf.readUnsignedShort()));
+ break;
+ case 0x65:
+ position.set(Position.KEY_VIN, buf.readSlice(17).toString(StandardCharsets.US_ASCII));
+ break;
+ case 0x73:
+ position.set(Position.KEY_VERSION_FW, buf.readSlice(16).toString(StandardCharsets.US_ASCII).trim());
+ break;
+ default:
+ buf.skipBytes(getTagLength(tag));
+ break;
+ }
+ }
+
+ if (position.getLatitude() != 0 && position.getLongitude() != 0) {
+ position.setValid(true);
+ } else {
+ getLastLocation(position, position.getDeviceTime());
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/YwtProtocol.java b/src/main/java/org/traccar/protocol/YwtProtocol.java
new file mode 100644
index 000000000..c525b75cf
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/YwtProtocol.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.handler.codec.LineBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import org.traccar.BaseProtocol;
+import org.traccar.PipelineBuilder;
+import org.traccar.TrackerServer;
+
+public class YwtProtocol extends BaseProtocol {
+
+ public YwtProtocol() {
+ addServer(new TrackerServer(false, getName()) {
+ @Override
+ protected void addProtocolHandlers(PipelineBuilder pipeline) {
+ pipeline.addLast(new LineBasedFrameDecoder(1024));
+ pipeline.addLast(new StringEncoder());
+ pipeline.addLast(new StringDecoder());
+ pipeline.addLast(new YwtProtocolDecoder(YwtProtocol.this));
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/protocol/YwtProtocolDecoder.java b/src/main/java/org/traccar/protocol/YwtProtocolDecoder.java
new file mode 100644
index 000000000..bf5a23fa7
--- /dev/null
+++ b/src/main/java/org/traccar/protocol/YwtProtocolDecoder.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2013 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.protocol;
+
+import io.netty.channel.Channel;
+import org.traccar.BaseProtocolDecoder;
+import org.traccar.DeviceSession;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.helper.Parser;
+import org.traccar.helper.PatternBuilder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.util.regex.Pattern;
+
+public class YwtProtocolDecoder extends BaseProtocolDecoder {
+
+ public YwtProtocolDecoder(Protocol protocol) {
+ super(protocol);
+ }
+
+ private static final Pattern PATTERN = new PatternBuilder()
+ .expression("%(..),") // type
+ .number("(d+):") // unit identifier
+ .number("d+,") // subtype
+ .number("(dd)(dd)(dd)") // date (yymmdd)
+ .number("(dd)(dd)(dd),") // time (hhmmss)
+ .expression("([EW])")
+ .number("(ddd.d{6}),") // longitude
+ .expression("([NS])")
+ .number("(dd.d{6}),") // latitude
+ .number("(d+)?,") // altitude
+ .number("(d+),") // speed
+ .number("(d+),") // course
+ .number("(d+),") // satellite
+ .expression("([^,]+),") // report identifier
+ .expression("([-0-9a-fA-F]+)") // status
+ .any()
+ .compile();
+
+ @Override
+ protected Object decode(
+ Channel channel, SocketAddress remoteAddress, Object msg) throws Exception {
+
+ String sentence = (String) msg;
+
+ // Synchronization
+ if (sentence.startsWith("%SN") && channel != null) {
+ int start = sentence.indexOf(':');
+ int end = start;
+ for (int i = 0; i < 4; i++) {
+ end = sentence.indexOf(',', end + 1);
+ }
+ if (end == -1) {
+ end = sentence.length();
+ }
+
+ channel.writeAndFlush(new NetworkMessage("%AT+SN=" + sentence.substring(start, end), remoteAddress));
+ return null;
+ }
+
+ Parser parser = new Parser(PATTERN, sentence);
+ if (!parser.matches()) {
+ return null;
+ }
+
+ Position position = new Position(getProtocolName());
+
+ String type = parser.next();
+
+ DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, parser.next());
+ if (deviceSession == null) {
+ return null;
+ }
+ position.setDeviceId(deviceSession.getDeviceId());
+
+ position.setTime(parser.nextDateTime());
+
+ position.setLongitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setLatitude(parser.nextCoordinate(Parser.CoordinateFormat.HEM_DEG));
+ position.setAltitude(parser.nextDouble(0));
+ position.setSpeed(UnitsConverter.knotsFromKph(parser.nextDouble()));
+ position.setCourse(parser.nextDouble());
+
+ int satellites = parser.nextInt();
+ position.setValid(satellites != 0);
+ position.set(Position.KEY_SATELLITES, satellites);
+
+ String reportId = parser.next();
+
+ position.set(Position.KEY_STATUS, parser.next());
+
+ // Send response
+ if ((type.equals("KP") || type.equals("EP")) && channel != null) {
+ channel.writeAndFlush(new NetworkMessage("%AT+" + type + "=" + reportId + "\r\n", remoteAddress));
+ }
+
+ return position;
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/Events.java b/src/main/java/org/traccar/reports/Events.java
new file mode 100644
index 000000000..66d9e708d
--- /dev/null
+++ b/src/main/java/org/traccar/reports/Events.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Geofence;
+import org.traccar.model.Group;
+import org.traccar.model.Maintenance;
+import org.traccar.reports.model.DeviceReport;
+
+public final class Events {
+
+ private Events() {
+ }
+
+ public static Collection<Event> getObjects(long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Collection<String> types, Date from, Date to) throws SQLException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<Event> result = new ArrayList<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ Collection<Event> events = Context.getDataManager().getEvents(deviceId, from, to);
+ boolean all = types.isEmpty() || types.contains(Event.ALL_EVENTS);
+ for (Event event : events) {
+ if (all || types.contains(event.getType())) {
+ long geofenceId = event.getGeofenceId();
+ long maintenanceId = event.getMaintenanceId();
+ if ((geofenceId == 0 || Context.getGeofenceManager().checkItemPermission(userId, geofenceId))
+ && (maintenanceId == 0
+ || Context.getMaintenancesManager().checkItemPermission(userId, maintenanceId))) {
+ result.add(event);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public static void getExcel(OutputStream outputStream,
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Collection<String> types, Date from, Date to) throws SQLException, IOException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<DeviceReport> devicesEvents = new ArrayList<>();
+ ArrayList<String> sheetNames = new ArrayList<>();
+ HashMap<Long, String> geofenceNames = new HashMap<>();
+ HashMap<Long, String> maintenanceNames = new HashMap<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ Collection<Event> events = Context.getDataManager().getEvents(deviceId, from, to);
+ boolean all = types.isEmpty() || types.contains(Event.ALL_EVENTS);
+ for (Iterator<Event> iterator = events.iterator(); iterator.hasNext();) {
+ Event event = iterator.next();
+ if (all || types.contains(event.getType())) {
+ long geofenceId = event.getGeofenceId();
+ long maintenanceId = event.getMaintenanceId();
+ if (geofenceId != 0) {
+ if (Context.getGeofenceManager().checkItemPermission(userId, geofenceId)) {
+ Geofence geofence = Context.getGeofenceManager().getById(geofenceId);
+ if (geofence != null) {
+ geofenceNames.put(geofenceId, geofence.getName());
+ }
+ } else {
+ iterator.remove();
+ }
+ } else if (maintenanceId != 0) {
+ if (Context.getMaintenancesManager().checkItemPermission(userId, maintenanceId)) {
+ Maintenance maintenance = Context.getMaintenancesManager().getById(maintenanceId);
+ if (maintenance != null) {
+ maintenanceNames.put(maintenanceId, maintenance.getName());
+ }
+ } else {
+ iterator.remove();
+ }
+ }
+ } else {
+ iterator.remove();
+ }
+ }
+ DeviceReport deviceEvents = new DeviceReport();
+ Device device = Context.getIdentityManager().getById(deviceId);
+ deviceEvents.setDeviceName(device.getName());
+ sheetNames.add(WorkbookUtil.createSafeSheetName(deviceEvents.getDeviceName()));
+ if (device.getGroupId() != 0) {
+ Group group = Context.getGroupsManager().getById(device.getGroupId());
+ if (group != null) {
+ deviceEvents.setGroupName(group.getName());
+ }
+ }
+ deviceEvents.setObjects(events);
+ devicesEvents.add(deviceEvents);
+ }
+ String templatePath = Context.getConfig().getString("report.templatesPath",
+ "templates/export/");
+ try (InputStream inputStream = new FileInputStream(templatePath + "/events.xlsx")) {
+ org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
+ jxlsContext.putVar("devices", devicesEvents);
+ jxlsContext.putVar("sheetNames", sheetNames);
+ jxlsContext.putVar("geofenceNames", geofenceNames);
+ jxlsContext.putVar("maintenanceNames", maintenanceNames);
+ jxlsContext.putVar("from", from);
+ jxlsContext.putVar("to", to);
+ ReportUtils.processTemplateWithSheets(inputStream, outputStream, jxlsContext);
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/reports/ReportUtils.java b/src/main/java/org/traccar/reports/ReportUtils.java
new file mode 100644
index 000000000..3a631e0d9
--- /dev/null
+++ b/src/main/java/org/traccar/reports/ReportUtils.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports;
+
+import org.apache.velocity.tools.generic.DateTool;
+import org.apache.velocity.tools.generic.NumberTool;
+import org.jxls.area.Area;
+import org.jxls.builder.xls.XlsCommentAreaBuilder;
+import org.jxls.common.CellRef;
+import org.jxls.formula.StandardFormulaProcessor;
+import org.jxls.transform.Transformer;
+import org.jxls.transform.poi.PoiTransformer;
+import org.jxls.util.TransformerFactory;
+import org.traccar.Context;
+import org.traccar.database.DeviceManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.handler.events.MotionEventHandler;
+import org.traccar.model.DeviceState;
+import org.traccar.model.Driver;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.reports.model.BaseReport;
+import org.traccar.reports.model.StopReport;
+import org.traccar.reports.model.TripReport;
+import org.traccar.reports.model.TripsConfig;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+public final class ReportUtils {
+
+ private ReportUtils() {
+ }
+
+ public static void checkPeriodLimit(Date from, Date to) {
+ long limit = Context.getConfig().getLong("report.periodLimit") * 1000;
+ if (limit > 0 && to.getTime() - from.getTime() > limit) {
+ throw new IllegalArgumentException("Time period exceeds the limit");
+ }
+ }
+
+ public static String getDistanceUnit(long userId) {
+ return (String) Context.getPermissionsManager().lookupAttribute(userId, "distanceUnit", "km");
+ }
+
+ public static String getSpeedUnit(long userId) {
+ return (String) Context.getPermissionsManager().lookupAttribute(userId, "speedUnit", "kn");
+ }
+
+ public static String getVolumeUnit(long userId) {
+ return (String) Context.getPermissionsManager().lookupAttribute(userId, "volumeUnit", "ltr");
+ }
+
+ public static TimeZone getTimezone(long userId) {
+ String timezone = (String) Context.getPermissionsManager().lookupAttribute(userId, "timezone", null);
+ return timezone != null ? TimeZone.getTimeZone(timezone) : TimeZone.getDefault();
+ }
+
+ public static Collection<Long> getDeviceList(Collection<Long> deviceIds, Collection<Long> groupIds) {
+ Collection<Long> result = new ArrayList<>();
+ result.addAll(deviceIds);
+ for (long groupId : groupIds) {
+ result.addAll(Context.getPermissionsManager().getGroupDevices(groupId));
+ }
+ return result;
+ }
+
+ public static double calculateDistance(Position firstPosition, Position lastPosition) {
+ return calculateDistance(firstPosition, lastPosition, true);
+ }
+
+ public static double calculateDistance(Position firstPosition, Position lastPosition, boolean useOdometer) {
+ double distance = 0.0;
+ double firstOdometer = firstPosition.getDouble(Position.KEY_ODOMETER);
+ double lastOdometer = lastPosition.getDouble(Position.KEY_ODOMETER);
+
+ if (useOdometer && (firstOdometer != 0.0 || lastOdometer != 0.0)) {
+ distance = lastOdometer - firstOdometer;
+ } else if (firstPosition.getAttributes().containsKey(Position.KEY_TOTAL_DISTANCE)
+ && lastPosition.getAttributes().containsKey(Position.KEY_TOTAL_DISTANCE)) {
+ distance = lastPosition.getDouble(Position.KEY_TOTAL_DISTANCE)
+ - firstPosition.getDouble(Position.KEY_TOTAL_DISTANCE);
+ }
+ return distance;
+ }
+
+ public static double calculateFuel(Position firstPosition, Position lastPosition) {
+
+ if (firstPosition.getAttributes().get(Position.KEY_FUEL_LEVEL) != null
+ && lastPosition.getAttributes().get(Position.KEY_FUEL_LEVEL) != null) {
+
+ BigDecimal value = new BigDecimal(firstPosition.getDouble(Position.KEY_FUEL_LEVEL)
+ - lastPosition.getDouble(Position.KEY_FUEL_LEVEL));
+ return value.setScale(1, RoundingMode.HALF_EVEN).doubleValue();
+ }
+ return 0;
+ }
+
+ public static String findDriver(Position firstPosition, Position lastPosition) {
+ if (firstPosition.getAttributes().containsKey(Position.KEY_DRIVER_UNIQUE_ID)) {
+ return firstPosition.getString(Position.KEY_DRIVER_UNIQUE_ID);
+ } else if (lastPosition.getAttributes().containsKey(Position.KEY_DRIVER_UNIQUE_ID)) {
+ return lastPosition.getString(Position.KEY_DRIVER_UNIQUE_ID);
+ }
+ return null;
+ }
+
+ public static String findDriverName(String driverUniqueId) {
+ if (driverUniqueId != null && Context.getDriversManager() != null) {
+ Driver driver = Context.getDriversManager().getDriverByUniqueId(driverUniqueId);
+ if (driver != null) {
+ return driver.getName();
+ }
+ }
+ return null;
+ }
+
+ public static org.jxls.common.Context initializeContext(long userId) {
+ org.jxls.common.Context jxlsContext = PoiTransformer.createInitialContext();
+ jxlsContext.putVar("distanceUnit", getDistanceUnit(userId));
+ jxlsContext.putVar("speedUnit", getSpeedUnit(userId));
+ jxlsContext.putVar("volumeUnit", getVolumeUnit(userId));
+ jxlsContext.putVar("webUrl", Context.getVelocityEngine().getProperty("web.url"));
+ jxlsContext.putVar("dateTool", new DateTool());
+ jxlsContext.putVar("numberTool", new NumberTool());
+ jxlsContext.putVar("timezone", getTimezone(userId));
+ jxlsContext.putVar("locale", Locale.getDefault());
+ jxlsContext.putVar("bracketsRegex", "[\\{\\}\"]");
+ return jxlsContext;
+ }
+
+ public static void processTemplateWithSheets(
+ InputStream templateStream, OutputStream targetStream,
+ org.jxls.common.Context jxlsContext) throws IOException {
+
+ Transformer transformer = TransformerFactory.createTransformer(templateStream, targetStream);
+ List<Area> xlsAreas = new XlsCommentAreaBuilder(transformer).build();
+ for (Area xlsArea : xlsAreas) {
+ xlsArea.applyAt(new CellRef(xlsArea.getStartCellRef().getCellName()), jxlsContext);
+ xlsArea.setFormulaProcessor(new StandardFormulaProcessor());
+ xlsArea.processFormulas();
+ }
+ transformer.deleteSheet(xlsAreas.get(0).getStartCellRef().getSheetName());
+ transformer.write();
+ }
+
+ private static TripReport calculateTrip(
+ ArrayList<Position> positions, int startIndex, int endIndex, boolean ignoreOdometer) {
+
+ Position startTrip = positions.get(startIndex);
+ Position endTrip = positions.get(endIndex);
+
+ double speedMax = 0.0;
+ double speedSum = 0.0;
+ for (int i = startIndex; i <= endIndex; i++) {
+ double speed = positions.get(i).getSpeed();
+ speedSum += speed;
+ if (speed > speedMax) {
+ speedMax = speed;
+ }
+ }
+
+ TripReport trip = new TripReport();
+
+ long tripDuration = endTrip.getFixTime().getTime() - startTrip.getFixTime().getTime();
+ long deviceId = startTrip.getDeviceId();
+ trip.setDeviceId(deviceId);
+ trip.setDeviceName(Context.getIdentityManager().getById(deviceId).getName());
+
+ trip.setStartPositionId(startTrip.getId());
+ trip.setStartLat(startTrip.getLatitude());
+ trip.setStartLon(startTrip.getLongitude());
+ trip.setStartTime(startTrip.getFixTime());
+ String startAddress = startTrip.getAddress();
+ if (startAddress == null && Context.getGeocoder() != null
+ && Context.getConfig().getBoolean("geocoder.onRequest")) {
+ startAddress = Context.getGeocoder().getAddress(startTrip.getLatitude(), startTrip.getLongitude(), null);
+ }
+ trip.setStartAddress(startAddress);
+
+ trip.setEndPositionId(endTrip.getId());
+ trip.setEndLat(endTrip.getLatitude());
+ trip.setEndLon(endTrip.getLongitude());
+ trip.setEndTime(endTrip.getFixTime());
+ String endAddress = endTrip.getAddress();
+ if (endAddress == null && Context.getGeocoder() != null
+ && Context.getConfig().getBoolean("geocoder.onRequest")) {
+ endAddress = Context.getGeocoder().getAddress(endTrip.getLatitude(), endTrip.getLongitude(), null);
+ }
+ trip.setEndAddress(endAddress);
+
+ trip.setDistance(calculateDistance(startTrip, endTrip, !ignoreOdometer));
+ trip.setDuration(tripDuration);
+ trip.setAverageSpeed(speedSum / (endIndex - startIndex));
+ trip.setMaxSpeed(speedMax);
+ trip.setSpentFuel(calculateFuel(startTrip, endTrip));
+
+ trip.setDriverUniqueId(findDriver(startTrip, endTrip));
+ trip.setDriverName(findDriverName(trip.getDriverUniqueId()));
+
+ if (!ignoreOdometer
+ && startTrip.getDouble(Position.KEY_ODOMETER) != 0
+ && endTrip.getDouble(Position.KEY_ODOMETER) != 0) {
+ trip.setStartOdometer(startTrip.getDouble(Position.KEY_ODOMETER));
+ trip.setEndOdometer(endTrip.getDouble(Position.KEY_ODOMETER));
+ } else {
+ trip.setStartOdometer(startTrip.getDouble(Position.KEY_TOTAL_DISTANCE));
+ trip.setEndOdometer(endTrip.getDouble(Position.KEY_TOTAL_DISTANCE));
+ }
+
+ return trip;
+ }
+
+ private static StopReport calculateStop(
+ ArrayList<Position> positions, int startIndex, int endIndex, boolean ignoreOdometer) {
+
+ Position startStop = positions.get(startIndex);
+ Position endStop = positions.get(endIndex);
+
+ StopReport stop = new StopReport();
+
+ long deviceId = startStop.getDeviceId();
+ stop.setDeviceId(deviceId);
+ stop.setDeviceName(Context.getIdentityManager().getById(deviceId).getName());
+
+ stop.setPositionId(startStop.getId());
+ stop.setLatitude(startStop.getLatitude());
+ stop.setLongitude(startStop.getLongitude());
+ stop.setStartTime(startStop.getFixTime());
+ String address = startStop.getAddress();
+ if (address == null && Context.getGeocoder() != null
+ && Context.getConfig().getBoolean("geocoder.onRequest")) {
+ address = Context.getGeocoder().getAddress(stop.getLatitude(), stop.getLongitude(), null);
+ }
+ stop.setAddress(address);
+
+ stop.setEndTime(endStop.getFixTime());
+
+ long stopDuration = endStop.getFixTime().getTime() - startStop.getFixTime().getTime();
+ stop.setDuration(stopDuration);
+ stop.setSpentFuel(calculateFuel(startStop, endStop));
+
+ long engineHours = 0;
+ if (startStop.getAttributes().containsKey(Position.KEY_HOURS)
+ && endStop.getAttributes().containsKey(Position.KEY_HOURS)) {
+ engineHours = endStop.getLong(Position.KEY_HOURS) - startStop.getLong(Position.KEY_HOURS);
+ } else if (Context.getConfig().getBoolean("processing.engineHours.enable")) {
+ // Temporary fallback for old data, to be removed in May 2019
+ for (int i = startIndex + 1; i <= endIndex; i++) {
+ if (positions.get(i).getBoolean(Position.KEY_IGNITION)
+ && positions.get(i - 1).getBoolean(Position.KEY_IGNITION)) {
+ engineHours += positions.get(i).getFixTime().getTime()
+ - positions.get(i - 1).getFixTime().getTime();
+ }
+ }
+ }
+ stop.setEngineHours(engineHours);
+
+ if (!ignoreOdometer
+ && startStop.getDouble(Position.KEY_ODOMETER) != 0
+ && endStop.getDouble(Position.KEY_ODOMETER) != 0) {
+ stop.setStartOdometer(startStop.getDouble(Position.KEY_ODOMETER));
+ stop.setEndOdometer(endStop.getDouble(Position.KEY_ODOMETER));
+ } else {
+ stop.setStartOdometer(startStop.getDouble(Position.KEY_TOTAL_DISTANCE));
+ stop.setEndOdometer(endStop.getDouble(Position.KEY_TOTAL_DISTANCE));
+ }
+
+ return stop;
+
+ }
+
+ private static <T extends BaseReport> T calculateTripOrStop(
+ ArrayList<Position> positions, int startIndex, int endIndex, boolean ignoreOdometer, Class<T> reportClass) {
+
+ if (reportClass.equals(TripReport.class)) {
+ return (T) calculateTrip(positions, startIndex, endIndex, ignoreOdometer);
+ } else {
+ return (T) calculateStop(positions, startIndex, endIndex, ignoreOdometer);
+ }
+ }
+
+ private static boolean isMoving(ArrayList<Position> positions, int index, TripsConfig tripsConfig) {
+ if (tripsConfig.getMinimalNoDataDuration() > 0) {
+ boolean beforeGap = index < positions.size() - 1
+ && positions.get(index + 1).getFixTime().getTime() - positions.get(index).getFixTime().getTime()
+ >= tripsConfig.getMinimalNoDataDuration();
+ boolean afterGap = index > 0
+ && positions.get(index).getFixTime().getTime() - positions.get(index - 1).getFixTime().getTime()
+ >= tripsConfig.getMinimalNoDataDuration();
+ if (beforeGap || afterGap) {
+ return false;
+ }
+ }
+ if (positions.get(index).getAttributes().containsKey(Position.KEY_MOTION)
+ && positions.get(index).getAttributes().get(Position.KEY_MOTION) instanceof Boolean) {
+ return positions.get(index).getBoolean(Position.KEY_MOTION);
+ } else {
+ return positions.get(index).getSpeed() > tripsConfig.getSpeedThreshold();
+ }
+ }
+
+ public static <T extends BaseReport> Collection<T> detectTripsAndStops(
+ IdentityManager identityManager, DeviceManager deviceManager,
+ Collection<Position> positionCollection,
+ TripsConfig tripsConfig, boolean ignoreOdometer, Class<T> reportClass) {
+
+ Collection<T> result = new ArrayList<>();
+
+ ArrayList<Position> positions = new ArrayList<>(positionCollection);
+ if (!positions.isEmpty()) {
+ boolean trips = reportClass.equals(TripReport.class);
+ MotionEventHandler motionHandler = new MotionEventHandler(identityManager, deviceManager, tripsConfig);
+ DeviceState deviceState = new DeviceState();
+ deviceState.setMotionState(isMoving(positions, 0, tripsConfig));
+ int startEventIndex = trips == deviceState.getMotionState() ? 0 : -1;
+ int startNoEventIndex = -1;
+ for (int i = 0; i < positions.size(); i++) {
+ Map<Event, Position> event = motionHandler.updateMotionState(deviceState, positions.get(i),
+ isMoving(positions, i, tripsConfig));
+ if (startEventIndex == -1
+ && (trips != deviceState.getMotionState() && deviceState.getMotionPosition() != null
+ || trips == deviceState.getMotionState() && event != null)) {
+ startEventIndex = i;
+ startNoEventIndex = -1;
+ } else if (trips != deviceState.getMotionState() && startEventIndex != -1
+ && deviceState.getMotionPosition() == null && event == null) {
+ startEventIndex = -1;
+ }
+ if (startNoEventIndex == -1
+ && (trips == deviceState.getMotionState() && deviceState.getMotionPosition() != null
+ || trips != deviceState.getMotionState() && event != null)) {
+ startNoEventIndex = i;
+ } else if (startNoEventIndex != -1 && deviceState.getMotionPosition() == null && event == null) {
+ startNoEventIndex = -1;
+ }
+ if (startEventIndex != -1 && startNoEventIndex != -1 && event != null
+ && trips != deviceState.getMotionState()) {
+ result.add(calculateTripOrStop(positions, startEventIndex, startNoEventIndex,
+ ignoreOdometer, reportClass));
+ startEventIndex = -1;
+ }
+ }
+ if (startEventIndex != -1 && (startNoEventIndex != -1 || !trips)) {
+ result.add(calculateTripOrStop(positions, startEventIndex,
+ startNoEventIndex != -1 ? startNoEventIndex : positions.size() - 1,
+ ignoreOdometer, reportClass));
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/Route.java b/src/main/java/org/traccar/reports/Route.java
new file mode 100644
index 000000000..6adb00aae
--- /dev/null
+++ b/src/main/java/org/traccar/reports/Route.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.model.Position;
+import org.traccar.reports.model.DeviceReport;
+
+public final class Route {
+
+ private Route() {
+ }
+
+ public static Collection<Position> getObjects(long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws SQLException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<Position> result = new ArrayList<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ result.addAll(Context.getDataManager().getPositions(deviceId, from, to));
+ }
+ return result;
+ }
+
+ public static void getExcel(OutputStream outputStream,
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws SQLException, IOException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<DeviceReport> devicesRoutes = new ArrayList<>();
+ ArrayList<String> sheetNames = new ArrayList<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ Collection<Position> positions = Context.getDataManager()
+ .getPositions(deviceId, from, to);
+ DeviceReport deviceRoutes = new DeviceReport();
+ Device device = Context.getIdentityManager().getById(deviceId);
+ deviceRoutes.setDeviceName(device.getName());
+ sheetNames.add(WorkbookUtil.createSafeSheetName(deviceRoutes.getDeviceName()));
+ if (device.getGroupId() != 0) {
+ Group group = Context.getGroupsManager().getById(device.getGroupId());
+ if (group != null) {
+ deviceRoutes.setGroupName(group.getName());
+ }
+ }
+ deviceRoutes.setObjects(positions);
+ devicesRoutes.add(deviceRoutes);
+ }
+ String templatePath = Context.getConfig().getString("report.templatesPath",
+ "templates/export/");
+ try (InputStream inputStream = new FileInputStream(templatePath + "/route.xlsx")) {
+ org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
+ jxlsContext.putVar("devices", devicesRoutes);
+ jxlsContext.putVar("sheetNames", sheetNames);
+ jxlsContext.putVar("from", from);
+ jxlsContext.putVar("to", to);
+ ReportUtils.processTemplateWithSheets(inputStream, outputStream, jxlsContext);
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/reports/Stops.java b/src/main/java/org/traccar/reports/Stops.java
new file mode 100644
index 000000000..98c9cef00
--- /dev/null
+++ b/src/main/java/org/traccar/reports/Stops.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.traccar.reports;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.traccar.Context;
+import org.traccar.Main;
+import org.traccar.database.DeviceManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.reports.model.DeviceReport;
+import org.traccar.reports.model.StopReport;
+
+public final class Stops {
+
+ private Stops() {
+ }
+
+ private static Collection<StopReport> detectStops(long deviceId, Date from, Date to) throws SQLException {
+ boolean ignoreOdometer = Context.getDeviceManager()
+ .lookupAttributeBoolean(deviceId, "report.ignoreOdometer", false, true);
+
+ IdentityManager identityManager = Main.getInjector().getInstance(IdentityManager.class);
+ DeviceManager deviceManager = Main.getInjector().getInstance(DeviceManager.class);
+
+ return ReportUtils.detectTripsAndStops(
+ identityManager, deviceManager, Context.getDataManager().getPositions(deviceId, from, to),
+ Context.getTripsConfig(), ignoreOdometer, StopReport.class);
+ }
+
+ public static Collection<StopReport> getObjects(
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws SQLException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<StopReport> result = new ArrayList<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ result.addAll(detectStops(deviceId, from, to));
+ }
+ return result;
+ }
+
+ public static void getExcel(
+ OutputStream outputStream, long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws SQLException, IOException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<DeviceReport> devicesStops = new ArrayList<>();
+ ArrayList<String> sheetNames = new ArrayList<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ Collection<StopReport> stops = detectStops(deviceId, from, to);
+ DeviceReport deviceStops = new DeviceReport();
+ Device device = Context.getIdentityManager().getById(deviceId);
+ deviceStops.setDeviceName(device.getName());
+ sheetNames.add(WorkbookUtil.createSafeSheetName(deviceStops.getDeviceName()));
+ if (device.getGroupId() != 0) {
+ Group group = Context.getGroupsManager().getById(device.getGroupId());
+ if (group != null) {
+ deviceStops.setGroupName(group.getName());
+ }
+ }
+ deviceStops.setObjects(stops);
+ devicesStops.add(deviceStops);
+ }
+ String templatePath = Context.getConfig().getString("report.templatesPath",
+ "templates/export/");
+ try (InputStream inputStream = new FileInputStream(templatePath + "/stops.xlsx")) {
+ org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
+ jxlsContext.putVar("devices", devicesStops);
+ jxlsContext.putVar("sheetNames", sheetNames);
+ jxlsContext.putVar("from", from);
+ jxlsContext.putVar("to", to);
+ ReportUtils.processTemplateWithSheets(inputStream, outputStream, jxlsContext);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/Summary.java b/src/main/java/org/traccar/reports/Summary.java
new file mode 100644
index 000000000..9810424d8
--- /dev/null
+++ b/src/main/java/org/traccar/reports/Summary.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+
+import org.jxls.util.JxlsHelper;
+import org.traccar.Context;
+import org.traccar.model.Position;
+import org.traccar.reports.model.SummaryReport;
+
+public final class Summary {
+
+ private Summary() {
+ }
+
+ private static SummaryReport calculateSummaryResult(long deviceId, Date from, Date to) throws SQLException {
+ SummaryReport result = new SummaryReport();
+ result.setDeviceId(deviceId);
+ result.setDeviceName(Context.getIdentityManager().getById(deviceId).getName());
+ Collection<Position> positions = Context.getDataManager().getPositions(deviceId, from, to);
+ if (positions != null && !positions.isEmpty()) {
+ Position firstPosition = null;
+ Position previousPosition = null;
+ double speedSum = 0;
+ boolean engineHoursEnabled = Context.getConfig().getBoolean("processing.engineHours.enable");
+ for (Position position : positions) {
+ if (firstPosition == null) {
+ firstPosition = position;
+ }
+ if (engineHoursEnabled && previousPosition != null
+ && position.getBoolean(Position.KEY_IGNITION)
+ && previousPosition.getBoolean(Position.KEY_IGNITION)) {
+ // Temporary fallback for old data, to be removed in May 2019
+ result.addEngineHours(position.getFixTime().getTime()
+ - previousPosition.getFixTime().getTime());
+ }
+ previousPosition = position;
+ speedSum += position.getSpeed();
+ result.setMaxSpeed(position.getSpeed());
+ }
+ boolean ignoreOdometer = Context.getDeviceManager()
+ .lookupAttributeBoolean(deviceId, "report.ignoreOdometer", false, true);
+ result.setDistance(ReportUtils.calculateDistance(firstPosition, previousPosition, !ignoreOdometer));
+ result.setAverageSpeed(speedSum / positions.size());
+ result.setSpentFuel(ReportUtils.calculateFuel(firstPosition, previousPosition));
+
+ if (engineHoursEnabled
+ && firstPosition.getAttributes().containsKey(Position.KEY_HOURS)
+ && previousPosition.getAttributes().containsKey(Position.KEY_HOURS)) {
+ result.setEngineHours(
+ previousPosition.getLong(Position.KEY_HOURS) - firstPosition.getLong(Position.KEY_HOURS));
+ }
+
+ if (!ignoreOdometer
+ && firstPosition.getDouble(Position.KEY_ODOMETER) != 0
+ && previousPosition.getDouble(Position.KEY_ODOMETER) != 0) {
+ result.setStartOdometer(firstPosition.getDouble(Position.KEY_ODOMETER));
+ result.setEndOdometer(previousPosition.getDouble(Position.KEY_ODOMETER));
+ } else {
+ result.setStartOdometer(firstPosition.getDouble(Position.KEY_TOTAL_DISTANCE));
+ result.setEndOdometer(previousPosition.getDouble(Position.KEY_TOTAL_DISTANCE));
+ }
+
+ }
+ return result;
+ }
+
+ public static Collection<SummaryReport> getObjects(long userId, Collection<Long> deviceIds,
+ Collection<Long> groupIds, Date from, Date to) throws SQLException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<SummaryReport> result = new ArrayList<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ result.add(calculateSummaryResult(deviceId, from, to));
+ }
+ return result;
+ }
+
+ public static void getExcel(OutputStream outputStream,
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws SQLException, IOException {
+ ReportUtils.checkPeriodLimit(from, to);
+ Collection<SummaryReport> summaries = getObjects(userId, deviceIds, groupIds, from, to);
+ String templatePath = Context.getConfig().getString("report.templatesPath",
+ "templates/export/");
+ try (InputStream inputStream = new FileInputStream(templatePath + "/summary.xlsx")) {
+ org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
+ jxlsContext.putVar("summaries", summaries);
+ jxlsContext.putVar("from", from);
+ jxlsContext.putVar("to", to);
+ JxlsHelper.getInstance().setUseFastFormulaProcessor(false)
+ .processTemplate(inputStream, outputStream, jxlsContext);
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/reports/Trips.java b/src/main/java/org/traccar/reports/Trips.java
new file mode 100644
index 000000000..3cda65553
--- /dev/null
+++ b/src/main/java/org/traccar/reports/Trips.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.traccar.Context;
+import org.traccar.Main;
+import org.traccar.database.DeviceManager;
+import org.traccar.database.IdentityManager;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.reports.model.DeviceReport;
+import org.traccar.reports.model.TripReport;
+
+public final class Trips {
+
+ private Trips() {
+ }
+
+ private static Collection<TripReport> detectTrips(long deviceId, Date from, Date to) throws SQLException {
+ boolean ignoreOdometer = Context.getDeviceManager()
+ .lookupAttributeBoolean(deviceId, "report.ignoreOdometer", false, true);
+
+ IdentityManager identityManager = Main.getInjector().getInstance(IdentityManager.class);
+ DeviceManager deviceManager = Main.getInjector().getInstance(DeviceManager.class);
+
+ return ReportUtils.detectTripsAndStops(
+ identityManager, deviceManager, Context.getDataManager().getPositions(deviceId, from, to),
+ Context.getTripsConfig(), ignoreOdometer, TripReport.class);
+ }
+
+ public static Collection<TripReport> getObjects(long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws SQLException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<TripReport> result = new ArrayList<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ result.addAll(detectTrips(deviceId, from, to));
+ }
+ return result;
+ }
+
+ public static void getExcel(OutputStream outputStream,
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws SQLException, IOException {
+ ReportUtils.checkPeriodLimit(from, to);
+ ArrayList<DeviceReport> devicesTrips = new ArrayList<>();
+ ArrayList<String> sheetNames = new ArrayList<>();
+ for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
+ Context.getPermissionsManager().checkDevice(userId, deviceId);
+ Collection<TripReport> trips = detectTrips(deviceId, from, to);
+ DeviceReport deviceTrips = new DeviceReport();
+ Device device = Context.getIdentityManager().getById(deviceId);
+ deviceTrips.setDeviceName(device.getName());
+ sheetNames.add(WorkbookUtil.createSafeSheetName(deviceTrips.getDeviceName()));
+ if (device.getGroupId() != 0) {
+ Group group = Context.getGroupsManager().getById(device.getGroupId());
+ if (group != null) {
+ deviceTrips.setGroupName(group.getName());
+ }
+ }
+ deviceTrips.setObjects(trips);
+ devicesTrips.add(deviceTrips);
+ }
+ String templatePath = Context.getConfig().getString("report.templatesPath",
+ "templates/export/");
+ try (InputStream inputStream = new FileInputStream(templatePath + "/trips.xlsx")) {
+ org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
+ jxlsContext.putVar("devices", devicesTrips);
+ jxlsContext.putVar("sheetNames", sheetNames);
+ jxlsContext.putVar("from", from);
+ jxlsContext.putVar("to", to);
+ ReportUtils.processTemplateWithSheets(inputStream, outputStream, jxlsContext);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/model/BaseReport.java b/src/main/java/org/traccar/reports/model/BaseReport.java
new file mode 100644
index 000000000..9f2d1188c
--- /dev/null
+++ b/src/main/java/org/traccar/reports/model/BaseReport.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports.model;
+
+public class BaseReport {
+
+ private long deviceId;
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ private String deviceName;
+
+ public String getDeviceName() {
+ return deviceName;
+ }
+
+ public void setDeviceName(String deviceName) {
+ this.deviceName = deviceName;
+ }
+
+ private double distance;
+
+ public double getDistance() {
+ return distance;
+ }
+
+ public void setDistance(double distance) {
+ this.distance = distance;
+ }
+
+ public void addDistance(double distance) {
+ this.distance += distance;
+ }
+
+ private double averageSpeed;
+
+ public double getAverageSpeed() {
+ return averageSpeed;
+ }
+
+ public void setAverageSpeed(Double averageSpeed) {
+ this.averageSpeed = averageSpeed;
+ }
+
+ private double maxSpeed;
+
+ public double getMaxSpeed() {
+ return maxSpeed;
+ }
+
+ public void setMaxSpeed(double maxSpeed) {
+ if (maxSpeed > this.maxSpeed) {
+ this.maxSpeed = maxSpeed;
+ }
+ }
+
+ private double spentFuel;
+
+ public double getSpentFuel() {
+ return spentFuel;
+ }
+
+ public void setSpentFuel(double spentFuel) {
+ this.spentFuel = spentFuel;
+ }
+
+ private double startOdometer;
+
+ public double getStartOdometer() {
+ return startOdometer;
+ }
+
+ public void setStartOdometer(double startOdometer) {
+ this.startOdometer = startOdometer;
+ }
+ private double endOdometer;
+
+ public double getEndOdometer() {
+ return endOdometer;
+ }
+
+ public void setEndOdometer(double endOdometer) {
+ this.endOdometer = endOdometer;
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/model/DeviceReport.java b/src/main/java/org/traccar/reports/model/DeviceReport.java
new file mode 100644
index 000000000..932753d15
--- /dev/null
+++ b/src/main/java/org/traccar/reports/model/DeviceReport.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class DeviceReport {
+
+ private String deviceName;
+
+ public String getDeviceName() {
+ return deviceName;
+ }
+
+ public void setDeviceName(String deviceName) {
+ this.deviceName = deviceName;
+ }
+
+ private String groupName = "";
+
+ public String getGroupName() {
+ return groupName;
+ }
+
+ public void setGroupName(String groupName) {
+ this.groupName = groupName;
+ }
+
+ private List<?> objects;
+
+ public Collection<?> getObjects() {
+ return objects;
+ }
+
+ public void setObjects(Collection<?> objects) {
+ this.objects = new ArrayList<>(objects);
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/model/StopReport.java b/src/main/java/org/traccar/reports/model/StopReport.java
new file mode 100644
index 000000000..245292b63
--- /dev/null
+++ b/src/main/java/org/traccar/reports/model/StopReport.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports.model;
+
+import java.util.Date;
+
+public class StopReport extends BaseReport {
+
+ private long positionId;
+
+ public long getPositionId() {
+ return positionId;
+ }
+
+ public void setPositionId(long positionId) {
+ this.positionId = positionId;
+ }
+
+ private double latitude;
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = latitude;
+ }
+
+ private double longitude;
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = longitude;
+ }
+
+ private Date startTime;
+
+ public Date getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(Date startTime) {
+ this.startTime = startTime;
+ }
+
+ private Date endTime;
+
+ public Date getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(Date endTime) {
+ this.endTime = endTime;
+ }
+
+ private String address;
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ private long duration;
+
+ public long getDuration() {
+ return duration;
+ }
+
+ public void setDuration(long duration) {
+ this.duration = duration;
+ }
+
+ private long engineHours; // milliseconds
+
+ public long getEngineHours() {
+ return engineHours;
+ }
+
+ public void setEngineHours(long engineHours) {
+ this.engineHours = engineHours;
+ }
+
+ public void addEngineHours(long engineHours) {
+ this.engineHours += engineHours;
+ }
+}
diff --git a/src/main/java/org/traccar/reports/model/SummaryReport.java b/src/main/java/org/traccar/reports/model/SummaryReport.java
new file mode 100644
index 000000000..6f9e9459f
--- /dev/null
+++ b/src/main/java/org/traccar/reports/model/SummaryReport.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports.model;
+
+public class SummaryReport extends BaseReport {
+
+ private long engineHours; // milliseconds
+
+ public long getEngineHours() {
+ return engineHours;
+ }
+
+ public void setEngineHours(long engineHours) {
+ this.engineHours = engineHours;
+ }
+
+ public void addEngineHours(long engineHours) {
+ this.engineHours += engineHours;
+ }
+}
diff --git a/src/main/java/org/traccar/reports/model/TripReport.java b/src/main/java/org/traccar/reports/model/TripReport.java
new file mode 100644
index 000000000..3140f3019
--- /dev/null
+++ b/src/main/java/org/traccar/reports/model/TripReport.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports.model;
+
+import java.util.Date;
+
+public class TripReport extends BaseReport {
+
+ private long startPositionId;
+
+ public long getStartPositionId() {
+ return startPositionId;
+ }
+
+ public void setStartPositionId(long startPositionId) {
+ this.startPositionId = startPositionId;
+ }
+
+ private long endPositionId;
+
+ public long getEndPositionId() {
+ return endPositionId;
+ }
+
+ public void setEndPositionId(long endPositionId) {
+ this.endPositionId = endPositionId;
+ }
+
+ private double startLat;
+
+ public double getStartLat() {
+ return startLat;
+ }
+
+ public void setStartLat(double startLat) {
+ this.startLat = startLat;
+ }
+
+ private double startLon;
+
+ public double getStartLon() {
+ return startLon;
+ }
+
+ public void setStartLon(double startLon) {
+ this.startLon = startLon;
+ }
+
+ private double endLat;
+
+ public double getEndLat() {
+ return endLat;
+ }
+
+ public void setEndLat(double endLat) {
+ this.endLat = endLat;
+ }
+
+ private double endLon;
+
+ public double getEndLon() {
+ return endLon;
+ }
+
+ public void setEndLon(double endLon) {
+ this.endLon = endLon;
+ }
+
+ private Date startTime;
+
+ public Date getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(Date startTime) {
+ this.startTime = startTime;
+ }
+
+ private String startAddress;
+
+ public String getStartAddress() {
+ return startAddress;
+ }
+
+ public void setStartAddress(String address) {
+ this.startAddress = address;
+ }
+
+ private Date endTime;
+
+ public Date getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(Date endTime) {
+ this.endTime = endTime;
+ }
+
+ private String endAddress;
+
+ public String getEndAddress() {
+ return endAddress;
+ }
+
+ public void setEndAddress(String address) {
+ this.endAddress = address;
+ }
+
+ private long duration;
+
+ public long getDuration() {
+ return duration;
+ }
+
+ public void setDuration(long duration) {
+ this.duration = duration;
+ }
+
+ private String driverUniqueId;
+
+ public String getDriverUniqueId() {
+ return driverUniqueId;
+ }
+
+ public void setDriverUniqueId(String driverUniqueId) {
+ this.driverUniqueId = driverUniqueId;
+ }
+
+ private String driverName;
+
+ public String getDriverName() {
+ return driverName;
+ }
+
+ public void setDriverName(String driverName) {
+ this.driverName = driverName;
+ }
+}
diff --git a/src/main/java/org/traccar/reports/model/TripsConfig.java b/src/main/java/org/traccar/reports/model/TripsConfig.java
new file mode 100644
index 000000000..0f0c615d3
--- /dev/null
+++ b/src/main/java/org/traccar/reports/model/TripsConfig.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.reports.model;
+
+public class TripsConfig {
+
+ public TripsConfig() {
+ }
+
+ public TripsConfig(double minimalTripDistance, long minimalTripDuration, long minimalParkingDuration,
+ long minimalNoDataDuration, boolean useIgnition, boolean processInvalidPositions, double speedThreshold) {
+ this.minimalTripDistance = minimalTripDistance;
+ this.minimalTripDuration = minimalTripDuration;
+ this.minimalParkingDuration = minimalParkingDuration;
+ this.minimalNoDataDuration = minimalNoDataDuration;
+ this.useIgnition = useIgnition;
+ this.processInvalidPositions = processInvalidPositions;
+ this.speedThreshold = speedThreshold;
+ }
+
+ private double minimalTripDistance;
+
+ public double getMinimalTripDistance() {
+ return minimalTripDistance;
+ }
+
+ public void setMinimalTripDistance(double minimalTripDistance) {
+ this.minimalTripDistance = minimalTripDistance;
+ }
+
+ private long minimalTripDuration;
+
+ public long getMinimalTripDuration() {
+ return minimalTripDuration;
+ }
+
+ public void setMinimalTripDuration(long minimalTripDuration) {
+ this.minimalTripDuration = minimalTripDuration;
+ }
+
+ private long minimalParkingDuration;
+
+ public long getMinimalParkingDuration() {
+ return minimalParkingDuration;
+ }
+
+ public void setMinimalParkingDuration(long minimalParkingDuration) {
+ this.minimalParkingDuration = minimalParkingDuration;
+ }
+
+ private long minimalNoDataDuration;
+
+ public long getMinimalNoDataDuration() {
+ return minimalNoDataDuration;
+ }
+
+ public void setMinimalNoDataDuration(long minimalNoDataDuration) {
+ this.minimalNoDataDuration = minimalNoDataDuration;
+ }
+
+ private boolean useIgnition;
+
+ public boolean getUseIgnition() {
+ return useIgnition;
+ }
+
+ public void setUseIgnition(boolean useIgnition) {
+ this.useIgnition = useIgnition;
+ }
+
+ private boolean processInvalidPositions;
+
+ public boolean getProcessInvalidPositions() {
+ return processInvalidPositions;
+ }
+
+ public void setProcessInvalidPositions(boolean processInvalidPositions) {
+ this.processInvalidPositions = processInvalidPositions;
+ }
+
+ private double speedThreshold;
+
+ public double getSpeedThreshold() {
+ return speedThreshold;
+ }
+
+ public void setSpeedThreshold(double speedThreshold) {
+ this.speedThreshold = speedThreshold;
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/HttpSmsClient.java b/src/main/java/org/traccar/sms/HttpSmsClient.java
new file mode 100644
index 000000000..8e2b67bf7
--- /dev/null
+++ b/src/main/java/org/traccar/sms/HttpSmsClient.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.sms;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.api.SecurityRequestFilter;
+import org.traccar.helper.DataConverter;
+import org.traccar.notification.MessageException;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+public class HttpSmsClient implements SmsManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(HttpSmsClient.class);
+
+ private String url;
+ private String authorizationHeader;
+ private String authorization;
+ private String template;
+ private boolean encode;
+ private MediaType mediaType;
+
+ public HttpSmsClient() {
+ url = Context.getConfig().getString("sms.http.url");
+ authorizationHeader = Context.getConfig().getString("sms.http.authorizationHeader",
+ SecurityRequestFilter.AUTHORIZATION_HEADER);
+ authorization = Context.getConfig().getString("sms.http.authorization");
+ if (authorization == null) {
+ String user = Context.getConfig().getString("sms.http.user");
+ String password = Context.getConfig().getString("sms.http.password");
+ authorization = "Basic "
+ + DataConverter.printBase64((user + ":" + password).getBytes(StandardCharsets.UTF_8));
+ }
+ template = Context.getConfig().getString("sms.http.template").trim();
+ if (template.charAt(0) == '{' || template.charAt(0) == '[') {
+ encode = false;
+ mediaType = MediaType.APPLICATION_JSON_TYPE;
+ } else {
+ encode = true;
+ mediaType = MediaType.APPLICATION_FORM_URLENCODED_TYPE;
+ }
+ }
+
+ private String prepareValue(String value) throws UnsupportedEncodingException {
+ return encode ? URLEncoder.encode(value, StandardCharsets.UTF_8.name()) : value;
+ }
+
+ private String preparePayload(String destAddress, String message) {
+ try {
+ return template
+ .replace("{phone}", prepareValue(destAddress))
+ .replace("{message}", prepareValue(message.trim()));
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Invocation.Builder getRequestBuilder() {
+ return Context.getClient().target(url).request()
+ .header(authorizationHeader, authorization);
+ }
+
+ @Override
+ public void sendMessageSync(String destAddress, String message, boolean command) throws MessageException {
+ Response response = getRequestBuilder().post(Entity.entity(preparePayload(destAddress, message), mediaType));
+ if (response.getStatus() / 100 != 2) {
+ throw new MessageException(response.readEntity(String.class));
+ }
+ }
+
+ @Override
+ public void sendMessageAsync(final String destAddress, final String message, final boolean command) {
+ getRequestBuilder().async().post(
+ Entity.entity(preparePayload(destAddress, message), mediaType), new InvocationCallback<String>() {
+ @Override
+ public void completed(String s) {
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ LOGGER.warn("SMS send failed", throwable);
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/SmsManager.java b/src/main/java/org/traccar/sms/SmsManager.java
new file mode 100644
index 000000000..3b0cbda7f
--- /dev/null
+++ b/src/main/java/org/traccar/sms/SmsManager.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.sms;
+
+import org.traccar.notification.MessageException;
+
+public interface SmsManager {
+
+ void sendMessageSync(
+ String destAddress, String message, boolean command) throws InterruptedException, MessageException;
+
+ void sendMessageAsync(
+ String destAddress, String message, boolean command);
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/ClientSmppSessionHandler.java b/src/main/java/org/traccar/sms/smpp/ClientSmppSessionHandler.java
new file mode 100644
index 000000000..6b9de3107
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/ClientSmppSessionHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.sms.smpp;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.cloudhopper.commons.charset.CharsetUtil;
+import com.cloudhopper.smpp.SmppConstants;
+import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler;
+import com.cloudhopper.smpp.pdu.DeliverSm;
+import com.cloudhopper.smpp.pdu.PduRequest;
+import com.cloudhopper.smpp.pdu.PduResponse;
+import com.cloudhopper.smpp.util.SmppUtil;
+
+public class ClientSmppSessionHandler extends DefaultSmppSessionHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClientSmppSessionHandler.class);
+
+ private SmppClient smppClient;
+
+ public ClientSmppSessionHandler(SmppClient smppClient) {
+ this.smppClient = smppClient;
+ }
+
+ @Override
+ public void firePduRequestExpired(PduRequest pduRequest) {
+ LOGGER.warn("PDU request expired: " + pduRequest);
+ }
+
+ @Override
+ public PduResponse firePduRequestReceived(PduRequest request) {
+ PduResponse response;
+ try {
+ if (request instanceof DeliverSm) {
+ String sourceAddress = ((DeliverSm) request).getSourceAddress().getAddress();
+ String message = CharsetUtil.decode(((DeliverSm) request).getShortMessage(),
+ smppClient.mapDataCodingToCharset(((DeliverSm) request).getDataCoding()));
+ LOGGER.info("SMS Message Received: " + message.trim() + ", Source Address: " + sourceAddress);
+
+ boolean isDeliveryReceipt;
+ if (smppClient.getDetectDlrByOpts()) {
+ isDeliveryReceipt = request.getOptionalParameters() != null;
+ } else {
+ isDeliveryReceipt = SmppUtil.isMessageTypeAnyDeliveryReceipt(((DeliverSm) request).getEsmClass());
+ }
+
+ if (!isDeliveryReceipt) {
+ TextMessageEventHandler.handleTextMessage(sourceAddress, message);
+ }
+ }
+ response = request.createResponse();
+ } catch (Exception error) {
+ LOGGER.warn("SMS receiving error", error);
+ response = request.createResponse();
+ response.setResultMessage(error.getMessage());
+ response.setCommandStatus(SmppConstants.STATUS_UNKNOWNERR);
+ }
+ return response;
+ }
+
+ @Override
+ public void fireChannelUnexpectedlyClosed() {
+ LOGGER.warn("SMPP session channel unexpectedly closed");
+ smppClient.scheduleReconnect();
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/EnquireLinkTask.java b/src/main/java/org/traccar/sms/smpp/EnquireLinkTask.java
new file mode 100644
index 000000000..7086709d7
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/EnquireLinkTask.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.sms.smpp;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.cloudhopper.smpp.SmppSession;
+import com.cloudhopper.smpp.pdu.EnquireLink;
+import com.cloudhopper.smpp.type.RecoverablePduException;
+import com.cloudhopper.smpp.type.SmppChannelException;
+import com.cloudhopper.smpp.type.SmppTimeoutException;
+import com.cloudhopper.smpp.type.UnrecoverablePduException;
+
+public class EnquireLinkTask implements Runnable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EnquireLinkTask.class);
+
+ private SmppClient smppClient;
+ private Integer enquireLinkTimeout;
+
+ public EnquireLinkTask(SmppClient smppClient, Integer enquireLinkTimeout) {
+ this.smppClient = smppClient;
+ this.enquireLinkTimeout = enquireLinkTimeout;
+ }
+
+ @Override
+ public void run() {
+ SmppSession smppSession = smppClient.getSession();
+ if (smppSession != null && smppSession.isBound()) {
+ try {
+ smppSession.enquireLink(new EnquireLink(), enquireLinkTimeout);
+ } catch (SmppTimeoutException | SmppChannelException
+ | RecoverablePduException | UnrecoverablePduException error) {
+ LOGGER.warn("Enquire link failed, executing reconnect: ", error);
+ smppClient.scheduleReconnect();
+ } catch (InterruptedException error) {
+ LOGGER.info("Enquire link interrupted, probably killed by reconnecting");
+ }
+ } else {
+ LOGGER.warn("Enquire link running while session is not connected");
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/ReconnectionTask.java b/src/main/java/org/traccar/sms/smpp/ReconnectionTask.java
new file mode 100644
index 000000000..c009de8e7
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/ReconnectionTask.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.sms.smpp;
+
+public class ReconnectionTask implements Runnable {
+
+ private final SmppClient smppClient;
+
+ protected ReconnectionTask(SmppClient smppClient) {
+ this.smppClient = smppClient;
+ }
+
+ @Override
+ public void run() {
+ smppClient.reconnect();
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/SmppClient.java b/src/main/java/org/traccar/sms/smpp/SmppClient.java
new file mode 100644
index 000000000..874253d36
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/SmppClient.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.sms.smpp;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.notification.MessageException;
+import org.traccar.sms.SmsManager;
+
+import com.cloudhopper.commons.charset.CharsetUtil;
+import com.cloudhopper.smpp.SmppBindType;
+import com.cloudhopper.smpp.SmppConstants;
+import com.cloudhopper.smpp.SmppSession;
+import com.cloudhopper.smpp.SmppSessionConfiguration;
+import com.cloudhopper.smpp.impl.DefaultSmppClient;
+import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler;
+import com.cloudhopper.smpp.pdu.SubmitSm;
+import com.cloudhopper.smpp.pdu.SubmitSmResp;
+import com.cloudhopper.smpp.tlv.Tlv;
+import com.cloudhopper.smpp.type.Address;
+import com.cloudhopper.smpp.type.RecoverablePduException;
+import com.cloudhopper.smpp.type.SmppChannelException;
+import com.cloudhopper.smpp.type.SmppTimeoutException;
+import com.cloudhopper.smpp.type.UnrecoverablePduException;
+
+public class SmppClient implements SmsManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SmppClient.class);
+
+ private SmppSessionConfiguration sessionConfig = new SmppSessionConfiguration();
+ private SmppSession smppSession;
+ private DefaultSmppSessionHandler sessionHandler = new ClientSmppSessionHandler(this);
+ private ExecutorService executorService = Executors.newCachedThreadPool();
+ private DefaultSmppClient clientBootstrap = new DefaultSmppClient();
+
+ private ScheduledExecutorService enquireLinkExecutor;
+ private ScheduledFuture<?> enquireLinkTask;
+ private Integer enquireLinkPeriod;
+ private Integer enquireLinkTimeout;
+
+ private ScheduledExecutorService reconnectionExecutor;
+ private ScheduledFuture<?> reconnectionTask;
+ private Integer reconnectionDelay;
+
+ private String sourceAddress;
+ private String commandSourceAddress;
+ private int submitTimeout;
+ private boolean requestDlr;
+ private boolean detectDlrByOpts;
+ private String notificationsCharsetName;
+ private byte notificationsDataCoding;
+ private String commandsCharsetName;
+ private byte commandsDataCoding;
+
+ private byte sourceTon;
+ private byte sourceNpi;
+ private byte commandSourceTon;
+ private byte commandSourceNpi;
+
+ private byte destTon;
+ private byte destNpi;
+
+ public SmppClient() {
+ sessionConfig.setName("Traccar.smppSession");
+ sessionConfig.setInterfaceVersion(
+ (byte) Context.getConfig().getInteger("sms.smpp.version", SmppConstants.VERSION_3_4));
+ sessionConfig.setType(SmppBindType.TRANSCEIVER);
+ sessionConfig.setHost(Context.getConfig().getString("sms.smpp.host", "localhost"));
+ sessionConfig.setPort(Context.getConfig().getInteger("sms.smpp.port", 2775));
+ sessionConfig.setSystemId(Context.getConfig().getString("sms.smpp.username", "user"));
+ sessionConfig.setSystemType(Context.getConfig().getString("sms.smpp.systemType", null));
+ sessionConfig.setPassword(Context.getConfig().getString("sms.smpp.password", "password"));
+ sessionConfig.getLoggingOptions().setLogBytes(false);
+ sessionConfig.getLoggingOptions().setLogPdu(Context.getConfig().getBoolean("sms.smpp.logPdu"));
+
+ sourceAddress = Context.getConfig().getString("sms.smpp.sourceAddress", "");
+ commandSourceAddress = Context.getConfig().getString("sms.smpp.commandSourceAddress", sourceAddress);
+ submitTimeout = Context.getConfig().getInteger("sms.smpp.submitTimeout", 10000);
+
+ requestDlr = Context.getConfig().getBoolean("sms.smpp.requestDlr");
+ detectDlrByOpts = Context.getConfig().getBoolean("sms.smpp.detectDlrByOpts");
+
+ notificationsCharsetName = Context.getConfig().getString("sms.smpp.notificationsCharset",
+ CharsetUtil.NAME_UCS_2);
+ notificationsDataCoding = (byte) Context.getConfig().getInteger("sms.smpp.notificationsDataCoding",
+ SmppConstants.DATA_CODING_UCS2);
+ commandsCharsetName = Context.getConfig().getString("sms.smpp.commandsCharset",
+ CharsetUtil.NAME_GSM);
+ commandsDataCoding = (byte) Context.getConfig().getInteger("sms.smpp.commandsDataCoding",
+ SmppConstants.DATA_CODING_DEFAULT);
+
+
+ sourceTon = (byte) Context.getConfig().getInteger("sms.smpp.sourceTon", SmppConstants.TON_ALPHANUMERIC);
+ commandSourceTon = (byte) Context.getConfig().getInteger("sms.smpp.commandSourceTon", sourceTon);
+ sourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.sourceNpi", SmppConstants.NPI_UNKNOWN);
+ commandSourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.commandSourceNpi", sourceNpi);
+
+ destTon = (byte) Context.getConfig().getInteger("sms.smpp.destTon", SmppConstants.TON_INTERNATIONAL);
+ destNpi = (byte) Context.getConfig().getInteger("sms.smpp.destNpi", SmppConstants.NPI_E164);
+
+ enquireLinkPeriod = Context.getConfig().getInteger("sms.smpp.enquireLinkPeriod", 60000);
+ enquireLinkTimeout = Context.getConfig().getInteger("sms.smpp.enquireLinkTimeout", 10000);
+ enquireLinkExecutor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable);
+ String name = sessionConfig.getName();
+ thread.setName("EnquireLink-" + name);
+ return thread;
+ }
+ });
+
+ reconnectionDelay = Context.getConfig().getInteger("sms.smpp.reconnectionDelay", 10000);
+ reconnectionExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable);
+ String name = sessionConfig.getName();
+ thread.setName("Reconnection-" + name);
+ return thread;
+ }
+ });
+
+ scheduleReconnect();
+ }
+
+ public synchronized SmppSession getSession() {
+ return smppSession;
+ }
+
+ public String mapDataCodingToCharset(byte dataCoding) {
+ switch (dataCoding) {
+ case SmppConstants.DATA_CODING_LATIN1:
+ return CharsetUtil.NAME_ISO_8859_1;
+ case SmppConstants.DATA_CODING_UCS2:
+ return CharsetUtil.NAME_UCS_2;
+ default:
+ return CharsetUtil.NAME_GSM;
+ }
+ }
+
+ public boolean getDetectDlrByOpts() {
+ return detectDlrByOpts;
+ }
+
+ protected synchronized void reconnect() {
+ try {
+ disconnect();
+ smppSession = clientBootstrap.bind(sessionConfig, sessionHandler);
+ stopReconnectionkTask();
+ runEnquireLinkTask();
+ LOGGER.info("SMPP session connected");
+ } catch (SmppTimeoutException | SmppChannelException
+ | UnrecoverablePduException | InterruptedException error) {
+ LOGGER.warn("Unable to connect to SMPP server: ", error);
+ }
+ }
+
+ public void scheduleReconnect() {
+ if (reconnectionTask == null || reconnectionTask.isDone()) {
+ reconnectionTask = reconnectionExecutor.scheduleWithFixedDelay(
+ new ReconnectionTask(this),
+ reconnectionDelay, reconnectionDelay, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private void stopReconnectionkTask() {
+ if (reconnectionTask != null) {
+ reconnectionTask.cancel(false);
+ }
+ }
+
+ private void disconnect() {
+ stopEnquireLinkTask();
+ destroySession();
+ }
+
+ private void runEnquireLinkTask() {
+ enquireLinkTask = enquireLinkExecutor.scheduleWithFixedDelay(
+ new EnquireLinkTask(this, enquireLinkTimeout),
+ enquireLinkPeriod, enquireLinkPeriod, TimeUnit.MILLISECONDS);
+ }
+
+ private void stopEnquireLinkTask() {
+ if (enquireLinkTask != null) {
+ enquireLinkTask.cancel(true);
+ }
+ }
+
+ private void destroySession() {
+ if (smppSession != null) {
+ LOGGER.info("Cleaning up SMPP session... ");
+ smppSession.destroy();
+ smppSession = null;
+ }
+ }
+
+ @Override
+ public synchronized void sendMessageSync(String destAddress, String message, boolean command)
+ throws MessageException, InterruptedException, IllegalStateException {
+ if (getSession() != null && getSession().isBound()) {
+ try {
+ SubmitSm submit = new SubmitSm();
+ byte[] textBytes;
+ textBytes = CharsetUtil.encode(message, command ? commandsCharsetName : notificationsCharsetName);
+ submit.setDataCoding(command ? commandsDataCoding : notificationsDataCoding);
+ if (requestDlr) {
+ submit.setRegisteredDelivery(SmppConstants.REGISTERED_DELIVERY_SMSC_RECEIPT_REQUESTED);
+ }
+
+ if (textBytes != null && textBytes.length > 255) {
+ submit.addOptionalParameter(new Tlv(SmppConstants.TAG_MESSAGE_PAYLOAD, textBytes,
+ "message_payload"));
+ } else {
+ submit.setShortMessage(textBytes);
+ }
+
+ submit.setSourceAddress(command ? new Address(commandSourceTon, commandSourceNpi, commandSourceAddress)
+ : new Address(sourceTon, sourceNpi, sourceAddress));
+ submit.setDestAddress(new Address(destTon, destNpi, destAddress));
+ SubmitSmResp submitResponce = getSession().submit(submit, submitTimeout);
+ if (submitResponce.getCommandStatus() == SmppConstants.STATUS_OK) {
+ LOGGER.info("SMS submitted, message id: " + submitResponce.getMessageId());
+ } else {
+ throw new IllegalStateException(submitResponce.getResultMessage());
+ }
+ } catch (SmppChannelException | RecoverablePduException
+ | SmppTimeoutException | UnrecoverablePduException error) {
+ throw new MessageException(error);
+ }
+ } else {
+ throw new MessageException(new SmppChannelException("SMPP session is not connected"));
+ }
+ }
+
+ @Override
+ public void sendMessageAsync(final String destAddress, final String message, final boolean command) {
+ executorService.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sendMessageSync(destAddress, message, command);
+ } catch (MessageException | InterruptedException | IllegalStateException error) {
+ LOGGER.warn("SMS sending error", error);
+ }
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/TextMessageEventHandler.java b/src/main/java/org/traccar/sms/smpp/TextMessageEventHandler.java
new file mode 100644
index 000000000..37c29972d
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/TextMessageEventHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.sms.smpp;
+
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+
+public final class TextMessageEventHandler {
+
+ private TextMessageEventHandler() {
+ }
+
+ public static void handleTextMessage(String phone, String message) {
+ Device device = Context.getDeviceManager().getDeviceByPhone(phone);
+ if (device != null && Context.getNotificationManager() != null) {
+ Event event = new Event(Event.TYPE_TEXT_MESSAGE, device.getId());
+ event.set("message", message);
+ Context.getNotificationManager().updateEvent(event, null);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/web/ConsoleServlet.java b/src/main/java/org/traccar/web/ConsoleServlet.java
new file mode 100644
index 000000000..2b38a935a
--- /dev/null
+++ b/src/main/java/org/traccar/web/ConsoleServlet.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.web;
+
+import org.h2.server.web.ConnectionInfo;
+import org.h2.server.web.WebServlet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class ConsoleServlet extends WebServlet {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ConsoleServlet.class);
+
+ @Override
+ public void init() {
+ super.init();
+
+ try {
+ Field field = WebServlet.class.getDeclaredField("server");
+ field.setAccessible(true);
+ org.h2.server.web.WebServer server = (org.h2.server.web.WebServer) field.get(this);
+
+ ConnectionInfo connectionInfo = new ConnectionInfo("Traccar|"
+ + Context.getConfig().getString("database.driver") + "|"
+ + Context.getConfig().getString("database.url") + "|"
+ + Context.getConfig().getString("database.user"));
+
+ Method method;
+
+ method = org.h2.server.web.WebServer.class.getDeclaredMethod("updateSetting", ConnectionInfo.class);
+ method.setAccessible(true);
+ method.invoke(server, connectionInfo);
+
+ method = org.h2.server.web.WebServer.class.getDeclaredMethod("setAllowOthers", boolean.class);
+ method.setAccessible(true);
+ method.invoke(server, true);
+
+ } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
+ LOGGER.warn("Console reflection error", e);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/web/CsvBuilder.java b/src/main/java/org/traccar/web/CsvBuilder.java
new file mode 100644
index 000000000..3fe7e408f
--- /dev/null
+++ b/src/main/java/org/traccar/web/CsvBuilder.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.web;
+
+import java.beans.Introspector;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.helper.DateUtil;
+
+public class CsvBuilder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CsvBuilder.class);
+
+ private static final String LINE_ENDING = "\r\n";
+ private static final String SEPARATOR = ";";
+
+ private StringBuilder builder = new StringBuilder();
+
+ private void addLineEnding() {
+ builder.append(LINE_ENDING);
+ }
+ private void addSeparator() {
+ builder.append(SEPARATOR);
+ }
+
+ private SortedSet<Method> getSortedMethods(Object object) {
+ Method[] methodArray = object.getClass().getMethods();
+ SortedSet<Method> methods = new TreeSet<>(new Comparator<Method>() {
+ @Override
+ public int compare(Method m1, Method m2) {
+ if (m1.getName().equals("getAttributes") && !m1.getName().equals(m2.getName())) {
+ return 1;
+ }
+ if (m2.getName().equals("getAttributes") && !m1.getName().equals(m2.getName())) {
+ return -1;
+ }
+ return m1.getName().compareTo(m2.getName());
+ }
+ });
+ methods.addAll(Arrays.asList(methodArray));
+ return methods;
+ }
+
+ public void addLine(Object object) {
+
+ SortedSet<Method> methods = getSortedMethods(object);
+
+ for (Method method : methods) {
+ if (method.getName().startsWith("get") && method.getParameterTypes().length == 0) {
+ try {
+ if (method.getReturnType().equals(boolean.class)) {
+ builder.append(method.invoke(object));
+ addSeparator();
+ } else if (method.getReturnType().equals(int.class)) {
+ builder.append(method.invoke(object));
+ addSeparator();
+ } else if (method.getReturnType().equals(long.class)) {
+ builder.append(method.invoke(object));
+ addSeparator();
+ } else if (method.getReturnType().equals(double.class)) {
+ builder.append(method.invoke(object));
+ addSeparator();
+ } else if (method.getReturnType().equals(String.class)) {
+ builder.append((String) method.invoke(object));
+ addSeparator();
+ } else if (method.getReturnType().equals(Date.class)) {
+ Date value = (Date) method.invoke(object);
+ builder.append(DateUtil.formatDate(value));
+ addSeparator();
+ } else if (method.getReturnType().equals(Map.class)) {
+ Map value = (Map) method.invoke(object);
+ if (value != null) {
+ try {
+ String map = Context.getObjectMapper().writeValueAsString(value);
+ map = map.replaceAll("[\\{\\}\"]", "");
+ map = map.replaceAll(",", " ");
+ builder.append(map);
+ addSeparator();
+ } catch (JsonProcessingException e) {
+ LOGGER.warn("Map JSON formatting error", e);
+ }
+ }
+ }
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Reflection invocation error", error);
+ }
+ }
+ }
+ addLineEnding();
+ }
+
+ public void addHeaderLine(Object object) {
+
+ SortedSet<Method> methods = getSortedMethods(object);
+
+ for (Method method : methods) {
+ if (method.getName().startsWith("get") && method.getParameterTypes().length == 0) {
+ String name = Introspector.decapitalize(method.getName().substring(3));
+ if (!name.equals("class")) {
+ builder.append(name);
+ addSeparator();
+ }
+ }
+ }
+ addLineEnding();
+ }
+
+ public void addArray(Collection<?> array) {
+ for (Object object : array) {
+ switch (object.getClass().getSimpleName().toLowerCase()) {
+ case "string":
+ builder.append(object.toString());
+ addLineEnding();
+ break;
+ case "long":
+ builder.append((long) object);
+ addLineEnding();
+ break;
+ case "double":
+ builder.append((double) object);
+ addLineEnding();
+ break;
+ case "boolean":
+ builder.append((boolean) object);
+ addLineEnding();
+ break;
+ default:
+ addLine(object);
+ break;
+ }
+ }
+ }
+
+ public String build() {
+ return builder.toString();
+ }
+
+}
diff --git a/src/main/java/org/traccar/web/GpxBuilder.java b/src/main/java/org/traccar/web/GpxBuilder.java
new file mode 100644
index 000000000..638d100e5
--- /dev/null
+++ b/src/main/java/org/traccar/web/GpxBuilder.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.web;
+
+import java.util.Collection;
+
+import org.traccar.helper.DateUtil;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.model.Position;
+
+public class GpxBuilder {
+
+ private StringBuilder builder = new StringBuilder();
+ private static final String HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>"
+ + "<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" creator=\"Traccar\" version=\"1.1\" "
+ + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
+ + "xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 "
+ + "http://www.topografix.com/GPX/1/1/gpx.xsd\"><trk>\n";
+ private static final String NAME = "<name>%1$s</name><trkseg>%n";
+ private static final String POINT = "<trkpt lat=\"%1$f\" lon=\"%2$f\">"
+ + "<time>%3$s</time>"
+ + "<geoidheight>%4$f</geoidheight>"
+ + "<course>%5$f</course>"
+ + "<speed>%6$f</speed>"
+ + "</trkpt>%n";
+ private static final String FOOTER = "</trkseg></trk></gpx>";
+
+ public GpxBuilder() {
+ builder.append(HEADER);
+ builder.append("<trkseg>\n");
+ }
+
+ public GpxBuilder(String name) {
+ builder.append(HEADER);
+ builder.append(String.format(NAME, name));
+ }
+
+ public void addPosition(Position position) {
+ builder.append(String.format(POINT, position.getLatitude(), position.getLongitude(),
+ DateUtil.formatDate(position.getFixTime()), position.getAltitude(),
+ position.getCourse(), UnitsConverter.mpsFromKnots(position.getSpeed())));
+ }
+
+ public void addPositions(Collection<Position> positions) {
+ for (Position position : positions) {
+ addPosition(position);
+ }
+ }
+
+ public String build() {
+ builder.append(FOOTER);
+ return builder.toString();
+ }
+
+}
diff --git a/src/main/java/org/traccar/web/WebServer.java b/src/main/java/org/traccar/web/WebServer.java
new file mode 100644
index 000000000..70fef4ed3
--- /dev/null
+++ b/src/main/java/org/traccar/web/WebServer.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2012 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.web;
+
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.proxy.AsyncProxyServlet;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.api.AsyncSocketServlet;
+import org.traccar.api.CorsResponseFilter;
+import org.traccar.api.MediaFilter;
+import org.traccar.api.ObjectMapperProvider;
+import org.traccar.api.ResourceErrorHandler;
+import org.traccar.api.SecurityRequestFilter;
+import org.traccar.api.resource.ServerResource;
+
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.net.InetSocketAddress;
+import java.util.EnumSet;
+
+public class WebServer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WebServer.class);
+
+ private Server server;
+
+ private void initServer(Config config) {
+
+ String address = config.getString("web.address");
+ int port = config.getInteger("web.port", 8082);
+ if (address == null) {
+ server = new Server(port);
+ } else {
+ server = new Server(new InetSocketAddress(address, port));
+ }
+ }
+
+ public WebServer(Config config) {
+
+ initServer(config);
+
+ ServletContextHandler servletHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
+
+ int sessionTimeout = config.getInteger("web.sessionTimeout");
+ if (sessionTimeout > 0) {
+ servletHandler.getSessionHandler().setMaxInactiveInterval(sessionTimeout);
+ }
+
+ initApi(config, servletHandler);
+
+ if (config.getBoolean("web.console")) {
+ servletHandler.addServlet(new ServletHolder(new ConsoleServlet()), "/console/*");
+ }
+
+ initWebApp(config, servletHandler);
+
+ servletHandler.setErrorHandler(new ErrorHandler() {
+ @Override
+ protected void handleErrorPage(
+ HttpServletRequest request, Writer writer, int code, String message) throws IOException {
+ writer.write("<!DOCTYPE<html><head><title>Error</title></head><html><body>"
+ + code + " - " + HttpStatus.getMessage(code) + "</body></html>");
+ }
+ });
+
+ HandlerList handlers = new HandlerList();
+ initClientProxy(config, handlers);
+ handlers.addHandler(servletHandler);
+ server.setHandler(handlers);
+ }
+
+ private void initClientProxy(Config config, HandlerList handlers) {
+ int port = config.getInteger("osmand.port");
+ if (port != 0) {
+ ServletContextHandler servletHandler = new ServletContextHandler() {
+ @Override
+ public void doScope(
+ String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+ if (target.equals("/") && request.getMethod().equals(HttpMethod.POST.asString())) {
+ super.doScope(target, baseRequest, request, response);
+ }
+ }
+ };
+ ServletHolder servletHolder = new ServletHolder(new AsyncProxyServlet.Transparent());
+ servletHolder.setInitParameter("proxyTo", "http://localhost:" + port);
+ servletHandler.addServlet(servletHolder, "/");
+ handlers.addHandler(servletHandler);
+ }
+ }
+
+ private void initWebApp(Config config, ServletContextHandler servletHandler) {
+ ServletHolder servletHolder = new ServletHolder(DefaultServlet.class);
+ servletHolder.setInitParameter("resourceBase", new File(config.getString("web.path")).getAbsolutePath());
+ if (config.getBoolean("web.debug")) {
+ servletHandler.setWelcomeFiles(new String[] {"debug.html", "index.html"});
+ } else {
+ String cache = config.getString("web.cacheControl");
+ if (cache != null && !cache.isEmpty()) {
+ servletHolder.setInitParameter("cacheControl", cache);
+ }
+ servletHandler.setWelcomeFiles(new String[] {"release.html", "index.html"});
+ }
+ servletHandler.addServlet(servletHolder, "/*");
+ }
+
+ private void initApi(Config config, ServletContextHandler servletHandler) {
+ servletHandler.addServlet(new ServletHolder(new AsyncSocketServlet()), "/api/socket");
+
+ if (config.hasKey("media.path")) {
+ ServletHolder servletHolder = new ServletHolder(DefaultServlet.class);
+ servletHolder.setInitParameter("resourceBase", new File(config.getString("media.path")).getAbsolutePath());
+ servletHolder.setInitParameter("dirAllowed", config.getString("media.dirAllowed", "false"));
+ servletHolder.setInitParameter("pathInfoOnly", "true");
+ servletHandler.addServlet(servletHolder, "/api/media/*");
+ servletHandler.addFilter(MediaFilter.class, "/api/media/*", EnumSet.allOf(DispatcherType.class));
+ }
+
+ ResourceConfig resourceConfig = new ResourceConfig();
+ resourceConfig.registerClasses(JacksonFeature.class, ObjectMapperProvider.class, ResourceErrorHandler.class);
+ resourceConfig.registerClasses(SecurityRequestFilter.class, CorsResponseFilter.class);
+ resourceConfig.packages(ServerResource.class.getPackage().getName());
+ servletHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/api/*");
+ }
+
+ public void start() {
+ try {
+ server.start();
+ } catch (Exception error) {
+ LOGGER.warn("Web server start failed", error);
+ }
+ }
+
+ public void stop() {
+ try {
+ server.stop();
+ } catch (Exception error) {
+ LOGGER.warn("Web server stop failed", error);
+ }
+ }
+
+}