Kaynağa Gözat

Merge commit '48e85fda607af559d4dac3488f4bc68f550dbb3b'

AY 4 yıl önce
ebeveyn
işleme
e6976f4bae
42 değiştirilmiş dosya ile 820 ekleme ve 28 silme
  1. 1 0
      .gitignore
  2. BIN
      documents/Derivatives/1_110 release mouse pads.png
  3. BIN
      documents/Derivatives/1_110 release mouse pads.psd
  4. 155 0
      examples/HomeDynamic2/OnOff/OnOff.ino
  5. 16 0
      examples/HomeDynamic2/OnOff/helper.ino
  6. 2 2
      file_system.go
  7. 20 0
      mod/iot/handlerManager.go
  8. 36 7
      mod/iot/hds/hds.go
  9. 1 1
      mod/iot/hds/utils.go
  10. 13 1
      mod/iot/hdsv2/hdsv2.go
  11. 1 0
      mod/iot/iot.go
  12. 174 0
      mod/iot/sonoff_s2x/sonoff_s2x.go
  13. 37 0
      mod/iot/sonoff_s2x/utils.go
  14. 7 0
      mod/storage/storage.go
  15. BIN
      web/SystemAO/desktop/img/baseline_brush_black_48dp.png
  16. BIN
      web/SystemAO/desktop/img/personalization.png
  17. BIN
      web/SystemAO/desktop/img/personalization.psd
  18. 319 0
      web/SystemAO/desktop/personalization.html
  19. BIN
      web/SystemAO/iot/hub/img/devices/XD28.png
  20. BIN
      web/SystemAO/iot/hub/img/devices/baseline_bug_report_white_48dp.png
  21. BIN
      web/SystemAO/iot/hub/img/devices/camera.png
  22. BIN
      web/SystemAO/iot/hub/img/devices/default.psd
  23. BIN
      web/SystemAO/iot/hub/img/devices/dht11.png
  24. BIN
      web/SystemAO/iot/hub/img/devices/esp32cam.png
  25. BIN
      web/SystemAO/iot/hub/img/devices/fridge.png
  26. BIN
      web/SystemAO/iot/hub/img/devices/gateway.png
  27. BIN
      web/SystemAO/iot/hub/img/devices/moisture.png
  28. BIN
      web/SystemAO/iot/hub/img/devices/panel.png
  29. BIN
      web/SystemAO/iot/hub/img/devices/peltier.png
  30. BIN
      web/SystemAO/iot/hub/img/devices/relay.png
  31. BIN
      web/SystemAO/iot/hub/img/devices/router.png
  32. BIN
      web/SystemAO/iot/hub/img/devices/sonoff_s26.png
  33. BIN
      web/SystemAO/iot/hub/img/devices/switch.png
  34. BIN
      web/SystemAO/iot/hub/img/devices/test.png
  35. BIN
      web/SystemAO/iot/hub/img/devices/test.psd
  36. BIN
      web/SystemAO/iot/hub/img/devices/unknown.png
  37. BIN
      web/SystemAO/iot/hub/img/devices/weather.png
  38. 19 6
      web/SystemAO/iot/hub/index.html
  39. 15 2
      web/desktop.system
  40. BIN
      web/img/desktop/bg/nobg.jpg
  41. BIN
      web/img/desktop/bg/nobg.psd
  42. 4 9
      web/mobile.system

+ 1 - 0
.gitignore

@@ -50,3 +50,4 @@ aroz_online
 arozos.exe
 */arozos.exe
 arozos
+upx.exe

BIN
documents/Derivatives/1_110 release mouse pads.png


BIN
documents/Derivatives/1_110 release mouse pads.psd


+ 155 - 0
examples/HomeDynamic2/OnOff/OnOff.ino

