I am trying to create an app to remotely control some lights. As for now it's going good, I can control the status of the relays that control the lights just by pushing the buttons I designed on the app regardless of the network the device on which the app is installed is connected.
What I want is:
Multiple devices will have this app installed and connected to the same place (I mean that will control the same lights). So when one changes the status of a light all the other instances of the app (the one making the changes included) should be updated with the new light statuses.
I thought to achieve this by putting a server listening inside the app so that when there's a change the real server, hosted on a Raspberry that controls a bunch of relays, can send a message to all the apps and update them. The apps will send their IP and the ID they are stored on the server everytime the app opens or there's an IP change.
The problem I'm facing is :
When working locally everything goes just fine, but when the device on which the app is installed connects to another network, it won't receive updates anymore. Of course I know this is due to the fact that, because of the Flutter Plugin I'm using, I only get the local IP of the device so through internet I won't reach it. I could retrieve both public and private IP addresses of a device, but then I don't know how to "ask" to the router which "owns" the public IP to let me access the device with the private IP without needing a portforwarding (portforwarding can't be done because I'm working with mobile devices).
My questions :
Is there a way to achieve what I want? Or a different method to dynamically update the apps whenever there's a change? Or a way to get both "external" and local IP and then call the apps using that?
Thanks in advance.
pubspec.yaml dependencies:
dependencies:
flutter:
sdk: flutter
get_ip: ^0.3.0
shared_preferences: ^0.4.3
AndroidManifest.xml permissions:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
Flutter server and ip sending method:
void listenForUpdates()
{
HttpServer.bind(InternetAddress.anyIPv4, int.parse(port)).then((server)
{
server.listen((HttpRequest request)
{
MapEntry<String, String> parameter = request.uri.queryParameters.entries.single;
print(parameter);
setState(() {
lightsOn[int.parse(parameter.key) - 1] = parameter.value.contains('true');
noResponse = false;
});
request.response.close();
});
});
}
void initPlatformState(bool resync) async
{
String ipAddress;
ipAddress = await GetIp.ipAddress;
HttpClient client = new HttpClient();
SharedPreferences prefs = await SharedPreferences.getInstance();
setState((){ deviceId = resync ? "new" : (prefs.getString('deviceId') ?? "new"); });
client.postUrl(Uri.parse('http://NameOfTheServer:Port/?q=device&ip=' + ipAddress + ':' + port + '&id=' + deviceId))
.catchError((onError)
{
setState((){ noResponse = true; });
})
.then((HttpClientRequest request)
{
request.headers.set(HttpHeaders.userAgentHeader, 'LumenApp - Dart:Flutter');
return request.close();
})
.then((HttpClientResponse response)
{
response.transform(utf8.decoder).listen((contents) async
{
if(response.statusCode == 200)
{
setState((){
deviceId = contents.split('|')[1];
noResponse = false;
});
await prefs.setString('deviceId', deviceId);
}
else
setState((){ noResponse = true; });
});
});
}
Methods of the classes that handle the requests from the app (Raspberry, Java):
@Override
public void run()
{
System.out.println("Successfully started thread with ThreadId " + this.threadId + ".");
if(http.res.getCurrentResponse().size() > 0)
{
http.res.send();
}
else
{
http.res.setHeaders(http.req.getHttpVersion(), 200);
try
{
String qParam = http.req.getParameterValueByName("q");
String deviceId = http.req.getParameterValueByName("id");
Boolean devPerm = AppUpdater.getDevicesId().contains(deviceId);
if(!devPerm && !deviceId.equalsIgnoreCase("new"))
{
http.res.setBody("ID EXPIRED");
}
else
{
if(qParam.equalsIgnoreCase("device"))
{
String deviceIp = http.req.getParameterValueByName("ip");
String toDelete = http.req.getParameterValueByName("delete");
if(toDelete == null)
{
toDelete = "false";
}
Integer id = AppUpdater.updateDevicesIp(deviceId, deviceIp, Boolean.parseBoolean(toDelete));
http.res.setBody("DeviceId|" + id);
}
else if(Integer.parseInt(qParam) >= 0 && devPerm)
{
Integer lParam = Integer.parseInt(http.req.getParameterValueByName("l"));
gpioHandler.changeGpiosState(qParam, lParam);
}
else if(qParam.equals("-1") && devPerm)
{
HashMap<Integer, Boolean> gpiosState = gpioHandler.getGpiosState();
http.res.setBody(qParam);
for(HashMap.Entry<Integer, Boolean> gpio : gpiosState.entrySet())
{
http.res.addBody("|" + gpio.getKey().toString() + "-" + gpio.getValue().toString());
}
}
}
}
catch(RuntimeException e)
{
e.printStackTrace();
System.err.println("One or more required parameters were missing.");
http.res.setHeaders(http.req.getHttpVersion(), 400);
}
finally
{
http.res.send();
}
}
public class AppUpdater
{
static final GpioHandler gpioHandler = new GpioHandler();
static HashMap<String, String> devices = new HashMap<String, String>();
public static Integer updateDevicesIp(String deviceId, String deviceIp, boolean toDelete)
{
Integer id = 0;
if(toDelete)
{
devices.remove(deviceIp);
}
else if(deviceId.equalsIgnoreCase("new"))
{
id = devices.size() + 1;
devices.put(id.toString(), deviceIp);
}
else if(devices.containsKey(deviceId))
{
devices.replace(deviceId, deviceIp);
id = Integer.parseInt(deviceId);
}
return id;
}
public static void notifyApp(String lightIndex, String lightStatus) throws IOException
{
for(HashMap.Entry<String, String> device : devices.entrySet())
{
String urlParameters = "?" + lightIndex + "=" + lightStatus;
URL url = new URL("http://" + device.getValue() + urlParameters);
System.out.println(url);
url.openStream();
}
}
public static ArrayList<String> getDevicesId()
{
ArrayList<String> devicesId = new ArrayList<String>();
for(HashMap.Entry<String, String> device : devices.entrySet())
{
devicesId.add(device.getKey());
}
return devicesId;
}
public static HashMap<String, String> getDevices()
{
return devices;
}
}
public void gpioListener()
{
for(GpioPinDigitalOutput pin : gpios)
{
pin.addListener(new GpioPinListenerDigital() {
@Override
public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event)
{
String gpioName = event.getPin().getName();
System.out.println(" --> GPIO PIN STATE CHANGE: " + gpioName + " = " + event.getState());
try
{
String lightStatus = event.getState().toString().equals("HIGH") ? "true" : "false";
String pinIndex = pinIndexes.get(gpioName.substring(gpioName.length() - 1));
AppUpdater.notifyApp(pinIndex, lightStatus);
}
catch(IOException e)
{
e.printStackTrace();
}
}
});
}
}
I found the solution. Websockets.
I wrote a little "double" server in Node JS and another server in Java, this time the Java server was less complex than before.
I used NodeJs because implementing websockets in Java was a pain.
I call it "double server" because I didn't find (nor search) a way to communicate with a simple Java ServerSocket to a NodeJs WebSocket so I made also a little HTTP server in the same NodeJs file.
I modified the app using the flutter websocket tutorial (using web_socket_channel plugin).
This also made the app very simpler.
So, summarizing:
I wrote a code for a WebSocket server in NodeJs using ws library and for an HTTP server, in NodeJs too, using express and http libraries.
I wrote my own Java classes for handling HTTP connections and I used Pi4j to handle the pin of my RaspberryPi.
The app was wrote in Android Studio using Flutter framework, with only web_socket_channel dependency.
The process is:
1) The apps connect to the WebSocket server.
2) The WebSocket server reads from a file and sends the current relays' statuses to the newly connected apps.
3) When an app asks for a change of a relay's status, the request is sent through the WebSocket and redirected to the Java server using NodeJs http library.
4) The Java server makes the desired change, updates the file with the new relays' statuses and returns a 200 HTTP response.
5) The change is caught by the GPIO Listener provided by the Pi4j library and it is sent, as a string, to the NodeJs HTTP Express server.
6) The NodeJs HTTP Express server says to the WebSocket server to notify all the apps with the change.
7) The apps are successfully updated whenever there's a change!