Invio Comandi ai GPS
Guida completa per inviare comandi ai dispositivi GPS tramite VPN 1NCE.
Overview
Con 1NCE VPN puoi inviare comandi real-time ai GPS tramite la connessione TCP bidirezionale.
Vantaggi vs SMS
| Feature | VPN Commands | SMS Commands |
|---|---|---|
| Latenza | < 1 secondo | 5-60 secondi |
| Costo | Gratis | €0.05-0.15 per SMS |
| Affidabilità | Alta (TCP) | Media (rete mobile) |
| Capacità dati | Illimitata | Max 160 caratteri |
| Tracking | Ack immediato | No conferma |
Architettura
Implementazione Backend
1. Publishers (API → Redis)
// services/commands/src/controllers/commands.controller.ts
import { Redis } from 'ioredis';
export class CommandsController {
private redis: Redis;
async sendCommand(req: Request, res: Response) {
const { deviceId, commandType, params } = req.body;
// Crea comando
const command = {
deviceId,
type: commandType,
params,
timestamp: Date.now(),
userId: req.user.id,
};
// Pubblica su Redis Stream
const messageId = await this.redis.xadd(
'device_commands',
'*',
'data', JSON.stringify(command)
);
res.json({
success: true,
commandId: messageId,
message: 'Command queued for delivery'
});
}
}
2. Consumer (Redis → Decoder)
Nel decoder Java, implementa consumer Redis:
// services/decoder/src/main/java/com/visla/decoder/command/CommandConsumer.java
public class CommandConsumer implements Runnable {
private final Jedis redis;
private final ConnectionManager connectionManager;
@Override
public void run() {
String lastId = "0-0";
while (true) {
try {
// Leggi nuovi comandi da Redis
List<StreamEntry> entries = redis.xread(
XReadParams.xReadParams()
.block(1000)
.count(10),
Collections.singletonMap("device_commands", lastId)
);
for (StreamEntry entry : entries) {
processCommand(entry);
lastId = entry.getID().toString();
}
} catch (Exception e) {
log.error("Error reading commands", e);
sleep(1000);
}
}
}
private void processCommand(StreamEntry entry) {
String data = entry.getFields().get("data");
Command command = parseCommand(data);
// Trova sessione attiva per questo dispositivo
DeviceSession session = connectionManager
.getActiveSession(command.getDeviceId());
if (session == null) {
log.warn("No active session for device {}",
command.getDeviceId());
publishCommandResponse(command, "FAILED",
"Device not connected");
return;
}
// Invia comando al GPS via TCP
Channel channel = session.getChannel();
ByteBuf encodedCommand = encodeCommand(command);
channel.writeAndFlush(encodedCommand).addListener(future -> {
if (future.isSuccess()) {
log.info("Command sent to device {}",
command.getDeviceId());
publishCommandResponse(command, "SENT", null);
} else {
log.error("Failed to send command", future.cause());
publishCommandResponse(command, "FAILED",
future.cause().getMessage());
}
});
}
private ByteBuf encodeCommand(Command command) {
// Codifica comando in formato protocollo GPS
// Esempio per S21L:
String cmd = formatS21LCommand(command);
return Unpooled.copiedBuffer(cmd, StandardCharsets.UTF_8);
}
private String formatS21LCommand(Command command) {
switch (command.getType()) {
case "ENGINE_STOP":
return "RELAY,1#";
case "ENGINE_START":
return "RELAY,0#";
case "REBOOT":
return "RESET#";
case "UPDATE_INTERVAL":
int seconds = (int) command.getParams().get("seconds");
return "TIMER," + seconds + "#";
default:
throw new IllegalArgumentException(
"Unknown command: " + command.getType());
}
}
private void publishCommandResponse(Command command,
String status,
String error) {
Map<String, String> response = new HashMap<>();
response.put("commandId", command.getId());
response.put("deviceId", command.getDeviceId());
response.put("status", status);
if (error != null) {
response.put("error", error);
}
response.put("timestamp", String.valueOf(System.currentTimeMillis()));
redis.xadd("command_responses",
StreamEntryID.NEW_ENTRY,
response);
}
}
3. Connection Manager
Gestisce le sessioni TCP attive:
// services/decoder/src/main/java/com/visla/decoder/session/ConnectionManager.java
public class ConnectionManager {
// Map: deviceId → Channel
private final Map<String, Channel> activeSessions =
new ConcurrentHashMap<>();
public void registerSession(String deviceId, Channel channel) {
activeSessions.put(deviceId, channel);
log.info("Device {} registered, IP: {}",
deviceId, channel.remoteAddress());
// Rimuovi quando si disconnette
channel.closeFuture().addListener(future -> {
activeSessions.remove(deviceId);
log.info("Device {} disconnected", deviceId);
});
}
public Channel getActiveSession(String deviceId) {
Channel channel = activeSessions.get(deviceId);
if (channel != null && channel.isActive()) {
return channel;
}
return null;
}
public Set<String> getConnectedDevices() {
return activeSessions.keySet();
}
public int getConnectionCount() {
return activeSessions.size();
}
}
Tipi di Comandi
Comandi S21L
// Definizioni comandi supportati
export enum CommandType {
ENGINE_STOP = 'engine_stop', // RELAY,1#
ENGINE_START = 'engine_start', // RELAY,0#
REBOOT = 'reboot', // RESET#
UPDATE_INTERVAL = 'update_interval', // TIMER,60#
REQUEST_POSITION = 'request_position', // WHERE#
SET_APN = 'set_apn', // APN,iot.1nce.net#
SET_GEOFENCE = 'set_geofence', // FENCE,...#
}
Esempio API Calls
// Engine Stop
POST /api/commands/send
{
"deviceId": "123",
"commandType": "engine_stop",
"params": {}
}
// Update Interval
POST /api/commands/send
{
"deviceId": "123",
"commandType": "update_interval",
"params": {
"seconds": 30
}
}
// Set Geofence
POST /api/commands/send
{
"deviceId": "123",
"commandType": "set_geofence",
"params": {
"lat": 41.9028,
"lon": 12.4964,
"radius": 500
}
}
Monitoring Esecuzione
Tracking Status
// Poll per conferma esecuzione
GET /api/commands/:commandId/status
Response:
{
"commandId": "1234567-0",
"status": "SENT", // QUEUED | SENT | FAILED
"sentAt": "2024-12-15T18:30:00Z",
"error": null
}
WebSocket Real-time
// Client WebSocket
const ws = new WebSocket('wss://api.vislagps.com/ws');
ws.on('open', () => {
ws.send(JSON.stringify({
type: 'subscribe',
channel: 'command_responses:123' // deviceId
}));
});
ws.on('message', (data) => {
const response = JSON.parse(data);
console.log('Command executed:', response);
// { status: 'SENT', commandId: '...', timestamp: ... }
});
Testing
Test Locale
# 1. Pubblica comando manualmente su Redis
docker exec redis redis-cli XADD device_commands \* \
data '{"deviceId":"123","type":"reboot","params":{},"timestamp":1702665000}'
# 2. Monitora log decoder
docker logs decoder -f | grep "Command sent"
# 3. Verifica risposta
docker exec redis redis-cli XREAD COUNT 1 STREAMS command_responses 0-0
Test con GPS Reale
# 1. Verifica GPS connesso
docker exec decoder netstat -tn | grep 100.64.25.10
# 2. Invia comando test (request position)
curl -X POST http://localhost/api/commands/send \
-H "Content-Type: application/json" \
-d '{
"deviceId": "123",
"commandType": "request_position",
"params": {}
}'
# 3. Monitora risposta GPS
docker logs decoder -f
Error Handling
Retry Logic
async function sendCommandWithRetry(
deviceId: string,
commandType: string,
params: any,
maxRetries = 3
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await sendCommand(deviceId, commandType, params);
// Aspetta conferma
const status = await waitForCommandStatus(result.commandId, 10000);
if (status === 'SENT') {
return result;
}
if (status === 'FAILED' && attempt < maxRetries) {
await sleep(2000 * attempt); // Exponential backoff
continue;
}
throw new Error(`Command failed: ${status}`);
} catch (error) {
if (attempt === maxRetries) throw error;
}
}
}
Timeout Handling
// Decoder Java - timeout comandi
channel.writeAndFlush(command)
.addListener(future -> {
if (future.isSuccess()) {
// Imposta timeout per ack
channel.eventLoop().schedule(() -> {
if (!commandAcknowledged) {
publishCommandResponse(command, "TIMEOUT",
"No ack from device");
}
}, 10, TimeUnit.SECONDS);
}
});
Security
Autorizzazione
// Verifica che user possa comandare il device
async function canSendCommand(userId: string, deviceId: string) {
const device = await db.query(
'SELECT owner_id FROM devices WHERE id = $1',
[deviceId]
);
return device.owner_id === userId;
}
Audit Log
// Log tutti i comandi inviati
await db.query(
`INSERT INTO command_logs
(user_id, device_id, command_type, params, timestamp)
VALUES ($1, $2, $3, $4, NOW())`,
[userId, deviceId, commandType, JSON.stringify(params)]
);
Performance
Batch Commands
// Invia comando a multipli GPS contemporaneamente
POST /api/commands/broadcast
{
"deviceIds": ["123", "124", "125"],
"commandType": "update_interval",
"params": { "seconds": 60 }
}
Rate Limiting
// Limita comandi per utente
const rateLimiter = new RateLimiter({
points: 100, // 100 comandi
duration: 60, // per minuto
});
await rateLimiter.consume(userId);