@@ -0,0 +1,155 @@
+/*
+ * Home Dynamic System v2
+ * Designed by tobychui
+ * 
+ * This is a basic IoT Switch that support single channel ON / OFF function only
+ * 
+ */
+
+#include <ESP8266WiFi.h>        // Include the Wi-Fi library
+#include <ESP8266WiFiMulti.h>   // Include the Wi-Fi-Multi library
+#include <ESP8266mDNS.h>        // Include the mDNS library
+#include <ESP8266WebServer.h>   // Include the WebServer library
+
+//Change the properties of the IoT device
+const String DeviceName = "Switch";     //The name of this IoT device
+const int ListeningPort = 12110;        //The port where this IoT device listen
+int signalOutputPin = LED_BUILTIN;      //The pin to activate during on, default LED pins as demo
+bool poweredOn = true;                  //The current power state of the switch
+
+//Library Objects
+ESP8266WiFiMulti wifiMulti;                // Create an instance of the ESP8266WiFiMulti class, called 'wifiMulti'
+ESP8266WebServer server(ListeningPort);    //Create an Web Server on the listening port
+
+//Change the WiFi Settings
+void WiFiConfig(){
+  wifiMulti.addAP("Toby Room Automation", "homedynamicsystem"); 
+  //Add more WiFi AP here if nessary
+  //wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2");
+  //wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3");
+}
+
+//Inject zeroconf attr into the MDNS respond (For scanning by ArozOS)
+void MDNSDynamicServiceTxtCallback(const MDNSResponder::hMDNSService p_hService) {
+    //Define the domain of the HDSv2 devices
+    MDNS.addDynamicServiceTxt(p_hService, "domain","hds.arozos.com");
+    MDNS.addDynamicServiceTxt(p_hService, "protocol","hdsv2");
+
+    //Define the OEM written values
+    MDNS.addDynamicServiceTxt(p_hService, "uuid",getMacAddress());
+    MDNS.addDynamicServiceTxt(p_hService, "model","Switch");
+    MDNS.addDynamicServiceTxt(p_hService, "vendor","HomeDynamic Project");
+    MDNS.addDynamicServiceTxt(p_hService, "version_minor","0.00");
+    MDNS.addDynamicServiceTxt(p_hService, "version_build","0");
+}
+  
+
+void hostProbeResult(String p_pcDomainName, bool p_bProbeResult) {
+  MDNS.setDynamicServiceTxtCallback(MDNSDynamicServiceTxtCallback);
+}
+
+void setup() {
+  //Use 115200 baudrate on serial monitor if you want to see what is happening to the device
+  Serial.begin(115200);
+  delay(10);
+  Serial.println('\n');
+
+  //Set output pins as OUTPUT and default it to HIGH
+  pinMode(signalOutputPin, OUTPUT);
+  digitalWrite(signalOutputPin, HIGH);
+ 
+  //Start WiFi Conenction Routines
+  WiFiConfig();
+  
+  Serial.println("Connecting ...");
+  while (wifiMulti.run() != WL_CONNECTED) {
+    delay(500);
+    Serial.print('.');
+  }
+  Serial.println('\n');
+  Serial.print("Connected to ");
+  Serial.println(WiFi.SSID());
+  Serial.print("IP address:\t");
+  Serial.println(WiFi.localIP());
+  
+  //Startup MDNS Responder
+  MDNS.setHostProbeResultCallback(hostProbeResult);
+  
+  if (!MDNS.begin(DeviceName)) {             // Start the mDNS responder for esp8266.local
+    Serial.println("Error setting up MDNS responder!");
+  }
+
+  //Advertise the port that you are using
+  MDNS.addService("http", "tcp", ListeningPort);
+  Serial.println("mDNS responder started");
+
+  //Startup the Web Server Endpoints
+  delay(100);
+  server.on("/", handle_index);
+  server.on("/status", handle_status);
+  server.on("/eps", handle_endpoints);
+  server.on("/on", handle_on);
+  server.on("/off", handle_off);
+  
+  server.begin();
+  Serial.println("HTTP server started");
+  Serial.print("Listening on port: ");
+  Serial.println(ListeningPort);
+}
+
+//Handlers for Web Server
+void handle_index() {
+  server.send(200, "text/html", ""); 
+}
+
+//Handle turning on the switch
+void handle_on() {
+  Serial.println("Turned ON");
+  digitalWrite(signalOutputPin, HIGH);
+  poweredOn = true;
+  server.send(200, "text/html", "OK"); 
+}
+
+//Handle turning off the switch
+void handle_off() {
+  Serial.println("Turned OFF");
+  digitalWrite(signalOutputPin, LOW);
+  poweredOn = false;
+  server.send(200, "text/html", "OK"); 
+}
+
+
+
+void handle_status() {
+  String powerState = "ON";
+  if (poweredOn == false){
+    powerState = "OFF";
+  }
+  server.send(200, "application/json", "{\"Power\":\"" + powerState + "\"\}"); 
+}
+
+void handle_endpoints() {
+  server.send(200, "application/json", "[{\
+  \"Name\": \"ON\",\
+  \"RelPath\":\"on\",\
+  \"Desc\":\"Switch on the device attached to the switch\",\
+  \"Type\":\"none\",\
+  \"AllowRead\":false,\
+  \"AllowWrite\":true\
+},{\
+  \"Name\": \"OFF\",\
+  \"RelPath\":\"off\",\
+  \"Desc\":\"Switch off the device attached to the switch\",\
+  \"Type\":\"none\",\
+  \"AllowRead\":false,\
+  \"AllowWrite\":true\
+}\
+]"); 
+}
+
+
+//Main Loop
+void loop() { 
+   server.handleClient();
+   MDNS.update();
+ }

+ 16 - 0
examples/HomeDynamic2/OnOff/helper.ino

@@ -0,0 +1,16 @@
+//Get MAC Address of the ESP8266, require WiFi
+String MACString;
+const char* getMacAddress(){
+  unsigned char mac[6];
+  WiFi.macAddress(mac);
+  MACString = "";
+  for (int i = 0; i < 6; ++i) {
+    MACString += String(mac[i], 16);
+    if (i < 5){
+      MACString += '-';
+    }
+  }
+  
+  const char* _result = MACString.c_str();
+  return _result;
+}

+ 2 - 2
file_system.go

@@ -1766,8 +1766,8 @@ func system_fs_listRoot(w http.ResponseWriter, r *http.Request) {
 			IsDir    bool
 		}
 		//List the root media folders under user:/
