Project background

This year, I believe many people will see a variety of doll machines, lucky boxes, lipstick challenges and similar machines in every shopping mall or movie theater. Lipstick Challenge is also a huge hit on Douyin. It’s tempting to think that you could win a YSL lipstick for just 10 yuan. A programmer might know something is wrong with these machines, but this one is aimed at impulse shoppers: couples, girls, adults with children; Weird creatures like programmers are generally ignored.

And then, our company provides solutions for the normal operation of these devices. So I have today’s climb pit summary, hahahaha…..

The solution we provide is as follows: one store will contain the following equipment: doll machine, lipstick challenge, leaderboard, central control, and of course, our backstage service. First of all, I will introduce the architecture of the whole system and the responsibilities of each device:

  • System Architecture Diagram

  • Service background
    • Android is not responsible for the service background, so there is no need to pay too much attention to it. Compared with the store equipment, the service background is responsible for sending quality to the machine, monitoring the status of the equipment, and some other information.
  • Central control (local service: not connected to the Internet) : This central control is also an Android device, which has two functions:
    1. Leaderboard: used to receive doll machine sent from the user folder doll information, and finally display it on the leaderboard.
    2. Resource distribution central control: as a resource distribution center, central control needs to distribute APK installation packages, pictures,
  • Doll machine: This piece actually contains two parts, one is Android device, the other is hardware device.
    1. Android devices are mainly used to display banner, process server data (such as: upper score), docking central control (resource update, data feedback, etc.) and docking hardware devices
    2. Hardware devices are mainly used to process information such as user login, gift, heartbeat, etc., and hand these information to Android devices for processing.
  • Lipstick Challenge: About lipstick Challenge, this device can be divided into three modules, namely, sewing game, routine program module and hardware module
    1. The game about The needle is made with the Egret engine, and finally embedded into the APP in the form of H5. It is mainly responsible for the main logic of the game and the feedback of the game logic data with the main module of the program.
    2. Conventional modules mainly include data processing of service background, docking hardware modules (grid selection, opening grid, etc.), docking games (starting games, feedback of game results, etc.) and material background management.
  • Rocket: It’s a bit special because it’s invisible to the user. It’s written right out of the factory, and it does the following:
    1. Shield the systemui program and launcher program of the device to prevent users from doing some illegal operations.
    2. Detect whether the U disk is inserted, and then move or take charge of formulating files (APK installation package, ipconfig.json file, teradata configuration file config.json, H5 resource file, etc.)
    3. Receive the broadcast, automatically install or update the doll machine or lipstick challenge program

Problems to be solved

Load resources synchronously

In terms of resource synchronization, first of all, let’s take a look at what resources we need to synchronize. These resources are: APK installation package, images, H5 related index resources.

The way resources are updated

About updating the way, here is actually a more pit place, at first we choose way of updating resources more silly, directly using the websocket resources update, at first only a device to connect, problem is not big, but it was found that multiple devices connected at the same time update resource problems especially big, The connection is often disconnected, causing resource updates to fail. So this is my first hole. After discovering this pit, I changed the method of selecting resource updates to NanoHttpd. NanoHttpd is an open source library, implemented in Java, that builds a lightweight Web server on Android devices. Creating a lightweight Web Server on Android devices is where we should have gone from the start. Why is that? First, NanoHttpd is relatively simple to use, so we can implement a Web server with only a few lines of code; Second, NanoHttpd is relatively stable, much more so than if we were to implement a resource distribution manually using webSockets.

So after we chose the update mode of the resource, another problem surfaced, regarding the IP address of the server. As we all know, when An Android device is connected to the mobile Internet or WiFi, it will be automatically assigned an IP address, so the IP address will change. Our device will be shut down every night, and then be assigned a new IP address when it is restarted the next day. Therefore, the IP address of the server is always changing, so what we need to do here is to find a way to fix the IP address of a device. So let’s talk about NanoHttpd creating a lightweight Web server and how to address IP changes.

NanoHttpd implements the Web Server

  • NanoHttpd project address
    • Github.com/NanoHttpd/n…
  • Gradle rely on
    implementation 'org. Nanohttpd: nanohttpd - webserver: 2.3.1'
Copy the code
  • implementation
    File resourceDir = new File(Environment.getExternalStorageDirectory(), "myRootDir");
    SimpleWebServer httpServer = new SimpleWebServer(null, 18103, resourceDir, true."*");
    httpServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
Copy the code
  • Parameters to the SimpleWebServer constructor
    • Host: indicates the IP address of the server
    • Port: indicates the port number (value range: 1024 to 65535).
    • Wwwroot: the root directory where static resources are placed
    • Quiet: Indicates whether the mode is quiet
    • Cors:
  • access
    • On the same LAN, then we enter the address in the browser: http://10.0.0.34:18103, we can access the resources in our server, of course, the current implementation of the server is static, can only handle get resource requests, can not handle POST, PUT and other requests, is currently unable to handle, If you need to handle other requests such as POST and PUT, you can implement them in the original project address by referring to their usage, which is not covered here.

Resolve the IP address change problem

On Android devices, one of its IP addresses changes, and each store has its own internal console, so we have to deal with IP address changes. Our solution has the following two steps:

  1. On the router, set a fixed IP address for the central control device in the store based on the Mac address
  2. Provide a configuration file with an IP address for each doll and lipstick challenge device. This file contains the IP address information of the store controller in the specified directory of the usb drive, but when the device is inserted, the Rocket program copies the configuration file from the USB drive to the specified directory of the device. The device must read the configuration file before connecting to the local server each time it is started.

When will the resource be updated

As for resource updating, we first need to make clear what resources we need to update and how we need to update them.

Updated resources

  • Resource.json
  • Apk package
  • The display wheel in the doll machine
  • The banner h5 resource is displayed in the doll machine

