Accessing HTTP server from any network Flutter

3,218

Solution 1

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!

Solution 2

Welcome to stackoverflow.

You have described something that is very common in the field of industrial automation or home automation, on which one or several devices monitor the state of a machine/system, in your case they are just a few lights, but the system is usually equipped with a PLC (Programmable Logic Controller) that also executes a program.

A Raspberry PI can act as a programmable PLC with the Codesys software that is very powerful and free/cheap.

This is what many use to build small automatism or robots based on Raspberry.

You can also communicate with the PLC using standard protocols such as Modbus and OPC UA.

Here an android App that can monitor through those protocols : https://www.suppanel.com/index.php/en/

Although I admit that what I propose is to enter a very different field of programming.

Share:
3,218
CrystalSpider
Author by

CrystalSpider

Student at University of Milano-Bicocca (Università degli Studi di Milano-Bicocca, UNIMIB), Front-end developer at Lynx S.p.A., passionate about IT, programming, logic, AI and quantum computing.

Updated on December 16, 2022

Comments

  • CrystalSpider
    CrystalSpider over 1 year

    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();
                        }
                    }
                });
            }
        }
    
  • CrystalSpider
    CrystalSpider over 4 years
    Okay, I understand. But I'm limited to the use of Java (and importing Java libs) and the use of Flutter and its plugins.