-		var filesInUserRoot []fileObject
-		filesInRoot, _ := filepath.Glob(*root_directory + "users/" + username + "/*")
+		filesInUserRoot := []fileObject{}
+		filesInRoot, _ := filepath.Glob(filepath.ToSlash(filepath.Clean(*root_directory)) + "/users/" + username + "/*")
 		for _, file := range filesInRoot {
 			thisFile := new(fileObject)
 			thisFile.Filename = filepath.Base(file)

+ 20 - 0
mod/iot/handlerManager.go

@@ -63,6 +63,26 @@ func (m *Manager) GetDeviceByID(devid string) *Device {
 	return nil
 }
 
+//Handle listing of all avaible scanner and its stats
+func (m *Manager) HandleIconLoad(w http.ResponseWriter, r *http.Request) {
+	devid, err := mv(r, "devid", false)
+	if err != nil {
+		sendErrorResponse(w, "Invalid device id")
+		return
+	}
+
+	//Get device icon from handler
+	targetDevice := m.GetDeviceByID(devid)
+	iconName := targetDevice.Handler.Icon(targetDevice)
+
+	iconFilePath := "./web/SystemAO/iot/hub/img/devices/" + iconName + ".png"
+	if fileExists(iconFilePath) {
+		http.ServeFile(w, r, iconFilePath)
+	} else {
+		http.ServeFile(w, r, "./web/SystemAO/iot/hub/img/devices/unknown.png")
+	}
+}
+
 //Handle listing of all avaible scanner and its stats
 func (m *Manager) HandleExecute(w http.ResponseWriter, r *http.Request) {
 	devid, err := mv(r, "devid", true)

+ 36 - 7
mod/iot/hds/hds.go

@@ -6,6 +6,7 @@ import (
 	"strconv"
 	"strings"
 	"sync"
+	"time"
 
 	"imuslab.com/arozos/mod/iot"
 )
@@ -76,6 +77,7 @@ func (h *Handler) Scan() ([]*iot.Device, error) {
 	//Create a IP scanner
 	var wg sync.WaitGroup
 	for i := 1; i < 256; i++ {
+		time.Sleep(300 * time.Microsecond)
 		wg.Add(1)
 		go func(wg *sync.WaitGroup) {
 			defer wg.Done()
@@ -104,6 +106,22 @@ func (h *Handler) Scan() ([]*iot.Device, error) {
 				deviceState["status"] = strings.TrimSpace(statusText)
 			}
 
+			//Create the hdsv1 endpoints (aka /on and /off)
+			endpoints := []*iot.Endpoint{}
+			endpoints = append(endpoints, &iot.Endpoint{
+				RelPath: "on",
+				Name:    "ON",
+				Desc:    "Turn on the device",
+				Type:    "none",
+			})
+			endpoints = append(endpoints, &iot.Endpoint{
+				RelPath: "off",
+				Name:    "OFF",
+				Desc:    "Turn off the device",
+				Type:    "none",
+			})
+
+			//Append the device to list
 			results = append(results, &iot.Device{
 				Name:         devName,
 				Port:         80, //HDS device use port 80 by default
@@ -112,11 +130,12 @@ func (h *Handler) Scan() ([]*iot.Device, error) {
 				Manufacturer: "Generic",
 				DeviceUUID:   uuid,
 
-				IPAddr:         targetIP,
-				RequireAuth:    false,
-				RequireConnect: false,
-				Status:         deviceState,
-				Handler:        h,
+				IPAddr:           targetIP,
+				RequireAuth:      false,
+				RequireConnect:   false,
+				Status:           deviceState,
+				Handler:          h,
+				ControlEndpoints: endpoints,
 			})
 
 			log.Println("*HDS* Found device ", devName, " at ", targetIP, " with UUID ", uuid)
@@ -138,10 +157,20 @@ func (h *Handler) Disconnect(device *iot.Device) error {
 	return nil
 }
 
+//Get the icon filename of the device, it is always switch for hdsv1
+func (h *Handler) Icon(device *iot.Device) string {
+	return "switch"
+}
+
 //Get the status of the device
 func (h *Handler) Execute(device *iot.Device, endpoint *iot.Endpoint, payload interface{}) (interface{}, error) {
-	var result interface{}
-	return result, nil
+	//GET request the target device endpoint
+	resp, err := tryGet("http://" + device.IPAddr + ":" + strconv.Itoa(device.Port) + "/" + endpoint.RelPath)
+	if err != nil {
+		return map[string]interface{}{}, err
+	}
+
+	return resp, nil
 }
 
 //Get the status of the device

+ 1 - 1
mod/iot/hds/utils.go

@@ -19,7 +19,7 @@ func isJSON(s string) bool {
 
 func tryGet(url string) (string, error) {
 	client := http.Client{
-		Timeout: 10 * time.Second,
+		Timeout: 5 * time.Second,
 	}
 
 	resp, err := client.Get(url)

+ 13 - 1
mod/iot/hdsv2/hdsv2.go

@@ -46,7 +46,7 @@ func (h *Handler) Scan() ([]*iot.Device, error) {
 	hosts := h.scanner.Scan(3, "hds.arozos.com")
 	for _, host := range hosts {
 		thisDevice := iot.Device{
-			Name:         host.HostName,
+			Name:         strings.Title(strings.ReplaceAll(host.HostName, ".local.", "")),
 			Port:         host.Port,
 			Model:        host.Model,
 			Version:      host.BuildVersion + "-" + host.MinorVersion,
@@ -99,6 +99,18 @@ func (h *Handler) Status(device *iot.Device) (map[string]interface{}, error) {
 	return getStatusForDevice(device)
 }
 
+//Get the icon filename of the device
+func (h *Handler) Icon(device *iot.Device) string {
+	devModel := device.Model
+	if devModel == "Switch" {
+		return "switch"
+	} else if devModel == "Test Unit" {
+		return "test"
+	} else {
+		return "unknown"
+	}
+}
+
 //Get the status of the device
 func (h *Handler) Execute(device *iot.Device, endpoint *iot.Endpoint, payload interface{}) (interface{}, error) {
 	var result interface{}

+ 1 - 0
mod/iot/iot.go

@@ -69,4 +69,5 @@ type ProtocolHandler interface {
 	Execute(device *Device, endpoint *Endpoint, payload interface{}) (interface{}, error) //Execute an endpoint for a device
 	Disconnect(device *Device) error                                                      //Disconnect from a device connection
 	Stats() Stats                                                                         //Return the properties and status of the Protocol Handler
+	Icon(device *Device) string                                                           //Get the icon of the device, see iot/hub/img/devices for a list of icons
 }

+ 174 - 0
mod/iot/sonoff_s2x/sonoff_s2x.go

@@ -0,0 +1,174 @@
+package sonoff_s2x
+
+import (
+	"log"
+	"regexp"
+	"strings"
+
+	"imuslab.com/arozos/mod/iot"
+	"imuslab.com/arozos/mod/network/mdns"
+)
+
+/*
+	Sonoff S2X Module
+
+	This is a module that handles Sonoff Tasmota 6.4.1(sonoff)
+	Core version: 2_4_2/2.2.1(cfd48f3)
+
+	See https://github.com/arendst/Tasmota for source code
+
+	mDNS must be set to enable in order to use this scanner
+*/
+
+type Handler struct {
+	scanner      *mdns.MDNSHost
+	lastScanTime int64
+}
+
+//Create a new Sonoff S2X Protocol Handler
+func NewProtocolHandler(scanner *mdns.MDNSHost) *Handler {
+	//Create a new MDNS Host
+	return &Handler{
+		scanner,
+		0,
+	}
+}
+
+func (h *Handler) Start() error {
+	log.Println("*IoT* Sonoff Tasmoto S2X 6.4 scanner loaded")
+	return nil
+}
+
+func (h *Handler) Scan() ([]*iot.Device, error) {
+	results := []*iot.Device{}
+	scannedDevices := h.scanner.Scan(10, "")
+	for _, dev := range scannedDevices {
+		if dev.Port == 80 {
+			//This things has web UI. Check if it is sonoff by grabbing its index
+			value, err := tryGet("http://" + dev.IPv4[0].String() + "/")
+			if err != nil {
+				//This things is not sonoff smart socket
+				log.Println(dev.HostName + " is not sonoff")
+				continue
+			}
+
+			//Check if the return value contains the keyword:
+			if strings.Contains(value, "Sonoff-Tasmota") {
+				//This is sonoff device!
+				//Extract its MAC Address from Web UI
+				info, err := tryGet("http://" + dev.IPv4[0].String() + "/in")
+				if err != nil {
+					//This things is not sonoff smart socket
+					log.Println(dev.HostName + " failed to extract its MAC address from /in page")
+					continue
+				}
+
+				//Try to seperate the MAC address out
+				//I have no idea what I am doing here
+				re := regexp.MustCompile("[[:alnum:]][[:alnum:]]:[[:alnum:]][[:alnum:]]:[[:alnum:]][[:alnum:]]:[[:alnum:]][[:alnum:]]:[[:alnum:]][[:alnum:]]:[[:alnum:]][[:alnum:]]")
+				match := re.FindStringSubmatch(info)
+				deviceMAC := ""
+				if len(match) > 0 {
+					deviceMAC = match[0]
+				} else {
+					//Can't find MAC address for no reason?
+					continue
+				}
+
+				//Try to get the device status
+				status, err := tryGet("http://" + dev.IPv4[0].String() + "/ay")
+				if err != nil {
+					continue
+				}
+
+				devStatus := map[string]interface{}{}
+				if strings.Contains(status, "ON") {
+					//It is on
+					devStatus["Power"] = "ON"
+				} else {
+					//It is off
+					devStatus["Power"] = "OFF"
+				}
+
+				toggleEndpoint := iot.Endpoint{
+					RelPath: "ay?o=1",
+					Name:    "Toggle Power",
+					Desc:    "Toggle the power of the smart switch",
+					Type:    "none",
+				}
+
+				results = append(results, &iot.Device{
+					Name:         strings.Title(strings.ReplaceAll(dev.HostName, ".local.", "")),
+					Port:         80,
+					Model:        "Sonoff S2X Smart Switch",
+					Version:      "",
+					Manufacturer: "Sonoff",
+					DeviceUUID:   deviceMAC,
+
+					IPAddr:           dev.IPv4[0].String(),
+					RequireAuth:      false,
+					RequireConnect:   false,
+					Status:           devStatus,
+					ControlEndpoints: []*iot.Endpoint{&toggleEndpoint},
+					Handler:          h,
+				})
+			} else {
+				continue
+			}
+		}
+	}
+	return results, nil
+}
+
+func (h *Handler) Connect(device *iot.Device, authInfo *iot.AuthInfo) error {
+	return nil
+}
+
+func (h *Handler) Disconnect(device *iot.Device) error {
+	return nil
+}
+
+func (h *Handler) Status(device *iot.Device) (map[string]interface{}, error) {
+	//Try to get the device status
+	status, err := tryGet("http://" + device.IPAddr + "/ay")
+	if err != nil {
+		return map[string]interface{}{}, err
+	}
+
+	devStatus := map[string]interface{}{}
+	if strings.Contains(status, "ON") {
+		//It is on
+		devStatus["Power"] = "ON"
+	} else {
+		//It is off
+		devStatus["Power"] = "OFF"
+	}
+	return devStatus, nil
+}
+
+func (h *Handler) Icon(device *iot.Device) string {
+	return "switch"
+}
+
+func (h *Handler) Execute(device *iot.Device, endpoint *iot.Endpoint, payload interface{}) (interface{}, error) {
+	results, err := tryGet("http://" + device.IPAddr + "/" + endpoint.RelPath)
+	if err != nil {
+		return nil, err
+	}
+
+	results = strings.ReplaceAll(results, "{t}", "")
+	return results, nil
+}
+
+func (h *Handler) Stats() iot.Stats {
+	return iot.Stats{
+		Name:          "Sonoff Tasmota",
+		Desc:          "Tasmota firmware for Sonoff S2X devices",
+		Version:       "1.0",
+		ProtocolVer:   "1.0",
+		Author:        "tobychui",
+		AuthorWebsite: "http://imuslab.com",
+		AuthorEmail:   "[email protected]",
+		ReleaseDate:   1616944405,
+	}
+}

+ 37 - 0
mod/iot/sonoff_s2x/utils.go

@@ -0,0 +1,37 @@
+package sonoff_s2x
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"strconv"
+	"time"
+)
+
+func isJSON(s string) bool {
+	var js map[string]interface{}
+	return json.Unmarshal([]byte(s), &js) == nil
+}
+
+func tryGet(url string) (string, error) {
+	client := http.Client{
+		Timeout: 10 * time.Second,
+	}
+
+	resp, err := client.Get(url)
+	if err != nil {
+		return "", err
+	}
+
+	if resp.StatusCode != 200 {
+		return "", errors.New("Server side return status code " + strconv.Itoa(resp.StatusCode))
+	}
+
+	content, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+
+	return string(content), nil
+}

+ 7 - 0
mod/storage/storage.go

@@ -9,6 +9,8 @@ package storage
 */
 
 import (
+	"os"
+
 	fs "imuslab.com/arozos/mod/filesystem"
 )
 
@@ -25,6 +27,11 @@ type StoragePool struct {
 	3. denied
 */
 
+//Create all the required folder structure if it didn't exists
+func init() {
+	os.MkdirAll("./system/storage", 0755)
+}
+
 //Create a new StoragePool objects with given uuids
 func NewStoragePool(fsHandlers []*fs.FileSystemHandler, owner string) (*StoragePool, error) {
 	//Move all fshandler into the storageHandler

BIN
web/SystemAO/desktop/img/baseline_brush_black_48dp.png


BIN
web/SystemAO/desktop/img/personalization.png


BIN
web/SystemAO/desktop/img/personalization.psd


+ 319 - 0
web/SystemAO/desktop/personalization.html

@@ -0,0 +1,319 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta name="mobile-web-app-capable" content="yes">
+        <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
+        <meta charset="UTF-8">
+        <link rel="stylesheet" href="../../script/semantic/semantic.min.css">
+        <script src="../../script/jquery.min.js"></script>
+        <script src="../../script/semantic/semantic.min.js"></script>
+        <script src="../../script/ao_module.js"></script>
+        <style>
+            .hidden{
+                display:none;
+            }
+
+            .backgroundpreview{
+                border: 1px solid #898989;
+            }
+        </style>
+    </head>
+    <body>
+        <div class="ui tabular menu" style="position:fixed; top:0px; left:0px; width: 100%;">
+            <div class="active item" data-tab="wallpaper">Wallpaper</div>
+            <div class="item" data-tab="theme">Theme</div>
+            <div class="item" data-tab="advance">Advance</div>
+        </div>
+        <div style="position:fixed; top: 42px; left:0px; width: 100%; height: calc(100% - 42px); overflow-y:auto;">
+            <div class="ui active tab" data-tab="wallpaper">
+                <!-- Wallpaper Functions -->
+                <br>
+                <div class="ui container">
+                    <h3 class="ui header">
+                        <i class="image outline icon"></i>
+                        <div class="content">
+                            Wallpapers
+                            <div class="sub header">Manage your desktop preferences</div>
+                        </div>
+                    </h3>
+                    <div class="ui divider"></div>
+
+                    <div class="ui grid">
+                        <div class="ten wide column">
+                            <img id="mainBackground" class="ui fluid image backgroundpreview" src="">
+                        </div>
+                        <div class="six wide column">
+                            
+                        </div>
+                    </div>
+                
+                    <div class="ui divider"></div>
+                    <div id="backgroundPreviewList" class="ui grid">
+                        <div class="four wide column">
+                            <img class="ui fluid image backgroundpreview" src="">
+                        </div>
+                        <div class="four wide column">
+                            <img class="ui fluid image backgroundpreview" src="">
+                        </div>
+                        <div class="four wide column">
+                            <img class="ui fluid image backgroundpreview" src="">
+                        </div>
+                        <div class="four wide column">
+                            <img class="ui fluid image backgroundpreview" src="">
+                        </div>
+                    </div>
+
+                    <div class="ui divider"></div>
+                    <h4 class="ui header">
+                        Background Wallpaper
+                        <div class="sub header">Set your desktop background wallpaper theme.</div>
+                    </h4>
+                    <select id="wallpaperlist" class="ui fluid dropdown" onchange="handleBackgroundSelectionChange(this.value);">
+                        <option value="">Wallpaper Packs</option>
+                    </select>
+                    <br>
+                    <button class="ui small green right floated button" onclick="applyWallpaper();"><i class="checkmark icon"></i> Apply Wallpaper</button>
+                    <br><br><br>
+                    <div class="ui green segment" style="display:none" id="wallpaperChangeConfirm">
+                        <h4 class="ui header">
+                            <i class="checkmark green icon"></i>
+                            <div class="content">
+                            Wallpaper Updated
+                            <div class="sub header">You should be seeing your desktop wallpaper change in a moment.</div>
+                            </div>
+                        </h4>
+                    </div>
+
+                    <!-- Wallpaper change interval-->
+                    <div class="ui divider"></div>
+                    <h4 class="ui header">
+                        Wallpaper Interval
+                        <div class="sub header">Set the interval between the wallpaper image cycles.</div>
+                    </h4>
+                    <select id="changeInterval" class="ui fluid dropdown" onchange="handleIntervalChange(this.value);">
+                        <option value="10">10 seconds</option>
+                        <option value="30">30 seconds</option>
+                        <option value="60">60 seconds</option>
+                        <option value="180">3 minutes</option>
+                        <option value="300">5 minutes</option>
+                        <option value="600">10 minutes</option>
+                        <option value="1800">30 minutes</option>
+                        <option value="3600">1 hour</option>
+                    </select>
+
+                    <div class="ui green segment" style="display:none" id="interfaceChangeConfirm">
+                        <h4 class="ui header">
+                            <i class="checkmark green icon"></i>
+                            <div class="content">
+                            Wallpaper Interval Updated
+                            <div class="sub header">This setting will only apply to this browser</div>
+                            </div>
+                        </h4>
+                    </div>
+                </div>
+                <br><br><br>
+            </div>
+            <div class="ui tab" data-tab="theme">
+                <!-- Theme Color Related !-->
+                <br>
+                <div class="ui container">
+                    <h3 class="ui header">
+                        <i class="paint brush icon"></i>
+                        <div class="content">
+                            Theme Color
+                            <div class="sub header">Change the system theme color settings</div>
+                        </div>
+                    </h3>
+
+                    <div class="ui divider"></div>
+                    <p>Work In Progress</p>
+                </div>
+            </div>
+            <div class="ui tab" data-tab="advance">
+                <!-- Advance User Customization !-->
+                <br>
+                <div class="ui container">
+                    <h3 class="ui header">
+                        <i class="setting icon"></i>
+                        <div class="content">
+                            Advance Customization
+                            <div class="sub header">Manage your desktop preferences</div>
+                        </div>
+                    </h3>
+                    <div class="ui divider"></div>
+                    <h4 class="ui header">
+                        User Defined Wallpaper
+                        <div class="sub header">Advanced user customization function for desktop interface</div>
+                    </h4>
+                    <h3 id="userSelectedFolderPath">Disabled</h3>
+                    <p>If you have set a folder for loading desktop wallpapers, the image files from that folder will be used instead of the system build in wallpapers.</p>
+                    <button class="ui small right floated button" onclick="clearUserSelectedFolder();"><i class="remove icon"></i> Clear Selection</button>
+                    <button class="ui small black right floated button" onclick="selectUserFolder();"><i class="folder open icon"></i> Select Folder</button>
+        
+                    <br><br>
+                </div>
+                
+            </div>
+        </div>
+        <br><br>
+        <script>
+            var desktopThemeList = [];
+            var isStartingUp = true;
+
+            $(".dropdown").dropdown();
+            $('.tabular.menu .item').tab();
+
+
+            //Startup process
+            initCurrentBackgroundPreview();
+            initDefaultBackgroundChangeValue();
+            initUserDefinedWallpaperFolder();
+
+            function initUserDefinedWallpaperFolder(){
+                var userDefinedFolder = localStorage.getItem("ao/desktop/backgroundoverwrite");
+                if (userDefinedFolder == null){
+                    $("#userSelectedFolderPath").text("Disabled");
+                }else{
+                    $("#userSelectedFolderPath").text(userDefinedFolder);
+                }
+            }
+            
+
+            function initDefaultBackgroundChangeValue(){
+                if (localStorage.getItem("ao/desktop/backgroundInterval") == null){
+                    //No background interval set.
+                    $("#changeInterval").dropdown("set selected", "30");
+                }else{
+                    //There is already a setting for background interval change. Use that instead
+                    var changeInterval = localStorage.getItem("ao/desktop/backgroundInterval");
+                    $("#changeInterval").dropdown("set selected", changeInterval);
+                }
+            }
+
+            //Change the interval to the given 
+            function handleIntervalChange(newInterval){
+                //Show change finsihed
+                if (isStartingUp){
+                    //Ignore startup change
+                    return;
+                }
+
+                //Save interval to localStorage
+                localStorage.setItem("ao/desktop/backgroundInterval", newInterval)
+                $("#interfaceChangeConfirm").slideDown('fast').delay(3000).slideUp('fast');
+
+                //Restart desktop background changer interval
+                if (ao_module_virtualDesktop){
+                    console.log("Restarting desktop background changer interval")
+                    parent.clearInterval(parent.backgroundIntervalCounter);
+                    parent.initBackgroundSwitchingAnimation();
+                }
+                
+            }
+
+            function selectUserFolder(){
+                ao_module_openFileSelector(folderSelected, undefined,"folder",false);
+            }
+
+            function folderSelected(filedata){
+                for (var i=0; i < filedata.length; i++){
+                    var filename = filedata[i].filename;
+                    var filepath = filedata[i].filepath;
+
+                    //Save the overwrite folder path
+                    localStorage.setItem("ao/desktop/backgroundoverwrite",filepath);
+                    $("#userSelectedFolderPath").text(filepath);
+                }
+            }
+
+            function clearUserSelectedFolder(){
+                //Clear user selected folder
+                localStorage.removeItem("ao/desktop/backgroundoverwrite");
+                initUserDefinedWallpaperFolder();
+            }
+
+            function initCurrentBackgroundPreview(){
+                //Get the list of theme in the system
+                $.get("../../system/desktop/theme", function(data) {
+                    desktopThemeList = data;
+                    //Generate the wallpaper list
+                    $("#wallpaperlist").html("");
+                    var deftaultData = "";
+                    desktopThemeList.forEach(themepack => {
+                        var encodedData = encodeURIComponent(JSON.stringify(themepack));
+                        var themeName = themepack.Theme.charAt(0).toUpperCase() + themepack.Theme.slice(1);
+                        $("#wallpaperlist").append(`<option value="${encodedData}">${themeName}</option>`);
+                        if (themepack.Theme == "default"){
+                            deftaultData = encodedData;
+                        }
+                    });
+
+                    //Get the one the user is currently using
+                    $.get("../../system/desktop/theme?get=true", function(data) {
+                        //Get the user theme settings
+                        $(".backgroundpreview").attr('src','../../img/desktop/bg/nobg.jpg');
+                        
+                        //Check if the theme exists
+                        var themeExists = false;
+                        var targetThemeObject;
+                        desktopThemeList.forEach(theme => {
+                            if (theme.Theme == data){
+                                //Theme exists
+                                themeExists = true;
+                                targetThemeObject = theme;
+                            }
+                        });
+
+                        if (themeExists == false){
+                            //This theme not exists. Do not load preview
+                            $("#wallpaperlist").dropdown("set selected","Default");
+                        }else{
+                            loadBackgroundPreview(targetThemeObject);
+                            var themeName = data.charAt(0).toUpperCase() + data.slice(1)
+                            $("#wallpaperlist").dropdown("set selected",themeName);
+                        }
+                        
+                        //End of startup process
+                        isStartingUp = false;
+                    });
+                });
+            }
+
+            function loadBackgroundPreview(targetThemeObject){
+                $("#backgroundPreviewList").html("");
+                var imageList = targetThemeObject.Bglist;
+                $("#mainBackground").attr("src","../../img/desktop/bg/" + targetThemeObject.Theme + "/" + imageList[0]);
+                
+                for (var i = 1; i < imageList.length; i++){
+                    $("#backgroundPreviewList").append(`<div class="four wide column">
+                        <img class="ui fluid image backgroundpreview" src="${"../../img/desktop/bg/" + targetThemeObject.Theme + "/" + imageList[i]}">
+                    </div>`);
+                }
+            }
+
+            function handleBackgroundSelectionChange(value){
+                var targetThemeObject = JSON.parse(decodeURIComponent(value));
+                loadBackgroundPreview(targetThemeObject);
+            }
+
+            function applyWallpaper(){
+                var targetWallpaper =JSON.parse(decodeURIComponent($("#wallpaperlist").val()));
+                $.get("../../system/desktop/theme?set=" + targetWallpaper.Theme, function(data) {
+                    if (ao_module_virtualDesktop == true){
+                        parent.changeDesktopTheme(targetWallpaper.Theme);
+                    }
+                    if (data.includes("Error")) {
+                        console.log(data);
+                        return;
+                    }
+
+                    //Reload the preview
+                    initCurrentBackgroundPreview();
+
+                    //Show change finsihed
+                    $("#wallpaperChangeConfirm").slideDown('fast').delay(3000).slideUp('fast');
+                });
+            }
+        </script>
+    </body>
+</html>

BIN
web/SystemAO/iot/hub/img/devices/XD28.png


BIN
web/SystemAO/iot/hub/img/devices/baseline_bug_report_white_48dp.png


BIN
web/SystemAO/iot/hub/img/devices/camera.png


BIN
web/SystemAO/iot/hub/img/devices/default.psd


BIN
web/SystemAO/iot/hub/img/devices/dht11.png


BIN
web/SystemAO/iot/hub/img/devices/esp32cam.png


BIN
web/SystemAO/iot/hub/img/devices/fridge.png


BIN
web/SystemAO/iot/hub/img/devices/gateway.png


BIN
web/SystemAO/iot/hub/img/devices/moisture.png


BIN
web/SystemAO/iot/hub/img/devices/panel.png


BIN
web/SystemAO/iot/hub/img/devices/peltier.png


BIN
web/SystemAO/iot/hub/img/devices/relay.png


BIN
web/SystemAO/iot/hub/img/devices/router.png


BIN
web/SystemAO/iot/hub/img/devices/sonoff_s26.png


BIN
web/SystemAO/iot/hub/img/devices/switch.png


BIN
web/SystemAO/iot/hub/img/devices/test.png


BIN
web/SystemAO/iot/hub/img/devices/test.psd


BIN
web/SystemAO/iot/hub/img/devices/unknown.png


BIN
web/SystemAO/iot/hub/img/devices/weather.png


+ 19 - 6
web/SystemAO/iot/hub/index.html

@@ -81,6 +81,19 @@
 		  <a class="right item" onClick="toggleSideMenu();"><i class="content icon"></i></a>
 	   </div>
 	   <div id="devList" class="ts container">
+			<div class="ts basic segment HDSDev">
+				<div class="ts grid">
+					<div class="four wide column"><img class="ts tiny devIcon image" src="img/system/loading.gif"></div>
+					<div class="twelve wide column">
+						<div class="ts container">
+							<div class="ts header">
+								<span class="devHeader">Scanning</span>
+								<div class="sub devProperty header">IoT Hub take a while to scan your network for the first startup.</div>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
 	   </div>
 	   <br><br><br>
 	</div>
@@ -202,16 +215,16 @@
 		}
 
 		function loadDevList(){
-			$("#devList").html("");
 			$.get("../../../system/iot/list", function(data){
+				$("#devList").html("");
 				if (data.error !== undefined){
 					alert(data.error);
 				}else{
 					data.forEach(device => {
-						deviceData = encodeURIComponent(JSON.stringify(device));
+						var deviceData = encodeURIComponent(JSON.stringify(device));
 						$("#devList").append(`<div class="ts segment HDSDev" devicedata="${deviceData}" uuid="${device.DeviceUUID}" devIp="${device.IPAddr}" port="${device.Port}" location="local">
 							<div class="ts grid">
-								<div class="four wide column"><img class="ts tiny devIcon image" src="img/system/loading.gif"></div>
+								<div class="four wide column"><img class="ts tiny devIcon image" src="../../../system/iot/icon?devid=${device.DeviceUUID}"></div>
 								<div class="twelve wide column">
 									<div class="ts container">
 										<div class="ts header">
@@ -297,9 +310,9 @@
 			deviceData = JSON.parse(decodeURIComponent(deviceData));
 
 			var epts = deviceData.ControlEndpoints;
-			if (epts.length == 0){
-				//This device has no control endpoints
-
+			if (epts == null || epts.length == 0){
+				//This device has no control endpoints. Show status info only.
+				updateStatus(deviceData.DeviceUUID);
 			}else{
 				epts.forEach(ept => {
 					//Check which type of ept is this. Accept {string, integer, float, bool, none}

+ 15 - 2
web/desktop.system

@@ -854,6 +854,7 @@
         var currentUserTheme = ""; //The theme for current user. 
         var backgroundCrossfadeInterval = 30000; //Time between the crossfade of each background image
         var nextSlideshowIndex = 0; //The next background image index to be shown
+        var backgroundIntervalCounter; //The interval object for background changer
 
         //Desktop icon realted
         var desktopIconSize = "medium"; //Size of desktop icons. Allow {small / medium / big}
@@ -2217,7 +2218,12 @@
 
         //Actiave background cross fade changing animation
         function initBackgroundSwitchingAnimation() {
-            setInterval(function() {
+            if (localStorage.getItem("ao/desktop/backgroundInterval") !== null && !isNaN(parseInt(localStorage.getItem("ao/desktop/backgroundInterval")))){
+                //Localstorage has interval setting that is not NaN
+                backgroundCrossfadeInterval = parseInt(localStorage.getItem("ao/desktop/backgroundInterval")) * 1000;
+            }
+
+            backgroundIntervalCounter = setInterval(function() {
                 $("body").css("background-image", "none").css({
                     'background-color': '#000000'
                 });
@@ -4075,7 +4081,14 @@
         }
 
         function personalization() {
-            alert("WIP");
+            //Show the personalization window
+            newFloatWindow({
+                url: "SystemAO/desktop/personalization.html",
+                appicon: "SystemAO/desktop/img/personalization.png",
+                width:640,
+                height:480,
+                title: "Personalization"
+            });
             hideAllContextMenus();
         }
 

BIN
web/img/desktop/bg/nobg.jpg


BIN
web/img/desktop/bg/nobg.psd


+ 4 - 9
web/mobile.system

@@ -691,15 +691,6 @@
                 openModule(moduleName);
             }
 
-            //Ask for confirmation before window close
-            function exitConfirm() {
-                if (!loggingOut){
-                    return 'Your data might not been saved. Are you sure you want to quit?';
-                }
-            }
-
-            window.onbeforeunload = exitConfirm;
-
             //In mobile interface, there will be some option ignored by default
             function newFloatWindow(options){
                 //Hide all other floatWindows
@@ -810,6 +801,10 @@
                 //Disabled in mobile mode
             }
 
+            function setFloatWindowSize(id, width, height){
+                //Disabled in mobile mode
+            }
+
             function closeFloatWindow(windowID){
                 //Get the content iframe with that windowID
                 var contentWindow = getFloatWindowByID(windowID).find("iframe")[0].contentWindow;