Updated configuration file

  • All the data about our resources and new data are saved in the folder “Resource. Json”, so we get Resource. Json from the central server (LAN) every 5 minutes. Each type of Resource is then judged based on the data written in resource-.json. Json file and the details are as follows:
    1. ResList Model for resources
    Public class ResListModel {// use HashMap<String, String> bannerFiles = new HashMap(); // All doll machines in stores will display the rotation chart // key is a picturehashPublic HashMap<String, String> PublicFiles = new HashMap(); // Private display rotation chart of a specific doll machine in the store // key is the id of the device // value is the picture of the picturehashPublic HashMap<String, HashMap<String, String>> PrivateFiles = new HashMap(); // UpdateApk path public String UpdateApk; // Updated APK package name public String UpdateApkPackageName; // Updated APK version name public String UpdateApkVersion; Public int UpdateApkVersionCode; }Copy the code
    1. Write to the resourse. json file
    ResListModel res = new ResListModel(); // Skip the process of adding data... ; File resourceFile = new File(baseDir,"Resource.json");
        RandomAccessFile out = new RandomAccessFile(resourceFile, "rw");
        byte[] json = JsonStream.serialize(res).getBytes("utf-8");
        out.setLength(json.length);
        out.write(json);
        out.close();
    Copy the code
    1. The content of the Resourse. Json
    {
        "PrivateFiles": {},"PublicFiles":
            {
                "1A7D3394A6F10D3668FB29D8CCA1CA8B":"Public/timg.jpg"
            },
        "UpdateApk":null,
        "UpdateApkPackageName":null,
        "UpdateApkVersion":null,
        "UpdateApkVersionCode": 0."bannerFiles":
            {
                "C609D70832710E3DCF0FB88918113B18":"banner/Resource.json"."FC1CF2C83E898357E1AD60CEF87BE6EB":"banner/app.8113390c.js"."27FBF214DF1E66D0307B7F78FEB8266F":"banner/manifest.json"."A192A95BFF57FF326185543A27058DE5":"banner/index.html"."61469B10DBD17FDEEB14C35C730E03C7":"banner/app.8113390c.css"}}Copy the code

Updates to resource files for resource images and banners

  • The resource files for images and banners are updated in a similar way, but in different directories. The update of such resources is judged by the hash value and file name of the technical resources. The doll machine or lipstick challenge device will fetch the resourse. json file from the central controller every 5 minutes, and then fetch the ResListModel, which was introduced earlier, is the configuration file that saves the resource updates. After we take out relative to the configuration of the first to local file name to judge whether the file already exists, if not, is directly added to the resource update list, if there is to determine whether a hash value is the same, the same is not updated, different local file deleted first, and then add it to the update list of resources.
  • Image and banner resource update flowchart:

  • Central control computes your hash value
Try {/ / banner resource file String fileName = fileFilter getAbsolutePath (). The substring (baseDirLength); RandomAccessFile randomAccessFile = new RandomAccessFile(fileFilter,"r");
        byte[] buf = new byte[(int) randomAccessFile.length()];
        randomAccessFile.read(buf);
        randomAccessFile.close();
        MessageDigest md5 = MessageDigest.getInstance("md5");
        byte[] hash = md5.digest(buf);
        String hashStr = ByteToHex(hash,0,hash.length);
        res.bannerFiles.put(hashStr,fileName);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
Copy the code
Public static String ByteToHex(byte[], int offset, int len) {StringBuffer sb = new StringBuffer();for (int i = offset; i < offset + len; i++) {
            int tmp = bt[i] & 0xff;
            String tmpStr = Integer.toHexString(tmp);
            if (tmpStr.length() < 2)
                sb.append("0");
            sb.append(tmpStr);
        }
        return sb.toString().toUpperCase();
    }

Copy the code
  • Check for updates (e.g. Banner resource file)
public static Observable<Boolean> updateBannerRes(ResListBean resListBean) throws IOException, HashMap<File, String> remoteFiles = new HashMap();for (HashMap.Entry<String, String> entry : resListBean.bannerFiles.entrySet()) {
            remoteFiles.put(new File(entry.getValue()), entry.getKey());
        }

        FileUtils.GetFilesInDir(bannerDir,localBannerList,null); int baseDirLength = resDir.getAbsolutePath().length()+1; // step1: delete local file (file not in remote banner)for (File localFile : localBannerList) {
            File chileFile = new File(localFile.getAbsolutePath().substring(baseDirLength));
            if(! remoteFiles.containsKey(chileFile)) { MainActivity.appendAndScrollLog(String.format("Delete banner resource file %s\n".localFile.getAbsolutePath()));
                localFile.delete(); ArrayList<Observable<File>> taskList = new ArrayList();for(Map.Entry<File, String> fileEntry : remoteFiles.entrySet()) { File file = new File(resDir,fileEntry.getKey().getAbsolutePath()); // step2: The local file name is the same as the remote file nameif (localBannerlist. contains(file)) {// step3: according tohashValue to determine whether it is the same file StringhashStr = FileUtils.getFileHashStr(file);
                if (TextUtils.equals(hashStr,fileEntry.getValue())){
                    MainActivity.appendAndScrollLog(String.format("Save banner file %s\n", file.getAbsolutePath()));
                    taskList.add(Observable.just(file));
                    continue; String url = new url ();"http", Config.instance.centralServerAddress, Config.instance.httpPort, new File(BuildConfig.APPLICATION_ID, fileEntry.getKey().getAbsolutePath()).getAbsolutePath()).toString(); / / step5: add file download list taskList. Add (DownLoadUtils. GetDownLoadFile (url, file)); }return Observable.concat(taskList)
                .toFlowable(BackpressureStrategy.MISSING)
                .parallel()
                .runOn(Schedulers.io())
                .sequential()
                .toList()
                .observeOn(Schedulers.computation())
                .map(new Function<List<File>, ArrayList<File>>() {
                    @Override
                    public ArrayList<File> apply(List<File> files) throws Exception {
                        ArrayList<File> list = new ArrayList();
                        for (File file : files) {
                            if (!file.getAbsolutePath().isEmpty()) {
                                list.add(file);
                            }
                        }
                        if (list.size() > 0) {
                            if(! Utils.EqualCollection(list,localBannerList)) {
                                Collections.sort(list);
                            } else{ list.clear(); }}return list;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .map(new Function<ArrayList<File>, Boolean>() {
                    @Override
                    public Boolean apply(ArrayList<File> list) throws Exception {
                        if (list.size() > 0) {
                            localBannerList = list;
                            webViewHasLoad = false;
                            loadH5();
                        }
                        return true;
                    }
                })
                .observeOn(Schedulers.io())
                .map(new Function<Boolean, Boolean>() {
                    @Override
                    public Boolean apply(Boolean aBoolean) throws Exception {
                        FileUtils.DelEmptyDir(resDir);
                        return true;
                    }
                })
                .toObservable();
    }

Copy the code

Program upgrade issues

The upgrade of the program is much simpler than the update of the picture resources.

  • Our implementation version update steps are as follows:
    • Step1: find out the local apk file (apk in the device is specified path and filename) and delete it.
    • Step2: determine whether the version number of the installation package in the central control is greater than that of the local program. If so, go to step3. Otherwise ignore and no program upgrade is required
    • Step3: download the apk installation package of the latest version
    • Step4: After downloading successfully, send broadcast (action: package name; Extra: APk file path) to the Rocket program
    • Step5: the rocket program receives the broadcast and upgrades the program
  • Program Upgrade Flowchart

  • Concrete code implementation

    public static Observable<Boolean> updateGame(ResListBean res) throws IOException, InterruptedException {
        ArrayList<File> apkList = new ArrayList();
        FileUtils.GetFilesInDir(resDir, apkList, new String[]{
                ".apk"}); // Delete the local APK packagefor (File file : apkList) {
            file.delete();
        }
        do {
            if (res.UpdateApk == null || res.UpdateApkVersion == null) {
                break; } // Determine whether an upgrade is requiredif (BuildConfig.VERSION_CODE >= res.UpdateApkVersionCode) {
                break; } // final String URL = new URL("http", Config.instance.centralServerAddress, Config.instance.httpPort, new File(BuildConfig.APPLICATION_ID, res.UpdateApk).getAbsolutePath()).toString();
            MainActivity.appendAndScrollLog(String.format("Download the upgrade file %s\n", url)); // Download the apK filereturn DownLoadUtils.getDownLoadFile(url,resDir.getAbsolutePath(),res.UpdateApk)
                    .subscribeOn(Schedulers.io())
                    .observeOn(Schedulers.io())
                    .flatMap(new Function<File, ObservableSource<String>>() {
                        @Override
                        public ObservableSource<String> apply(File file) throws Exception {
                            String path = file.getAbsolutePath();
                            MainActivity.appendAndScrollLog(String.format("Upgrade file download completed %s %s\n", path, url));
                            PackageManager pm = MainActivity.instance.getPackageManager();
                            PackageInfo pi = pm.getPackageArchiveInfo(path, 0);
                            if (pi == null) {
                                MainActivity.appendAndScrollLog(String.format("Failed to open upgrade file %s\n", path));
                                return Observable.just("");
                            }
                            MainActivity.appendAndScrollLog(String.format("Upgrade file comparison: Native(%s %s)/Remote(%s %s)\n", BuildConfig.APPLICATION_ID, BuildConfig.VERSION_NAME, pi.packageName, pi.versionName));
                            if(! BuildConfig.APPLICATION_ID.equals(pi.packageName) || BuildConfig.VERSION_CODE >= pi.versionCode) {return Observable.just("");
                            }
                            return Observable.just(path);
                        }
                    })
                    .flatMap(new Function<String, Observable<Boolean>>() {
                        @Override
                        public Observable<Boolean> apply(String updateApk) throws Exception {
                            if(! updateApk.isEmpty()) { Log.e(TAG,"Wait until the game ends to install the upgrade file...");
                                MainActivity.appendAndScrollLog("Wait until the game is over to install the upgrade file... \n"); Synchronized (GamePlay. Class) {// Prevent updating the game version log.e (TAG,"Broadcast");
                                    Intent intent = new Intent();
                                    intent.setAction(Config.updateBroadcast);
                                    intent.putExtra("apk", updateApk); MainActivity.instance.sendBroadcast(intent); System.exit(0); }}return Observable.just(true); }}); }while (false);
        return Observable.just(true);
    }

Copy the code

Resource file Download

For downloading the resource files, I chose OkDownload. Okdownload is a reliable, flexible, high-performance and powerful download engine that supports multi-threading, multi-tasking, breakpoint continuation. See okDownload GitHub for details

  • Depend on the way
    implementation 'com. Liulishuo. Okdownload: okdownload: 1.0.5'
    implementation 'com. Liulishuo. Okdownload: okhttp: 1.0.5'
Copy the code
  • Simple practical Examples

Single file download

DownloadTask task = new DownloadTask.Builder(url, parentFile)
         .setFilename(filename)
         // the minimal interval millisecond for callback progress
         .setMinIntervalMillisCallbackProcess(30)
         // do re-download even if the task has already been completed in the past.
         .setPassIfAlreadyCompleted(false)
         .build();
 
 
task.enqueue(listener);
 
// cancel
task.cancel();
 
// execute task synchronized
task.execute(listener);
Copy the code

Multifile download

final DownloadTask[] tasks = new DownloadTask[2];
tasks[0] = new DownloadTask.Builder("url1"."path"."filename1").build();
tasks[1] = new DownloadTask.Builder("url2"."path"."filename1").build();
DownloadTask.enqueue(tasks, listener);
Copy the code
  • Combined with Rxjava file download
Public class DownLoadUtils {/** ** Downloads files from the central controller to the local PC * @param URL * @param parentPath Path of the parent file saved to the local PC * @param downloadFileName Save to the local file name * @return*/ public Observable<File> getDownLoadFile(String URL,String parentPath,String downloadFileName){// Download a File that is not available locally MainActivity.appendAndScrollLog(String.format("Start downloading resource file %s\n", url));
        final DownloadTask task = new DownloadTask.Builder(url, parentPath, downloadFileName).build();
        return Observable.create(new ObservableOnSubscribe<File>() {
            @Override
            public void subscribe(final ObservableEmitter<File> emitter) throws Exception {
                task.enqueue(new DownloadListener2() {
                    @Override
                    public void taskStart(DownloadTask task) {

                    }

                    @Override
                    public void taskEnd(DownloadTask task, EndCause cause, Exception realCause) {
                        if(cause ! = EndCause.COMPLETED) { MainActivity.appendAndScrollLog(String.format("Resource file download failed %s %s\n", cause.toString(), task.getUrl()));
                            emitter.onNext(new File(""));
                            emitter.onComplete();
                            return;
                        }
                        File file = task.getFile();
                        MainActivity.appendAndScrollLog(String.format("Resource file download completed %s\n", file.getAbsolutePath())); emitter.onNext(file); emitter.onComplete(); }}); } }).retry(); } /** * download file from central control to local * @param url * @param saveFile saveFile to local * @return
     */
    public static Observable<File> getDownLoadFile(String url, File saveFile){
        returngetDownLoadFile(url,saveFile.getParentFile().getAbsolutePath(),saveFile.getName()); }}Copy the code

Mask drop-down menus and bottom navigation bars

Like dolls machine and check the equipment is on line directly to the user, so we can’t put our Android devices to all our customers, we need to do with the user’s behavior restrictions, such as banning user via the navigation bar or the drop-down menu to exit the current program, to prevent them to make some dangerous operation. My solution is to set the current Rocket application as the default launcher and desktop application, and disable the launcher and Systemui programs that come with Android devices, so that our Rocket application will start when the device starts up. And successfully banned users from using navigation bars and drop-down menus to do illegal operations.

  • Find the package names of the launcher program and systemui program that come with Android devices

    • Adb shell PM List Packages are used to find a list of installed programs on the device, mainly by package name.
    • Look for the package name of the program launcher and find the package name: com.android.launcher3
    LW-PC0920@lw1002022 MINGW64 ~/Desktop
    $ adb shell pm list packages | grep launcher
    package:com.android.launcher3
    Copy the code
    • Find the package name of the systemui program: find the package name: com.android.systemui
    LW-PC0920@lw1002022 MINGW64 ~/Desktop
    $ adb shell pm list packages | grep systemui
    package:com.android.systemui
    Copy the code
  • Do not use the launcher and Systemui programs of Android devices

    • Disable the use of the launcher program
    adb shell pm disable com.android.launcher3
    Copy the code
    • Disable the systemui program
    adb shell pm disable com.android.systemui
    Copy the code
  • The code implementation forbids the use of the launcher program and systemui program in Android devices

    public static void enableLauncher(Boolean enabled) {
        List<PackageInfo> piList = MainActivity.instance.packageManager.getInstalledPackages(0);
        ArrayList<String> packages = new ArrayList();
        for (PackageInfo pi : piList) {
            String name = pi.packageName;
            if (name.contains("systemui") || name.contains("launcher")) { packages.add(name); }}for (String packageName : packages) {
            su(String.format("pm %s %s\n", enabled ? "enable" : "disable", packageName)); }} public static int su(String CMD) {try {Process p = runtime.getruntime ().exec() {Process p = runtime.getruntime ()."su");
            DataOutputStream os = new DataOutputStream(p.getOutputStream());
            os.writeBytes(cmd);
            os.writeBytes("exit\n");
            os.flush();
            os.close();
            return p.waitFor();
        } catch (Exception ex) {
            return -1;
        }
    }
Copy the code

The realization of the Iot

As for the realization of IoT, we use Ali’s “Micro Message Queue for IoT” service. About the service, Ali’s explanation is as follows:

The Micro message Queue for IoT is a child of Message Queue (MQ). Message Queue (MQ) has opened up full support for MQTT protocol by launching micromessage queue for IoT to address the special messaging needs of users on the mobile Internet and the Internet of Things

  • The MQTT protocol?
    • MQTT stands for Message Queuing Telemetry Transport. MQTT is a lightweight, publish-subscribe model based im protocol. This protocol has many advantages in the field of mobile Internet and Internet of Things because of its open design, simple protocol and rich platform support, which can connect almost all connected objects with the outside world.
  • The characteristics of the MQTT
    • Use the Publish/subscribe (Pub/Sub) messaging pattern to provide one-to-many message distribution, decoupling applications;
    • Message transmission that shields payload content;
    • Use TCP/IP to provide basic network connectivity;
    • There are three levels of messaging services;
    • Small transfer, low overhead (the header length is fixed at 2 bytes), minimal protocol switching to reduce network traffic.
  • Explanation of key nouns
    noun explain
    Parent Topic The MQTT protocol is based on the Pub/Sub model, so any message belongs to a Topic. According to the MQTT protocol, there are multiple levels of a Topic, and the first-level Topic is defined as a Parent Topic, which needs to be created on the MQ console before MQTT can be used.
    Subtopic MQTT’s secondary topics, and even tertiary topics, are subclasses of parent topics. When used, set directly in the code, no need to create. Note that the MQTT limits the total length of Parent Topic and Subtopic to 64 characters, exceeding which will cause client exceptions.
    Client ID The Client ID of the MQTT is unique to each Client and must be globally unique. Using the same Client ID to connect to the MQTT service will be rejected

Android implements iot

The realization process of displaying iot connection is as follows: First, we will batch generate triples of devices from the management background, the file name format is deviceName. Json (for example: 00001.json), which is the triplet information about each device; Then we insert the usb flash drive with the triplet file into the Android device (doll machine or lipstick challenge); The Rocket program automatically detects the insertion of a USB drive and cuts the file to the specified directory on the Android device. Then the Android device can read the triplet information in the specified file; This triplet is finally used to connect the MQTT.

  • Add the dependent
    implementation 'org. Eclipse. Paho: org. Eclipse paho. Client. Mqttv3:1.2.0'
Copy the code
  • About triples

    • Three things to care about in Android devices, the three essential elements of the MQTT protocol used to identify a device. If the same triplet exists, then an error must occur, causing MQTT to frequently disconnect and reconnect. This triad is mainly generated in the management background of Ali, Android devices this end just need to use it.
    attribute use
    productKey The key of the corresponding program, similar to appID
    deviceName The Client ID is used to uniquely identify an Android device
    deviceSecret The HmacSHA1 algorithm is used to calculate the signature string and set the signature string to the Password parameter for authentication
  • Topics about subscriptions

    • Topics are set up in the background management of Ali Cloud, and we send and receive messages through these topics.
  • Code to implement iot connection

    • Cut the triplet configuration file
    @param packageName public static void moveConfig(String packageName) {File usbConfigDir = new File(UsbStorage.usbPath, Config.wejoyConfigDirInUsb); File extProjectDir = new File(Environment.getExternalStorageDirectory(), Config.resourceDirName); File extConfigFile = new File(extProjectDir, Config.wejoyConfigFileInSdcard);if(! usbConfigDir.exists() || extConfigFile.exists()) {return;
        }
        extProjectDir.mkdirs();
        File[] configFiles = usbConfigDir.listFiles();
        if (configFiles.length > 0) {
            Arrays.sort(configFiles);
            moveFile(configFiles[0], extConfigFile);
        }
    }
    
    public static void moveFile(File src, File dst) {
        su(String.format("mv -f %s %s\n", src.getAbsolutePath(), dst.getAbsolutePath()));
    }
    
    Copy the code
    • Read configuration file information for the specified path (triplet)
    public static File configFile = new File(new File(Environment.getExternalStorageDirectory(), "WejoyRes"), "Config.json");
    
    static void read() throws IOException {
        if (configFile.exists()) {
            RandomAccessFile in = new RandomAccessFile(configFile, "r");
            byte[] buf = new byte[(int) configFile.length()];
            in.read(buf);
            in.close();
            instance = JsonIterator.deserialize(new String(buf, "utf-8"), Config.class);
        } else {
            instance = new Config();
        }
        mqttRequestTopic = String.format("/sys/%s/%s/rrpc/request/", instance.productKey, instance.deviceName);
        mqttResponseTopic = String.format("/sys/%s/%s/rrpc/response/", instance.productKey, instance.deviceName);
        mqttPublishTopic = String.format("/%s/%s/update", instance.productKey, instance.deviceName);
    }
    
    Copy the code
    • Connect the MQTT
    
    static void init() {
        instance = new IoT();
        DeviceInfo deviceInfo = new DeviceInfo();
        deviceInfo.productKey = Config.instance.productKey;
        deviceInfo.deviceName = Config.instance.deviceName;
        deviceInfo.deviceSecret = Config.instance.deviceSecret;
        final LinkKitInitParams params = new LinkKitInitParams();
        params.deviceInfo = deviceInfo;
        params.connectConfig = new IoTApiClientConfig();
        LinkKit.getInstance().registerOnPushListener(instance);
        initDisposable = Observable.interval(0, Config.instance.mqttConnectIntervalSeconds, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(Schedulers.io())
                .map(new Function<Long, Boolean>() {
                    @Override
                    public Boolean apply(Long aLong) throws Exception {
                        if(! initialized) { LinkKit.getInstance().init(MainActivity.instance, params, instance); }return initialized;
                    }
                })
                .subscribe(new Consumer<Boolean>() {
                    @Override
                    public void accept(Boolean aBoolean) throws Exception {
                        if(aBoolean) { initDisposable.dispose(); }}}); }Copy the code
    • Sending a message: When sending a message, we need to specify a topic, otherwise the server will not receive our message.
        static void publish(String json) {
        Log.e(TAG, "publish: "+json );
        MqttPublishRequest res = new MqttPublishRequest();
        res.isRPC = false;
        res.topic = Config.mqttPublishTopic;
        res.payloadObj = json;
        LinkKit.getInstance().publish(res, new IConnectSendListener() {
            @Override
            public void onResponse(ARequest aRequest, AResponse aResponse) {
            }
    
            @Override
            public void onFailure(ARequest aRequest, AError aError) {
            }
        });
    }
    Copy the code
    • Receiving a message: When receiving a message, we also need to determine which topic it is from. We do not process any other topic except the one we specify. When we receive a message from a server, we first determine the type of message and then react accordingly. For example, we received the background request to the doll machine on the instructions, then we send the hardware module in the equipment on the instructions, and wait for the device response and send a response to the background information. This response message needs to be completed within the specified time, otherwise it is considered timeout.
    
    @Override
    public void onNotify(String s, final String topic, final AMessage aMessage) {
        if(! topic.startsWith(Config.mqttRequestTopic)) {return;
        }
        Observable.create(new ObservableOnSubscribe<MqttMessage>() {
            @Override
            public void subscribe(ObservableEmitter<MqttMessage> emitter) throws Exception {
                MqttMessage msg = JsonIterator.deserialize(new String((byte[]) aMessage.data, "utf-8"), MqttMessage.class);
                if (msg == null) {
                    return;
                }
                emitter.onNext(msg);
                emitter.onComplete();
            }
        })
                .subscribeOn(Schedulers.io())
                .observeOn(Schedulers.io())
                .flatMap(new Function<MqttMessage, ObservableSource<MqttMessage>>() {
                    @Override
                    public ObservableSource<MqttMessage> apply(MqttMessage msg) throws Exception {
                        Log.e(TAG, "Received message key:"+msg.key+" msg:"+msg.body.m);
                        switch (msg.key) {
                            case "h": {//
                                SetHeartBeatDownstream setHeartBeatDownstream = msg.body.m.as(SetHeartBeatDownstream.class); // Communicate with the device and wait for a response from the devicereturn Device.setHeartBeat(setHeartBeatDownstream);
                            }
                            case "b": {// AddCoinsDownstream addCoinsDownstream = msg.body.m.as(AddCoinsDownstream.class); // Communicate with the device and wait for a response from the devicereturn Device.addCoins(addCoinsDownstream);
                            }
                            case "g": {// // communicates with the device and waits for a responsereturn Device.getParam();
                            }
                            case "s": {//
                                SetParamDownstream setParamDownstream = msg.body.m.as(SetParamDownstream.class); // Communicate with the device and wait for a response from the devicereturn Device.setParam(setParamDownstream); }}return Observable.never();
                    }
                })
                .observeOn(Schedulers.io())
                .map(new Function<MqttMessage, Boolean>() {
                    @Override
                    public Boolean apply(MqttMessage msg) throws Exception {
                        MqttPublishRequest res = new MqttPublishRequest();
                        res.isRPC = false;
                        res.topic = topic.replace("request"."response");
                        //res.msgId = topic.split("/") [6]; res.payloadObj = JsonStream.serialize(msg); LinkKit.getInstance().publish(res, newIConnectSendListener() {
                            @Override
                            public void onResponse(ARequest aRequest, AResponse aResponse) {
                            }
    
                            @Override
                            public void onFailure(ARequest aRequest, AError aError) {
                            }
                        });
                        return true;
                    }
                })
                .subscribe();
    }
    Copy the code

Android communicates with hardware

In the two devices of doll machine and lipstick challenge, we need to communicate with the devices, such as: doll machine coin, doll machine gift feedback, press the selected lipstick grid and so on, all of which need to communicate with the hardware module. In terms of the framework selection of serial communication, we mainly choose Google’s Android-Serialport-API to achieve. Original project Address

  • Depend on the way

    1. Add in the root build.gradle
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io'}}}Copy the code
    1. Child Module adds dependencies
    dependencies {
        implementation 'com. Making. Licheedev. Android - SerialPort - API: SerialPort: 1.0.1'
    }
    Copy the code
  • Modifying su Paths

// su the default path is"/system/bin/su"// Serialport.setsupath ("/system/xbin/su");
Copy the code
  • The connection method

When connecting the serial port, you need to specify the serial port number and baud rate, and then periodically process the instructions sent by the machine.

    static void init() throws IOException {
        SerialPort.setSuPath("/system/xbin/su"); SerialPort = new serialPort (config.serialport, config.baudrate); // set serialPort number and baudrate. / / receive instruction stream inputStream = serialPort. GetInputStream (); / / send the instruction stream outputStream = serialPort. GetOutputStream (); // Process machine information every 100ms observable. interval(100, TimeUnit.MILLISECONDS) .observeOn(serialScheduler) .subscribe(new Consumer<Long>() { @Override public void accept(Long Throws Exception {// Process the instruction sent by the machine handleRecv(); }}); }Copy the code
  • Send instructions to the machine

Sending instructions to the machine is implemented in combination with Rxjava. In addition, the instruction to the machine needs to have a specified format (internally formulated communication protocol), we send and receive data is a byte array, so our format needs to be carried out in strict accordance with the protocol we developed, the following is a simple example of the doll machine coin:

    static ObservableSource<MqttMessage> addCoins(final AddCoinsDownstream msg) {
        return Observable.create(new ObservableOnSubscribe<MqttMessage>() {
            @Override
            public void subscribe(ObservableEmitter<MqttMessage> emitter) throws Exception {
                currentUser = msg.u;
                currentHeadUrl = msg.h;
                currentNickname = msg.nk;
                byte[] buf = new byte[]{0x11, addCoinsCmd, msg.num, msg.c, 0, 0x00, 0x00};
                byte[] ret = sign(buf);
                try {
                    outputStream.write(ret);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                penddingCmd = addCoinsCmd;
                penddingEmitter = emitter;
            }
        })
                .subscribeOn(serialScheduler);
    }

Copy the code
  • Receiving machine instruction

The receiving of machine messages is carried out every 100ms. When processing machine instructions, invalid bytes should be filtered first, and then messages should be processed according to the protocols formulated by us, such as determining the score of the machine or the result of the game. Finally, CRC16 verification is carried out on the data returned from the machine.


    static void handleRecv() {
        try {
            for(; ;) { int len = inputStream.available();if (len <= 0) {
                    break;
                }
                len = inputStream.read(buf, bufReadOffset, buf.length - bufReadOffset);
                //Log.d("serialPort", String.format("read: %s", byteToHex(buf, bufReadOffset, len)));
                bufReadOffset += len;
                for(; ;) {if (bufParseEnd == -1) {
                        for (; bufParseStart < bufReadOffset; bufParseStart++) {
                            if (buf[bufParseStart] == (byte) 0xAA) {
                                bufParseEnd = bufParseStart + 1;
                                break; }}}if (bufParseEnd != -1) {
                        for (; bufParseEnd < bufReadOffset; bufParseEnd++) {
                            if (buf[bufParseEnd] == (byte) 0xAA) {
                                bufParseStart = bufParseEnd;
                                bufParseEnd += 1;
                                continue;
                            }
                            if (buf[bufParseEnd] == (byte) 0xDD) {
                                if (bufParseEnd - bufParseStart >= 5) {
                                    bufParseEnd += 1;
                                    byte size = buf[bufParseStart + 1];
                                    byte index = buf[bufParseStart + 2];
                                    byte cmd = buf[bufParseStart + 3];
                                    byte check = (byte) (size ^ index ^ cmd);
                                    for (int i = bufParseStart + 4; i < bufParseEnd - 2; i++) {
                                        check ^= buf[i];
                                    }
                                    if (check == buf[bufParseEnd - 2]) {
                                        //Log.d("serialPort", String.format("protocol: %s, size: %d, index: %d, cmd: %d, check: %d, data: %s", byteToHex(buf, bufParseStart, bufParseEnd - bufParseStart), size, index, cmd, check, byteToHex(buf, bufParseStart + 4, size - 3))); Switch (CMD) {// Heartbeatcase heartBeatCmd: {
                                            }
                                            break; / / pointscase addCoinsCmd: {
                                                
                                            }
                                            break; // Game resultscasegameResultCmd: { boolean gift = buf[bufParseStart + 7] ! = 0; IoT.sendGameResult(gift);ifWssender.getinstance ().sendUserInfo(currentUser, currentHeadUrl, currentNickname); }}break;
                                            default:
                                                break;
                                        }
                                    }
                                }
                                bufParseStart = bufParseEnd;
                                bufParseEnd = -1;
                                break; }}}if (bufParseStart >= bufReadOffset || bufParseEnd >= bufReadOffset) {
                        break; }}if (bufReadOffset == buf.length) {
                    System.arraycopy(buf, bufParseStart, buf, 0, bufReadOffset - bufParseStart);
                    if(bufParseEnd ! = -1) { bufParseEnd -= bufParseStart; bufReadOffset = bufParseEnd; }else{ bufReadOffset = 0; } bufParseStart = 0; } } } catch (IOException e) { e.printStackTrace(); }}Copy the code

Websocket communication

We choose Websocket for communication between central control and doll machine. The central control end is server, and then the doll machine is client.

server

  • Server implementation: the current Server implementation is only to receive the doll machine data feedback, so there is no complex operation.

class WSServer extends WebSocketServer {
    private MainActivity mainActivity;

    public void setMainActivity(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

    WSServer(InetSocketAddress address) {
        super(address);
    }

    @Override
    public void onOpen(WebSocket conn, ClientHandshake handshake) {
        mainActivity.appendAndScrollLog("Client:" + conn.getRemoteSocketAddress() + "Connected \n");
    }

    @Override
    public void onClose(WebSocket conn, int code, String reason, boolean remote) {
        mainActivity.appendAndScrollLog("Client:" + conn.getRemoteSocketAddress() + "Disconnected \n");
    }

    @Override
    public void onMessage(WebSocket conn, final String message) {
        Observable.create(new ObservableOnSubscribe<SocketMessage>() {
            @Override
            public void subscribe(ObservableEmitter<SocketMessage> emitter) throws Exception {
                final SocketMessage socketMessage = JsonIterator.deserialize(message, SocketMessage.class);
                emitter.onNext(socketMessage);
                emitter.onComplete();
            }
        })
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<SocketMessage>() {
                    @Override
                    public void accept(SocketMessage socketMessage) throws Exception {
                        if(socketmessage.getCode () == socketmessage.type_user) {// Clip to doll}else if(socketmessage.getCode () == socketmessage.type_hello) {// Connection greeting}}}); } @Override public void onError(WebSocket conn, Exception ex) { } @Override public voidonStart() {}}Copy the code
  • Simple use mode
    appendAndScrollLog("Initialize the WebSocket service... \n");
    WSServer wsServer = new WSServer(18104);
    wsServer.setMainActivity(MainActivity.this);
    wsServer.setConnectionLostTimeout(5);
    wsServer.setReuseAddr(true);
    wsServer.start();
    appendAndScrollLog("Initializing WebSocket service completed \n");
Copy the code

client

On the client side, the current operations to be done include disconnecting and reconnecting people and sending data. Disconnect and reconnect in a new child thread. Otherwise, the following error occurs:

You cannot initialize a reconnect out of the websocket thread. Use reconnect in another thread to insure a successful cleanup
Copy the code

Therefore, we need to do it in a new child thread every time we disconnect and restart. In addition, when sending data, if the socket is not connected, it will report an exception. Therefore, when we have data to send if the socket is not connected, we will cache the data locally first, and then send the stranded data out once the socket is connected.

  • Depend on the configuration
    implementation 'org. Java - websocket: Java - websocket: 1.3.9'
Copy the code
  • WSClient.java

class WSClient extends WebSocketClient {

    private static final String TAG = "WSClient"; private static WSClient instance; private static URI sUri; private WSReceiver mWSReceiver; private Disposable mReconnectDisposable; private ConnectCallback mConnectCallback; /** * step 1: set url * @param uri */ public static voidsetUri(URI uri){
        sUri = uri;
    }

    /**
     * step 1:
     * 需要先调用,设置服务端的 url
     * @param ipAddress
     * @param port
     */
    public static void setUri(String ipAddress,int port){
        try {
            sUri = new URI(String.format("ws://%s:%d", ipAddress, port));
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }


    public static WSClient getInstance() {if (instance == null) {
            synchronized (WSClient.class){
                if(instance == null) { instance = new WSClient(sUri); }}}returninstance; } /** * step 2: connect websocket */ public voidonConnect() {setConnectionLostTimeout(Config.instance.webSocketTimeoutSeconds);
        setReuseAddr(true); connect(); } private WSClient(URI server) { super(server); // Initialize the message sender wsSender.getInstance ().setwsclient (this); // Initialize message receiver mWSReceiver = new WSReceiver(); mWSReceiver.setWSClient(this); mWSReceiver.setWSSender(WSSender.getInstance()); } @Override public void onOpen(ServerHandshake handshakedata) { Log.d(TAG,"onOpen: ");
        MainActivity.appendAndScrollLog("Websocket connected \n");
        Observable.just("")
                .subscribeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Object>() {
                    @Override
                    public void accept(Object o) throws Exception {
                        if(mConnectCallback ! = null) { mConnectCallback.onWebsocketConnected(); }}}); Wssender.getinstance ().clearallMessage (); } @Override public void onMessage(String message) { Log.d(TAG,"onMessage: ");
        mWSReceiver.handlerMessage(message);
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {
        Log.d(TAG, "onClose: ");
        MainActivity.appendAndScrollLog(String.format("Websocket disconnected, cause: %s\n",reason));
        Observable.just("")
                .subscribeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Object>() {
                    @Override
                    public void accept(Object o) throws Exception {
                        if(mConnectCallback ! = null) { mConnectCallback.onWebsocketClosed(); }}}); onReconnect(); } @Override public void onError(Exception ex) {if(ex ! = null) { Log.d(TAG,"onError: "+ex.getMessage());
            MainActivity.appendAndScrollLog(String.format("Websocket error: %s\n",ex.getMessage()));
        }
        onReconnect();
    }


    public void onReconnect() {
        if(mReconnectDisposable ! = null && ! mReconnectDisposable.isDisposed()){return;
        }
        mReconnectDisposable = Observable.timer(1, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Exception {
                        Log.d(TAG, "websocket reconnect"); WSClient.this.reconnect(); mReconnectDisposable.dispose(); }}); } public voidsetConnectCallback(ConnectCallback mConnectCallback) { this.mConnectCallback = mConnectCallback; } public interface ConnectCallback{ void onWebsocketConnected(); void onWebsocketClosed(); }}Copy the code
  • WSSender.java
/** * Created by runla on 2018/10/26. Public class WSSender {private static Final String TAG ="WSSender"; public static final int MAX_MESSAGE_COUNT = 128; private static WSSender instance; private WSClient mWSClientManager; Private LinkedList<String> mMessageList = new LinkedList<>(); privateWSSender() {
    }

    public static WSSender getInstance() {
        if (instance == null) {
            synchronized (WSSender.class) {
                if(instance == null) { instance = new WSSender(); }}}return instance;
    }

    public void setWSClient(WSClient wsClientManager) { this.mWSClientManager = wsClientManager; } /** * send all stranded messages */ public voidclearAllMessage() {
        if (mWSClientManager == null) {
            return;
        }

        while(mMessageList.size() > 0 && mMessageList.getFirst() ! = null) { Log.d(TAG,"sendMessage: "+ mMessageList.size()); mWSClientManager.send(mMessageList.getFirst()); mMessageList.removeFirst(); }} /** * send a message, if the message is not sent, then wait until the connection is successful to try again to send ** @param MSG * @return
     */
    public boolean sendMessage(String msg) {
        if (mWSClientManager == null) {
            throw new NullPointerException("websocket client is null");
        }
        if (TextUtils.isEmpty(msg)) {
            return false; } // Add the data to the end of the queue mmessagelist.addlast (MSG);while(mMessageList.size() > 0 && mMessageList.getFirst() ! = null) { Log.d(TAG,"sendMessage: " + mMessageList.size());
            if(! MWSClientManager. IsOpen ()) {/ / try rewiring mWSClientManager onReconnect ();break;
            } else{ mWSClientManager.send(mMessageList.getFirst()); mMessageList.removeFirst(); }} // If the size of the message queue exceeds the maximum size we set, remove the message that was added firstif (mMessageList.size() >= MAX_MESSAGE_COUNT) {
            mMessageList.removeFirst();
        }
        return false; }}Copy the code
  • WSReceiver.java
/** * Created by runla on 2018/10/26. Public class WSReceiver {private WSClient mWSClientManager; private WSSender mWSSender; private OnMessageCallback onMessageCallback; publicWSReceiver() {
    }


    public void setWSClient(WSClient mWSClientManager) {
        this.mWSClientManager = mWSClientManager;
    }

    public void setWSSender(WSSender mWSSender) { this.mWSSender = mWSSender; Public void handlerMessage(String message){public void handlerMessage(String message){if(onMessageCallback ! = null){ onMessageCallback.onHandlerMessage(message); } } public voidsetOnMessageCallback(OnMessageCallback onMessageCallback) { this.onMessageCallback = onMessageCallback; } public interface OnMessageCallback{ void onHandlerMessage(String message); }}Copy the code
  • Connect call
    appendAndScrollLog("Initialize the WebSocket client... \n");
    WSClient.setUri( Config.instance.centralServerAddress, Config.instance.webSocketPort);
    WSClient.getInstance().onConnect();
    WSClient.getInstance().setConnectCallback(MainActivity.this);
    appendAndScrollLog("Initializing WebSocket client completed \n");

Copy the code
  • Data sent
Wssender.getinstance ().clearallMessage (); Wssender.getinstance ().sendMessage(MSG);Copy the code

Database storage

In the middle console, we need to display the ranking version, which is used to display the ranking of the dolls in this month and this week. Therefore, we need to save the number of dolls in the middle console and other personal information of the user. GreenDAO is an open source ORM framework for Android, which is portable and fast. By mapping Java objects to SQLite databases, we do not need to write complex SQL statements when operating databases. In terms of performance, GreenDAO is highly optimized for Android, with minimal memory overhead, small dependency volume, and database encryption support. I’m not going to do the use of GreenDAO here, see the official GreenDAO website for details.


Write in the last

About the architecture of the whole system has met a lot of pit, in the process of building the above is my part of the solution for this project, all the current is not possible to write and the project is now in xi ‘an and chengdu and other places have stores points, according to the feedback, the profit is great, but this type of project bonus period is too long, estimate is about 2 ~ 3 years. If you need us to provide solutions for the development of lipstick machine or doll machine, please contact us. At present, we have relatively mature solutions in this aspect.