From 90cce2cbee999127646c4b73b0a69dcebfc9752d Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Mon, 20 Jan 2025 17:48:28 +0100
Subject: [PATCH 01/50]  fix

---
 config/config.go                              | 38 ++++++++++++
 config/config_test.go                         | 61 +++++++++++++++++++
 config/testdata/fromenv/config.ini            |  8 +++
 .../.config/ArduinoCreateAgent/config.ini     |  8 +++
 .../.config/ArduinoCreateAgent/config.ini     | 10 +++
 main.go                                       | 34 +----------
 6 files changed, 128 insertions(+), 31 deletions(-)
 create mode 100644 config/config_test.go
 create mode 100644 config/testdata/fromenv/config.ini
 create mode 100644 config/testdata/home/.config/ArduinoCreateAgent/config.ini
 create mode 100644 config/testdata/noconfig/.config/ArduinoCreateAgent/config.ini

diff --git a/config/config.go b/config/config.go
index 69d29eeee..50978eb82 100644
--- a/config/config.go
+++ b/config/config.go
@@ -142,3 +142,41 @@ func SetInstallCertsIni(filename string, value string) error {
 	}
 	return nil
 }
+
+func GetConfigPath() *paths.Path {
+	// Let's handle the config
+	configDir := GetDefaultConfigDir()
+	var configPath *paths.Path
+
+	// see if the env var is defined, if it is take the config from there, this will override the default path
+	if envConfig := os.Getenv("ARDUINO_CREATE_AGENT_CONFIG"); envConfig != "" {
+		configPath = paths.New(envConfig)
+		if configPath.NotExist() {
+			log.Panicf("config from env var %s does not exists", envConfig)
+		}
+		log.Infof("using config from env variable: %s", configPath)
+	} else if defaultConfigPath := configDir.Join("config.ini"); defaultConfigPath.Exist() {
+		// by default take the config from the ~/.arduino-create/config.ini file
+		configPath = defaultConfigPath
+		log.Infof("using config from default: %s", configPath)
+	} else {
+		// Fall back to the old config.ini location
+		src, _ := os.Executable()
+		oldConfigPath := paths.New(src).Parent().Join("config.ini")
+		if oldConfigPath.Exist() {
+			err := oldConfigPath.CopyTo(defaultConfigPath)
+			if err != nil {
+				log.Errorf("cannot copy old %s, to %s, generating new config", oldConfigPath, configPath)
+			} else {
+				configPath = defaultConfigPath
+				log.Infof("copied old %s, to %s", oldConfigPath, configPath)
+			}
+		}
+	}
+	if configPath == nil {
+		configPath = GenerateConfig(configDir)
+	}
+
+	return configPath
+
+}
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 000000000..76e6988c0
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,61 @@
+package config
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/arduino/go-paths-helper"
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetConfigPath(t *testing.T) {
+	t.Run("read config.ini from ARDUINO_CREATE_AGENT_CONFIG", func(t *testing.T) {
+		os.Setenv("ARDUINO_CREATE_AGENT_CONFIG", "./testdata/fromenv/config.ini")
+		defer os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
+		configPath := GetConfigPath()
+		assert.Equal(t, "./testdata/fromenv/config.ini", configPath.String())
+	})
+
+	t.Run("panic if config.ini does not exist", func(t *testing.T) {
+		os.Setenv("ARDUINO_CREATE_AGENT_CONFIG", "./testdata/nonexistent_config.ini")
+		defer os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
+
+		defer func() {
+			if r := recover(); r != nil {
+				entry, ok := r.(*logrus.Entry)
+				if !ok {
+					t.Errorf("Expected panic of type *logrus.Entry but got %T", r)
+				} else {
+					assert.Equal(t, "config from env var ./testdata/nonexistent_config.ini does not exists", entry.Message)
+				}
+			} else {
+				t.Errorf("Expected panic but did not get one")
+			}
+		}()
+
+		GetConfigPath()
+	})
+
+	t.Run("read config.ini from $HOME", func(t *testing.T) {
+		os.Setenv("HOME", "./testdata/home")
+		defer os.Unsetenv("HOME")
+		configPath := GetConfigPath()
+		assert.Equal(t, "testdata/home/.config/ArduinoCreateAgent/config.ini", configPath.String())
+	})
+
+	t.Run("fallback old : read config.ini where the binary is launched", func(t *testing.T) {
+		src, _ := os.Executable()
+		paths.New(src).Parent().Join("config.ini").Create() // create a config.ini in the same directory as the binary
+		// The fallback path is the directory where the binary is launched
+		fmt.Println(src)
+		os.Setenv("HOME", "./testdata/noconfig") // force to not have a config in the home directory
+		defer os.Unsetenv("HOME")
+
+		// expect it creates a config.ini in the same directory as the binary
+		configPath := GetConfigPath()
+		assert.Equal(t, "testdata/home/.config/ArduinoCreateAgent/config.ini", configPath.String())
+	})
+
+}
diff --git a/config/testdata/fromenv/config.ini b/config/testdata/fromenv/config.ini
new file mode 100644
index 000000000..5b31315b9
--- /dev/null
+++ b/config/testdata/fromenv/config.ini
@@ -0,0 +1,8 @@
+gc = std
+hostname = unknown-hostname
+regex = usb|acm|com
+v = true
+appName = CreateAgent/Stable
+updateUrl = https://downloads.arduino.cc/
+origins = https://local.arduino.cc:8000, https://local.arduino.cc:8001, https://create-dev.arduino.cc, https://*.sparklyunicorn.cc, https://*.iot-cloud-arduino-cc.pages.dev, https://cloud.oniudra.cc, https://app.oniudra.cc,https://*.iot-cloud-arduino-cc.pages.dev
+crashreport = false
diff --git a/config/testdata/home/.config/ArduinoCreateAgent/config.ini b/config/testdata/home/.config/ArduinoCreateAgent/config.ini
new file mode 100644
index 000000000..92f231faf
--- /dev/null
+++ b/config/testdata/home/.config/ArduinoCreateAgent/config.ini
@@ -0,0 +1,8 @@
+gc = std
+hostname = unknown-hostname
+regex = usb|acm|com
+v = true
+appName = config-from-home-dir
+updateUrl = https://downloads.arduino.cc/
+origins = https://local.arduino.cc:8000, https://local.arduino.cc:8001, https://*.iot-cloud-arduino-cc.pages.dev
+crashreport = false
diff --git a/config/testdata/noconfig/.config/ArduinoCreateAgent/config.ini b/config/testdata/noconfig/.config/ArduinoCreateAgent/config.ini
new file mode 100644
index 000000000..f63377db5
--- /dev/null
+++ b/config/testdata/noconfig/.config/ArduinoCreateAgent/config.ini
@@ -0,0 +1,10 @@
+gc = std  # Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)
+hostname = unknown-hostname  # Override the hostname we get from the OS
+regex = usb|acm|com  # Regular expression to filter serial port list
+v = true  # show debug logging
+appName = CreateAgent/Stable
+updateUrl = https://downloads.arduino.cc/
+origins = https://local.arduino.cc:8000
+#httpProxy = http://your.proxy:port # Proxy server for HTTP requests
+crashreport = false # enable crashreport logging
+autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
\ No newline at end of file
diff --git a/main.go b/main.go
index 1ca857b02..a83d7f4c5 100755
--- a/main.go
+++ b/main.go
@@ -22,6 +22,7 @@ import (
 	_ "embed"
 	"encoding/json"
 	"flag"
+	"fmt"
 	"html/template"
 	"io"
 	"os"
@@ -188,38 +189,9 @@ func loop() {
 		h.broadcastSys <- mapB
 	}
 
-	// Let's handle the config
-	configDir := config.GetDefaultConfigDir()
-	var configPath *paths.Path
+	configPath := config.GetConfigPath()
 
-	// see if the env var is defined, if it is take the config from there, this will override the default path
-	if envConfig := os.Getenv("ARDUINO_CREATE_AGENT_CONFIG"); envConfig != "" {
-		configPath = paths.New(envConfig)
-		if configPath.NotExist() {
-			log.Panicf("config from env var %s does not exists", envConfig)
-		}
-		log.Infof("using config from env variable: %s", configPath)
-	} else if defaultConfigPath := configDir.Join("config.ini"); defaultConfigPath.Exist() {
-		// by default take the config from the ~/.arduino-create/config.ini file
-		configPath = defaultConfigPath
-		log.Infof("using config from default: %s", configPath)
-	} else {
-		// Fall back to the old config.ini location
-		src, _ := os.Executable()
-		oldConfigPath := paths.New(src).Parent().Join("config.ini")
-		if oldConfigPath.Exist() {
-			err := oldConfigPath.CopyTo(defaultConfigPath)
-			if err != nil {
-				log.Errorf("cannot copy old %s, to %s, generating new config", oldConfigPath, configPath)
-			} else {
-				configPath = defaultConfigPath
-				log.Infof("copied old %s, to %s", oldConfigPath, configPath)
-			}
-		}
-	}
-	if configPath == nil {
-		configPath = config.GenerateConfig(configDir)
-	}
+	fmt.Println("configPath: ", configPath)
 
 	// if the default browser is Safari, prompt the user to install HTTPS certificates
 	// and eventually install them

From c96490b00cc260f6ee243d679d079a44864d8ad5 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 21 Jan 2025 11:33:56 +0100
Subject: [PATCH 02/50] feat(config): add default configuration file and update
 config handling

---
 config/config-default.ini             | 10 ++++++
 config/config.go                      |  2 +-
 config/config_test.go                 | 48 +++++++++++++++++++++------
 config/testdata/fromlegacy/.gitignore |  1 +
 4 files changed, 50 insertions(+), 11 deletions(-)
 create mode 100644 config/config-default.ini
 create mode 100644 config/testdata/fromlegacy/.gitignore

diff --git a/config/config-default.ini b/config/config-default.ini
new file mode 100644
index 000000000..f63377db5
--- /dev/null
+++ b/config/config-default.ini
@@ -0,0 +1,10 @@
+gc = std  # Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)
+hostname = unknown-hostname  # Override the hostname we get from the OS
+regex = usb|acm|com  # Regular expression to filter serial port list
+v = true  # show debug logging
+appName = CreateAgent/Stable
+updateUrl = https://downloads.arduino.cc/
+origins = https://local.arduino.cc:8000
+#httpProxy = http://your.proxy:port # Proxy server for HTTP requests
+crashreport = false # enable crashreport logging
+autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
\ No newline at end of file
diff --git a/config/config.go b/config/config.go
index 50978eb82..9919bfbbc 100644
--- a/config/config.go
+++ b/config/config.go
@@ -108,7 +108,7 @@ func GetDefaultHomeDir() *paths.Path {
 	return paths.New(homeDir)
 }
 
-//go:embed config.ini
+//go:embed config-default.ini
 var configContent []byte
 
 // GenerateConfig function will take a directory path as an input
diff --git a/config/config_test.go b/config/config_test.go
index 76e6988c0..4d1c64ec8 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1,9 +1,9 @@
 package config
 
 import (
-	"fmt"
 	"os"
 	"testing"
+	"time"
 
 	"github.com/arduino/go-paths-helper"
 	"github.com/sirupsen/logrus"
@@ -38,24 +38,52 @@ func TestGetConfigPath(t *testing.T) {
 		GetConfigPath()
 	})
 
-	t.Run("read config.ini from $HOME", func(t *testing.T) {
+	t.Run("read config.ini from $HOME/.config/ArduinoCreateAgent folder", func(t *testing.T) {
 		os.Setenv("HOME", "./testdata/home")
 		defer os.Unsetenv("HOME")
 		configPath := GetConfigPath()
 		assert.Equal(t, "testdata/home/.config/ArduinoCreateAgent/config.ini", configPath.String())
 	})
 
-	t.Run("fallback old : read config.ini where the binary is launched", func(t *testing.T) {
-		src, _ := os.Executable()
-		paths.New(src).Parent().Join("config.ini").Create() // create a config.ini in the same directory as the binary
-		// The fallback path is the directory where the binary is launched
-		fmt.Println(src)
-		os.Setenv("HOME", "./testdata/noconfig") // force to not have a config in the home directory
+	t.Run("legacy config are copied to new location", func(t *testing.T) {
+
+		createLegacyConfig := func() string {
+			// Create a "legacy" config.ini in the same directory as the binary executable
+			src, err := os.Executable()
+			if err != nil {
+				t.Fatal(err)
+			}
+			legacyConfigPath, err := paths.New(src).Parent().Join("config.ini").Create()
+			if err != nil {
+				t.Fatal(err)
+			}
+			// adding a timestamp to the content to make it unique
+			c := "hostname = legacy-config-file-" + time.Now().String()
+			n, err := legacyConfigPath.WriteString(c)
+			if err != nil || n <= 0 {
+				t.Fatalf("Failed to write legacy config file: %v", err)
+			}
+			return c
+		}
+
+		wantContent := createLegacyConfig()
+
+		// Expectation: it copies the "legacy" config.ini into the location pointed by $HOME
+		os.Setenv("HOME", "./testdata/fromlegacy")
 		defer os.Unsetenv("HOME")
 
-		// expect it creates a config.ini in the same directory as the binary
+		// remove any existing config.ini in the into the location pointed by $HOME
+		err := os.Remove("./testdata/fromlegacy/.config/ArduinoCreateAgent/config.ini")
+		if err != nil && !os.IsNotExist(err) {
+			t.Fatal(err)
+		}
+
 		configPath := GetConfigPath()
-		assert.Equal(t, "testdata/home/.config/ArduinoCreateAgent/config.ini", configPath.String())
+		assert.Equal(t, "testdata/fromlegacy/.config/ArduinoCreateAgent/config.ini", configPath.String())
+
+		given, err := paths.New(configPath.String()).ReadFile()
+		assert.Nil(t, err)
+		assert.Equal(t, wantContent, string(given))
 	})
 
 }
diff --git a/config/testdata/fromlegacy/.gitignore b/config/testdata/fromlegacy/.gitignore
new file mode 100644
index 000000000..2fa7ce7c4
--- /dev/null
+++ b/config/testdata/fromlegacy/.gitignore
@@ -0,0 +1 @@
+config.ini

From 7b4859a7b35003f5323bc70a45eea87ed38a292c Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 21 Jan 2025 11:34:07 +0100
Subject: [PATCH 03/50] fix(.gitignore): add entry to ignore config.ini file

---
 config/testdata/fromlegacy/.gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/config/testdata/fromlegacy/.gitignore b/config/testdata/fromlegacy/.gitignore
index 2fa7ce7c4..f67b7f089 100644
--- a/config/testdata/fromlegacy/.gitignore
+++ b/config/testdata/fromlegacy/.gitignore
@@ -1 +1,2 @@
+# ingore because this config file
 config.ini

From 279de2ac65f7bfe6ad8597fcaaf00f6fc2df90e0 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 21 Jan 2025 12:48:37 +0100
Subject: [PATCH 04/50] test(config): add test for writing default config.ini
 file and update test data

---
 config/config_test.go                         | 19 +++++++++++++++++++
 config/testdata/fromdefault/.gitignore        |  1 +
 config/testdata/fromenv/config.ini            |  2 +-
 .../.config/ArduinoCreateAgent/config.ini     |  2 +-
 4 files changed, 22 insertions(+), 2 deletions(-)
 create mode 100644 config/testdata/fromdefault/.gitignore

diff --git a/config/config_test.go b/config/config_test.go
index 4d1c64ec8..666d85fbc 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -86,4 +86,23 @@ func TestGetConfigPath(t *testing.T) {
 		assert.Equal(t, wantContent, string(given))
 	})
 
+	t.Run("write the default config.ini file", func(t *testing.T) {
+		os.Setenv("HOME", "./testdata/fromdefault")
+		os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
+
+		// ensure the config.ini does not exist in the target directory
+		os.Remove("./testdata/fromdefault/.config/ArduinoCreateAgent/config.ini")
+
+		configPath := GetConfigPath()
+
+		assert.Equal(t, "testdata/fromdefault/.config/ArduinoCreateAgent/config.ini", configPath.String())
+
+		givenContent, err := paths.New(configPath.String()).ReadFile()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, configContent, givenContent)
+	})
+
 }
diff --git a/config/testdata/fromdefault/.gitignore b/config/testdata/fromdefault/.gitignore
new file mode 100644
index 000000000..2fa7ce7c4
--- /dev/null
+++ b/config/testdata/fromdefault/.gitignore
@@ -0,0 +1 @@
+config.ini
diff --git a/config/testdata/fromenv/config.ini b/config/testdata/fromenv/config.ini
index 5b31315b9..cfd58d812 100644
--- a/config/testdata/fromenv/config.ini
+++ b/config/testdata/fromenv/config.ini
@@ -1,5 +1,5 @@
 gc = std
-hostname = unknown-hostname
+hostname = this-is-a-config-file-from-home-dir-from-ARDUINO_CREATE_AGENT_CONFIG-env
 regex = usb|acm|com
 v = true
 appName = CreateAgent/Stable
diff --git a/config/testdata/home/.config/ArduinoCreateAgent/config.ini b/config/testdata/home/.config/ArduinoCreateAgent/config.ini
index 92f231faf..a023d62d1 100644
--- a/config/testdata/home/.config/ArduinoCreateAgent/config.ini
+++ b/config/testdata/home/.config/ArduinoCreateAgent/config.ini
@@ -1,5 +1,5 @@
 gc = std
-hostname = unknown-hostname
+hostname = this-is-a-config-file-from-home-dir
 regex = usb|acm|com
 v = true
 appName = config-from-home-dir

From 4f3ac5a2ce1680ec895db5039357f0f7dddf30c8 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 21 Jan 2025 17:27:03 +0100
Subject: [PATCH 05/50] test(config): add tests for retrieving config paths
 from XDG_CONFIG_HOME and HOME directories

---
 config/config_test.go                         | 176 ++++++++----------
 .../.config/ArduinoCreateAgent/config.ini     |   8 +
 .../fromxdghome/ArduinoCreateAgent/config.ini |  10 +
 3 files changed, 99 insertions(+), 95 deletions(-)
 create mode 100644 config/testdata/fromhome/.config/ArduinoCreateAgent/config.ini
 create mode 100644 config/testdata/fromxdghome/ArduinoCreateAgent/config.ini

diff --git a/config/config_test.go b/config/config_test.go
index 666d85fbc..447f3793e 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -6,103 +6,89 @@ import (
 	"time"
 
 	"github.com/arduino/go-paths-helper"
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestGetConfigPath(t *testing.T) {
-	t.Run("read config.ini from ARDUINO_CREATE_AGENT_CONFIG", func(t *testing.T) {
-		os.Setenv("ARDUINO_CREATE_AGENT_CONFIG", "./testdata/fromenv/config.ini")
-		defer os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
-		configPath := GetConfigPath()
-		assert.Equal(t, "./testdata/fromenv/config.ini", configPath.String())
-	})
-
-	t.Run("panic if config.ini does not exist", func(t *testing.T) {
-		os.Setenv("ARDUINO_CREATE_AGENT_CONFIG", "./testdata/nonexistent_config.ini")
-		defer os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
-
-		defer func() {
-			if r := recover(); r != nil {
-				entry, ok := r.(*logrus.Entry)
-				if !ok {
-					t.Errorf("Expected panic of type *logrus.Entry but got %T", r)
-				} else {
-					assert.Equal(t, "config from env var ./testdata/nonexistent_config.ini does not exists", entry.Message)
-				}
-			} else {
-				t.Errorf("Expected panic but did not get one")
-			}
-		}()
-
-		GetConfigPath()
-	})
-
-	t.Run("read config.ini from $HOME/.config/ArduinoCreateAgent folder", func(t *testing.T) {
-		os.Setenv("HOME", "./testdata/home")
-		defer os.Unsetenv("HOME")
-		configPath := GetConfigPath()
-		assert.Equal(t, "testdata/home/.config/ArduinoCreateAgent/config.ini", configPath.String())
-	})
-
-	t.Run("legacy config are copied to new location", func(t *testing.T) {
-
-		createLegacyConfig := func() string {
-			// Create a "legacy" config.ini in the same directory as the binary executable
-			src, err := os.Executable()
-			if err != nil {
-				t.Fatal(err)
-			}
-			legacyConfigPath, err := paths.New(src).Parent().Join("config.ini").Create()
-			if err != nil {
-				t.Fatal(err)
-			}
-			// adding a timestamp to the content to make it unique
-			c := "hostname = legacy-config-file-" + time.Now().String()
-			n, err := legacyConfigPath.WriteString(c)
-			if err != nil || n <= 0 {
-				t.Fatalf("Failed to write legacy config file: %v", err)
-			}
-			return c
-		}
-
-		wantContent := createLegacyConfig()
-
-		// Expectation: it copies the "legacy" config.ini into the location pointed by $HOME
-		os.Setenv("HOME", "./testdata/fromlegacy")
-		defer os.Unsetenv("HOME")
-
-		// remove any existing config.ini in the into the location pointed by $HOME
-		err := os.Remove("./testdata/fromlegacy/.config/ArduinoCreateAgent/config.ini")
-		if err != nil && !os.IsNotExist(err) {
-			t.Fatal(err)
-		}
-
-		configPath := GetConfigPath()
-		assert.Equal(t, "testdata/fromlegacy/.config/ArduinoCreateAgent/config.ini", configPath.String())
-
-		given, err := paths.New(configPath.String()).ReadFile()
-		assert.Nil(t, err)
-		assert.Equal(t, wantContent, string(given))
-	})
-
-	t.Run("write the default config.ini file", func(t *testing.T) {
-		os.Setenv("HOME", "./testdata/fromdefault")
-		os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
-
-		// ensure the config.ini does not exist in the target directory
-		os.Remove("./testdata/fromdefault/.config/ArduinoCreateAgent/config.ini")
-
-		configPath := GetConfigPath()
-
-		assert.Equal(t, "testdata/fromdefault/.config/ArduinoCreateAgent/config.ini", configPath.String())
-
-		givenContent, err := paths.New(configPath.String()).ReadFile()
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		assert.Equal(t, configContent, givenContent)
-	})
+func TestGetConfigPathFromXDG_CONFIG_HOME(t *testing.T) {
+	// read config from $XDG_CONFIG_HOME/ArduinoCreateAgent/config.ini
+	os.Setenv("XDG_CONFIG_HOME", "./testdata/fromxdghome")
+	defer os.Unsetenv("XDG_CONFIG_HOME")
+	configPath := GetConfigPath()
+	assert.Equal(t, "testdata/fromxdghome/ArduinoCreateAgent/config.ini", configPath.String())
+}
+
+func TestGetConfigPathFromHOME(t *testing.T) {
+	// Test case 2: read config from $HOME/.config/ArduinoCreateAgent/config.ini "
+	os.Setenv("HOME", "./testdata/fromhome")
+	defer os.Unsetenv("HOME")
+	configPath := GetConfigPath()
+	assert.Equal(t, "testdata/fromhome/.config/ArduinoCreateAgent/config.ini", configPath.String())
+
+}
+
+func TestGetConfigPathFromARDUINO_CREATE_AGENT_CONFIG(t *testing.T) {
+	// read config from ARDUINO_CREATE_AGENT_CONFIG/config.ini"
+	os.Setenv("HOME", "./fromhome")
+	os.Setenv("ARDUINO_CREATE_AGENT_CONFIG", "./testdata/fromenv/config.ini")
+	defer os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
+
+	configPath := GetConfigPath()
+	assert.Equal(t, "./testdata/fromenv/config.ini", configPath.String())
+}
 
+func TestGetConfigPathFromLegacyConfig(t *testing.T) {
+	// If no config is found, copy the legacy config to the new location
+	src, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+	}
+	legacyConfigPath, err := paths.New(src).Parent().Join("config.ini").Create()
+	if err != nil {
+		t.Fatal(err)
+	}
+	// adding a timestamp to the content to make it unique
+	legacyContent := "hostname = legacy-config-file-" + time.Now().String()
+	n, err := legacyConfigPath.WriteString(legacyContent)
+	if err != nil || n <= 0 {
+		t.Fatalf("Failed to write legacy config file: %v", err)
+	}
+
+	// remove any existing config.ini in the into the location pointed by $HOME
+	err = os.Remove("./testdata/fromlegacy/.config/ArduinoCreateAgent/config.ini")
+	if err != nil && !os.IsNotExist(err) {
+		t.Fatal(err)
+	}
+
+	// Expectation: it copies the "legacy" config.ini into the location pointed by $HOME
+	os.Setenv("HOME", "./testdata/fromlegacy")
+	defer os.Unsetenv("HOME")
+
+	configPath := GetConfigPath()
+	assert.Equal(t, "testdata/fromlegacy/.config/ArduinoCreateAgent/config.ini", configPath.String())
+
+	given, err := paths.New(configPath.String()).ReadFile()
+	assert.Nil(t, err)
+	assert.Equal(t, legacyContent, string(given))
 }
+
+// func TestGetConfigPathCreateDefaultConfig(t *testing.T) {
+// 	os.Setenv("HOME", "./testdata/noconfig")
+// 	os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
+
+// 	// ensure the config.ini does not exist in HOME directory
+// 	os.Remove("./testdata/noconfig/.config/ArduinoCreateAgent/config.ini")
+// 	// ensure the config.ini does not exist in target directory
+// 	os.Remove("./testdata/fromdefault/.config/ArduinoCreateAgent/config.ini")
+
+// 	configPath := GetConfigPath()
+
+// 	assert.Equal(t, "testdata/fromdefault/.config/ArduinoCreateAgent/config.ini", configPath.String())
+
+// 	givenContent, err := paths.New(configPath.String()).ReadFile()
+// 	if err != nil {
+// 		t.Fatal(err)
+// 	}
+
+// 	assert.Equal(t, string(configContent), string(givenContent))
+
+// }
diff --git a/config/testdata/fromhome/.config/ArduinoCreateAgent/config.ini b/config/testdata/fromhome/.config/ArduinoCreateAgent/config.ini
new file mode 100644
index 000000000..a023d62d1
--- /dev/null
+++ b/config/testdata/fromhome/.config/ArduinoCreateAgent/config.ini
@@ -0,0 +1,8 @@
+gc = std
+hostname = this-is-a-config-file-from-home-dir
+regex = usb|acm|com
+v = true
+appName = config-from-home-dir
+updateUrl = https://downloads.arduino.cc/
+origins = https://local.arduino.cc:8000, https://local.arduino.cc:8001, https://*.iot-cloud-arduino-cc.pages.dev
+crashreport = false
diff --git a/config/testdata/fromxdghome/ArduinoCreateAgent/config.ini b/config/testdata/fromxdghome/ArduinoCreateAgent/config.ini
new file mode 100644
index 000000000..f63377db5
--- /dev/null
+++ b/config/testdata/fromxdghome/ArduinoCreateAgent/config.ini
@@ -0,0 +1,10 @@
+gc = std  # Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)
+hostname = unknown-hostname  # Override the hostname we get from the OS
+regex = usb|acm|com  # Regular expression to filter serial port list
+v = true  # show debug logging
+appName = CreateAgent/Stable
+updateUrl = https://downloads.arduino.cc/
+origins = https://local.arduino.cc:8000
+#httpProxy = http://your.proxy:port # Proxy server for HTTP requests
+crashreport = false # enable crashreport logging
+autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
\ No newline at end of file

From c41b815787b77a60660461f9d9896666e27bd412 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 22 Jan 2025 17:14:05 +0100
Subject: [PATCH 06/50] feat(config): pass config path to main loop and update
 handling

---
 main.go | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/main.go b/main.go
index a83d7f4c5..4bda82c0d 100755
--- a/main.go
+++ b/main.go
@@ -142,8 +142,11 @@ func main() {
 	// Check if certificates made with Agent <=1.2.7 needs to be moved over the new location
 	cert.MigrateCertificatesGeneratedWithOldAgentVersions(config.GetCertificatesDir())
 
+	configPath := config.GetConfigPath()
+	fmt.Println("configPath: ", configPath)
+
 	// Launch main loop in a goroutine
-	go loop()
+	go loop(configPath)
 
 	// SetupSystray is the main thread
 	configDir := config.GetDefaultConfigDir()
@@ -156,6 +159,7 @@ func main() {
 		AdditionalConfig: *additionalConfig,
 		ConfigDir:        configDir,
 	}
+	Systray.SetCurrentConfigFile(configPath)
 
 	if src, err := os.Executable(); err != nil {
 		panic(err)
@@ -166,11 +170,15 @@ func main() {
 	}
 }
 
-func loop() {
+func loop(configPath *paths.Path) {
 	if *hibernate {
 		return
 	}
 
+	if configPath == nil {
+		log.Panic("configPath is nil")
+	}
+
 	log.SetLevel(log.InfoLevel)
 	log.SetOutput(os.Stdout)
 
@@ -189,10 +197,6 @@ func loop() {
 		h.broadcastSys <- mapB
 	}
 
-	configPath := config.GetConfigPath()
-
-	fmt.Println("configPath: ", configPath)
-
 	// if the default browser is Safari, prompt the user to install HTTPS certificates
 	// and eventually install them
 	if runtime.GOOS == "darwin" {
@@ -230,7 +234,6 @@ func loop() {
 	if err != nil {
 		log.Panicf("cannot parse arguments: %s", err)
 	}
-	Systray.SetCurrentConfigFile(configPath)
 
 	// Parse additional ini config if defined
 	if len(*additionalConfig) > 0 {

From 55ca9e5f67f0bde13288412840c4f449bd6cd14a Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 22 Jan 2025 17:34:56 +0100
Subject: [PATCH 07/50] remove global systray

---
 hub.go    |  5 +++--
 info.go   | 23 +++++++++++++----------
 main.go   | 26 +++++++++++++-------------
 update.go | 25 ++++++++++++++-----------
 4 files changed, 43 insertions(+), 36 deletions(-)

diff --git a/hub.go b/hub.go
index a162dd01a..7ad58f686 100755
--- a/hub.go
+++ b/hub.go
@@ -223,9 +223,10 @@ func checkCmd(m []byte) {
 		go logAction(sl)
 	} else if strings.HasPrefix(sl, "restart") {
 		log.Println("Received restart from the daemon. Why? Boh")
-		Systray.Restart()
+		// TODO enable them
+		// Systray.Restart()
 	} else if strings.HasPrefix(sl, "exit") {
-		Systray.Quit()
+		// Systray.Quit()
 	} else if strings.HasPrefix(sl, "memstats") {
 		memoryStats()
 	} else if strings.HasPrefix(sl, "gc") {
diff --git a/info.go b/info.go
index 88145c02b..7da0f6986 100644
--- a/info.go
+++ b/info.go
@@ -19,6 +19,7 @@ import (
 	"runtime"
 	"strings"
 
+	"github.com/arduino/arduino-create-agent/systray"
 	"github.com/gin-gonic/gin"
 	"go.bug.st/serial"
 )
@@ -40,14 +41,16 @@ func infoHandler(c *gin.Context) {
 	})
 }
 
-func pauseHandler(c *gin.Context) {
-	go func() {
-		ports, _ := serial.GetPortsList()
-		for _, element := range ports {
-			spClose(element)
-		}
-		*hibernate = true
-		Systray.Pause()
-	}()
-	c.JSON(200, nil)
+func PauseHandler(s *systray.Systray) func(c *gin.Context) {
+	return func(c *gin.Context) {
+		go func() {
+			ports, _ := serial.GetPortsList()
+			for _, element := range ports {
+				spClose(element)
+			}
+			*hibernate = true
+			s.Pause()
+		}()
+		c.JSON(200, nil)
+	}
 }
diff --git a/main.go b/main.go
index 4bda82c0d..bb63cb211 100755
--- a/main.go
+++ b/main.go
@@ -103,9 +103,8 @@ var homeTemplateHTML string
 
 // global clients
 var (
-	Tools   *tools.Tools
-	Systray systray.Systray
-	Index   *index.Resource
+	Tools *tools.Tools
+	Index *index.Resource
 )
 
 type logWriter struct{}
@@ -145,12 +144,10 @@ func main() {
 	configPath := config.GetConfigPath()
 	fmt.Println("configPath: ", configPath)
 
-	// Launch main loop in a goroutine
-	go loop(configPath)
-
 	// SetupSystray is the main thread
 	configDir := config.GetDefaultConfigDir()
-	Systray = systray.Systray{
+
+	stray := &systray.Systray{
 		Hibernate: *hibernate,
 		Version:   version + "-" + commit,
 		DebugURL: func() string {
@@ -159,18 +156,21 @@ func main() {
 		AdditionalConfig: *additionalConfig,
 		ConfigDir:        configDir,
 	}
-	Systray.SetCurrentConfigFile(configPath)
+	stray.SetCurrentConfigFile(configPath)
+
+	// Launch main loop in a goroutine
+	go loop(stray, configPath)
 
 	if src, err := os.Executable(); err != nil {
 		panic(err)
 	} else if restartPath := updater.Start(src); restartPath != "" {
-		Systray.RestartWith(restartPath)
+		stray.RestartWith(restartPath)
 	} else {
-		Systray.Start()
+		stray.Start()
 	}
 }
 
-func loop(configPath *paths.Path) {
+func loop(stray *systray.Systray, configPath *paths.Path) {
 	if *hibernate {
 		return
 	}
@@ -435,8 +435,8 @@ func loop(configPath *paths.Path) {
 	r.Handle("WS", "/socket.io/", socketHandler)
 	r.Handle("WSS", "/socket.io/", socketHandler)
 	r.GET("/info", infoHandler)
-	r.POST("/pause", pauseHandler)
-	r.POST("/update", updateHandler)
+	r.POST("/pause", PauseHandler(stray))
+	r.POST("/update", UpdateHandler(stray))
 
 	// Mount goa handlers
 	goa := v2.Server(config.GetDataDir().String(), Index)
diff --git a/update.go b/update.go
index 33c028bce..0065a09b5 100644
--- a/update.go
+++ b/update.go
@@ -30,20 +30,23 @@
 package main
 
 import (
+	"github.com/arduino/arduino-create-agent/systray"
 	"github.com/arduino/arduino-create-agent/updater"
 	"github.com/gin-gonic/gin"
 )
 
-func updateHandler(c *gin.Context) {
-	restartPath, err := updater.CheckForUpdates(version, *updateURL, *appName)
-	if err != nil {
-		c.JSON(500, gin.H{"error": err.Error()})
-		return
-	}
-	c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"})
-	if restartPath == "quit" {
-		Systray.Quit()
-	} else {
-		Systray.RestartWith(restartPath)
+func UpdateHandler(s *systray.Systray) func(c *gin.Context) {
+	return func(c *gin.Context) {
+		restartPath, err := updater.CheckForUpdates(version, *updateURL, *appName)
+		if err != nil {
+			c.JSON(500, gin.H{"error": err.Error()})
+			return
+		}
+		c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"})
+		if restartPath == "quit" {
+			s.Quit()
+		} else {
+			s.RestartWith(restartPath)
+		}
 	}
 }

From f07103e566c6df074d2c8da3cf1b5e5d34a0ee2d Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 22 Jan 2025 18:02:08 +0100
Subject: [PATCH 08/50] WIP: remove hub and sh globals

---
 conn.go       | 163 +++++++++++++++++++++++++-------------------------
 hub.go        |  44 ++++++++------
 main.go       |  19 +++---
 serial.go     |  25 ++++----
 serialport.go |   6 +-
 5 files changed, 135 insertions(+), 122 deletions(-)

diff --git a/conn.go b/conn.go
index b6e03268c..e09cd7fcd 100644
--- a/conn.go
+++ b/conn.go
@@ -79,116 +79,119 @@ type Upload struct {
 
 var uploadStatusStr = "ProgrammerStatus"
 
-func uploadHandler(c *gin.Context) {
-	data := new(Upload)
-	if err := c.BindJSON(data); err != nil {
-		c.String(http.StatusBadRequest, fmt.Sprintf("err with the payload. %v", err.Error()))
-		return
-	}
-
-	log.Printf("%+v %+v %+v %+v %+v %+v", data.Port, data.Board, data.Rewrite, data.Commandline, data.Extra, data.Filename)
-
-	if data.Port == "" {
-		c.String(http.StatusBadRequest, "port is required")
-		return
-	}
-
-	if data.Board == "" {
-		c.String(http.StatusBadRequest, "board is required")
-		log.Error("board is required")
-		return
-	}
-
-	if !data.Extra.Network {
-		if data.Signature == "" {
-			c.String(http.StatusBadRequest, "signature is required")
+func UploadHandler(h *hub) func(c *gin.Context) {
+	return func(c *gin.Context) {
+		data := new(Upload)
+		if err := c.BindJSON(data); err != nil {
+			c.String(http.StatusBadRequest, fmt.Sprintf("err with the payload. %v", err.Error()))
 			return
 		}
 
-		if data.Commandline == "" {
-			c.String(http.StatusBadRequest, "commandline is required for local board")
+		log.Printf("%+v %+v %+v %+v %+v %+v", data.Port, data.Board, data.Rewrite, data.Commandline, data.Extra, data.Filename)
+
+		if data.Port == "" {
+			c.String(http.StatusBadRequest, "port is required")
 			return
 		}
 
-		err := utilities.VerifyInput(data.Commandline, data.Signature)
-
-		if err != nil {
-			c.String(http.StatusBadRequest, "signature is invalid")
+		if data.Board == "" {
+			c.String(http.StatusBadRequest, "board is required")
+			log.Error("board is required")
 			return
 		}
-	}
 
-	buffer := bytes.NewBuffer(data.Hex)
+		if !data.Extra.Network {
+			if data.Signature == "" {
+				c.String(http.StatusBadRequest, "signature is required")
+				return
+			}
 
-	filePath, err := utilities.SaveFileonTempDir(data.Filename, buffer)
-	if err != nil {
-		c.String(http.StatusBadRequest, err.Error())
-		return
-	}
+			if data.Commandline == "" {
+				c.String(http.StatusBadRequest, "commandline is required for local board")
+				return
+			}
 
-	tmpdir, err := os.MkdirTemp("", "extrafiles")
-	if err != nil {
-		c.String(http.StatusBadRequest, err.Error())
-		return
-	}
+			err := utilities.VerifyInput(data.Commandline, data.Signature)
 
-	for _, extraFile := range data.ExtraFiles {
-		path, err := utilities.SafeJoin(tmpdir, extraFile.Filename)
-		if err != nil {
-			c.String(http.StatusBadRequest, err.Error())
-			return
+			if err != nil {
+				c.String(http.StatusBadRequest, "signature is invalid")
+				return
+			}
 		}
-		log.Printf("Saving %s on %s", extraFile.Filename, path)
 
-		err = os.MkdirAll(filepath.Dir(path), 0744)
-		if err != nil {
-			c.String(http.StatusBadRequest, err.Error())
-			return
-		}
+		buffer := bytes.NewBuffer(data.Hex)
 
-		err = os.WriteFile(path, extraFile.Hex, 0644)
+		filePath, err := utilities.SaveFileonTempDir(data.Filename, buffer)
 		if err != nil {
 			c.String(http.StatusBadRequest, err.Error())
 			return
 		}
-	}
 
-	if data.Rewrite != "" {
-		data.Board = data.Rewrite
-	}
-
-	go func() {
-		// Resolve commandline
-		commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, Tools)
+		tmpdir, err := os.MkdirTemp("", "extrafiles")
 		if err != nil {
-			send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
+			c.String(http.StatusBadRequest, err.Error())
 			return
 		}
 
-		l := PLogger{Verbose: true}
-
-		// Upload
-		if data.Extra.Network {
-			err = errors.New("network upload is not supported anymore, pease use OTA instead")
-		} else {
-			send(map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"})
-			err = upload.Serial(data.Port, commandline, data.Extra, l)
+		for _, extraFile := range data.ExtraFiles {
+			path, err := utilities.SafeJoin(tmpdir, extraFile.Filename)
+			if err != nil {
+				c.String(http.StatusBadRequest, err.Error())
+				return
+			}
+			log.Printf("Saving %s on %s", extraFile.Filename, path)
+
+			err = os.MkdirAll(filepath.Dir(path), 0744)
+			if err != nil {
+				c.String(http.StatusBadRequest, err.Error())
+				return
+			}
+
+			err = os.WriteFile(path, extraFile.Hex, 0644)
+			if err != nil {
+				c.String(http.StatusBadRequest, err.Error())
+				return
+			}
 		}
 
-		// Handle result
-		if err != nil {
-			send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
-			return
+		if data.Rewrite != "" {
+			data.Board = data.Rewrite
 		}
-		send(map[string]string{uploadStatusStr: "Done", "Flash": "Ok"})
-	}()
 
-	c.String(http.StatusAccepted, "")
+		go func() {
+			// Resolve commandline
+			commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, Tools)
+			if err != nil {
+				send(h, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
+				return
+			}
+
+			l := PLogger{Verbose: true}
+
+			// Upload
+			if data.Extra.Network {
+				err = errors.New("network upload is not supported anymore, pease use OTA instead")
+			} else {
+				send(h, map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"})
+				err = upload.Serial(data.Port, commandline, data.Extra, l)
+			}
+
+			// Handle result
+			if err != nil {
+				send(h, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
+				return
+			}
+			send(h, map[string]string{uploadStatusStr: "Done", "Flash": "Ok"})
+		}()
+
+		c.String(http.StatusAccepted, "")
+	}
 }
 
 // PLogger sends the info from the upload to the websocket
 type PLogger struct {
 	Verbose bool
+	h       *hub
 }
 
 // Debug only sends messages if verbose is true (always true for now)
@@ -202,15 +205,15 @@ func (l PLogger) Debug(args ...interface{}) {
 func (l PLogger) Info(args ...interface{}) {
 	output := fmt.Sprint(args...)
 	log.Println(output)
-	send(map[string]string{uploadStatusStr: "Busy", "Msg": output})
+	send(l.h, map[string]string{uploadStatusStr: "Busy", "Msg": output})
 }
 
-func send(args map[string]string) {
+func send(h *hub, args map[string]string) {
 	mapB, _ := json.Marshal(args)
 	h.broadcastSys <- mapB
 }
 
-func wsHandler() *WsServer {
+func wsHandler(h *hub) *WsServer {
 	server, err := socketio.NewServer(nil)
 	if err != nil {
 		log.Fatal(err)
diff --git a/hub.go b/hub.go
index 7ad58f686..5c4605b73 100755
--- a/hub.go
+++ b/hub.go
@@ -45,14 +45,20 @@ type hub struct {
 
 	// Unregister requests from connections.
 	unregister chan *connection
+
+	// Serial hub to communicate with serial ports
+	serialHub *serialhub
 }
 
-var h = hub{
-	broadcast:    make(chan []byte, 1000),
-	broadcastSys: make(chan []byte, 1000),
-	register:     make(chan *connection),
-	unregister:   make(chan *connection),
-	connections:  make(map[*connection]bool),
+func NewHub() *hub {
+	return &hub{
+		broadcast:    make(chan []byte, 1000),
+		broadcastSys: make(chan []byte, 1000),
+		register:     make(chan *connection),
+		unregister:   make(chan *connection),
+		connections:  make(map[*connection]bool),
+		serialHub:    NewSerialHub(),
+	}
 }
 
 const commands = `{
@@ -108,7 +114,7 @@ func (h *hub) run() {
 			h.unregisterConnection(c)
 		case m := <-h.broadcast:
 			if len(m) > 0 {
-				checkCmd(m)
+				h.checkCmd(m)
 				h.sendToRegisteredConnections(m)
 			}
 		case m := <-h.broadcastSys:
@@ -117,7 +123,7 @@ func (h *hub) run() {
 	}
 }
 
-func checkCmd(m []byte) {
+func (h *hub) checkCmd(m []byte) {
 	//log.Print("Inside checkCmd")
 	s := string(m[:])
 
@@ -154,7 +160,7 @@ func checkCmd(m []byte) {
 			buftype := strings.Replace(args[3], "\n", "", -1)
 			bufferAlgorithm = buftype
 		}
-		go spHandlerOpen(args[1], baud, bufferAlgorithm)
+		go h.spHandlerOpen(args[1], baud, bufferAlgorithm)
 
 	} else if strings.HasPrefix(sl, "close") {
 
@@ -228,13 +234,13 @@ func checkCmd(m []byte) {
 	} else if strings.HasPrefix(sl, "exit") {
 		// Systray.Quit()
 	} else if strings.HasPrefix(sl, "memstats") {
-		memoryStats()
+		h.memoryStats()
 	} else if strings.HasPrefix(sl, "gc") {
-		garbageCollection()
+		h.garbageCollection()
 	} else if strings.HasPrefix(sl, "hostname") {
-		getHostname()
+		h.getHostname()
 	} else if strings.HasPrefix(sl, "version") {
-		getVersion()
+		h.getVersion()
 	} else {
 		go spErr("Could not understand command.")
 	}
@@ -254,7 +260,7 @@ func logAction(sl string) {
 	}
 }
 
-func memoryStats() {
+func (h *hub) memoryStats() {
 	var memStats runtime.MemStats
 	runtime.ReadMemStats(&memStats)
 	json, _ := json.Marshal(memStats)
@@ -262,22 +268,22 @@ func memoryStats() {
 	h.broadcastSys <- json
 }
 
-func getHostname() {
+func (h *hub) getHostname() {
 	h.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
 }
 
-func getVersion() {
+func (h *hub) getVersion() {
 	h.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
 }
 
-func garbageCollection() {
+func (h *hub) garbageCollection() {
 	log.Printf("Starting garbageCollection()\n")
 	h.broadcastSys <- []byte("{\"gc\":\"starting\"}")
-	memoryStats()
+	h.memoryStats()
 	debug.SetGCPercent(100)
 	debug.FreeOSMemory()
 	debug.SetGCPercent(-1)
 	log.Printf("Done with garbageCollection()\n")
 	h.broadcastSys <- []byte("{\"gc\":\"done\"}")
-	memoryStats()
+	h.memoryStats()
 }
diff --git a/main.go b/main.go
index bb63cb211..4510d1bba 100755
--- a/main.go
+++ b/main.go
@@ -107,14 +107,15 @@ var (
 	Index *index.Resource
 )
 
-type logWriter struct{}
+// TODO: enable it
+// type logWriter struct{}
 
-func (u *logWriter) Write(p []byte) (n int, err error) {
-	h.broadcastSys <- p
-	return len(p), nil
-}
+// func (u *logWriter) Write(p []byte) (n int, err error) {
+// 	h.broadcastSys <- p
+// 	return len(p), nil
+// }
 
-var loggerWs logWriter
+// var loggerWs logWriter
 
 func homeHandler(c *gin.Context) {
 	homeTemplate.Execute(c.Writer, c.Request.Host)
@@ -191,6 +192,8 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 		os.Exit(0)
 	}
 
+	h := NewHub()
+
 	logger := func(msg string) {
 		mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg}
 		mapB, _ := json.Marshal(mapD)
@@ -390,7 +393,7 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 
 	r := gin.New()
 
-	socketHandler := wsHandler().ServeHTTP
+	socketHandler := wsHandler(h).ServeHTTP
 
 	extraOrigins := []string{
 		"https://create.arduino.cc",
@@ -429,7 +432,7 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 	r.LoadHTMLFiles("templates/nofirefox.html")
 
 	r.GET("/", homeHandler)
-	r.POST("/upload", uploadHandler)
+	r.POST("/upload", UpdateHandler(stray))
 	r.GET("/socket.io/", socketHandler)
 	r.POST("/socket.io/", socketHandler)
 	r.Handle("WS", "/socket.io/", socketHandler)
diff --git a/serial.go b/serial.go
index 64e5b8f7f..b404f8f23 100755
--- a/serial.go
+++ b/serial.go
@@ -32,8 +32,13 @@ import (
 type serialhub struct {
 	// Opened serial ports.
 	ports map[*serport]bool
+	mu    sync.Mutex
+}
 
-	mu sync.Mutex
+func NewSerialHub() *serialhub {
+	return &serialhub{
+		ports: make(map[*serport]bool),
+	}
 }
 
 // SerialPortList is the serial port list
@@ -59,15 +64,11 @@ type SpPortItem struct {
 // serialPorts contains the ports attached to the machine
 var serialPorts SerialPortList
 
-var sh = serialhub{
-	ports: make(map[*serport]bool),
-}
-
 // Register serial ports from the connections.
 func (sh *serialhub) Register(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Registering a port: ", p.portConf.Name)
-	h.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}")
+	sh.hub.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}")
 	sh.ports[port] = true
 	sh.mu.Unlock()
 }
@@ -76,7 +77,7 @@ func (sh *serialhub) Register(port *serport) {
 func (sh *serialhub) Unregister(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Unregistering a port: ", p.portConf.Name)
-	h.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
+	sh.hub.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
 	delete(sh.ports, port)
 	close(port.sendBuffered)
 	close(port.sendNoBuf)
@@ -105,10 +106,10 @@ func (sp *SerialPortList) List() {
 
 	if err != nil {
 		//log.Println(err)
-		h.broadcastSys <- []byte("Error creating json on port list " +
+		sh.hub.broadcastSys <- []byte("Error creating json on port list " +
 			err.Error())
 	} else {
-		h.broadcastSys <- ls
+		sh.hub.broadcastSys <- ls
 	}
 }
 
@@ -257,13 +258,13 @@ func (sp *SerialPortList) getPortByName(portname string) *SpPortItem {
 
 func spErr(err string) {
 	//log.Println("Sending err back: ", err)
-	//h.broadcastSys <- []byte(err)
-	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
+	//sh.hub.broadcastSys <- []byte(err)
+	sh.hub.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
 }
 
 func spClose(portname string) {
 	if myport, ok := sh.FindPortByName(portname); ok {
-		h.broadcastSys <- []byte("Closing serial port " + portname)
+		sh.hub.broadcastSys <- []byte("Closing serial port " + portname)
 		myport.Close()
 	} else {
 		spErr("We could not find the serial port " + portname + " that you were trying to close.")
diff --git a/serialport.go b/serialport.go
index 0d386bbfc..1ee7bbf46 100755
--- a/serialport.go
+++ b/serialport.go
@@ -273,7 +273,7 @@ func (p *serport) writerRaw() {
 	h.broadcastSys <- []byte(msgstr)
 }
 
-func spHandlerOpen(portname string, baud int, buftype string) {
+func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	log.Print("Inside spHandler")
 
@@ -330,8 +330,8 @@ func spHandlerOpen(portname string, baud int, buftype string) {
 	bw.Init()
 	p.bufferwatcher = bw
 
-	sh.Register(p)
-	defer sh.Unregister(p)
+	h.serialHub.Register(p)
+	defer h.serialHub.Unregister(p)
 
 	serialPorts.MarkPortAsOpened(portname)
 	serialPorts.List()

From ef94d97f587046305ac7cdc59a0934a7c5b12e00 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Fri, 24 Jan 2025 18:02:39 +0100
Subject: [PATCH 09/50] remove global clients: use callback to avoid nesting
 import

---
 hub.go        | 60 +++++++++++++++++++++++++++++++++++----------------
 info.go       |  4 ++--
 main.go       | 12 +++++++----
 serial.go     | 48 +++++++++++++++++++++++------------------
 serialport.go | 54 ++++++++++++++++++++++++++++++++--------------
 5 files changed, 116 insertions(+), 62 deletions(-)

diff --git a/hub.go b/hub.go
index 5c4605b73..2af0e8845 100755
--- a/hub.go
+++ b/hub.go
@@ -19,7 +19,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"html"
-	"io"
 	"os"
 	"runtime"
 	"runtime/debug"
@@ -46,19 +45,41 @@ type hub struct {
 	// Unregister requests from connections.
 	unregister chan *connection
 
+	//TODO globals clients
 	// Serial hub to communicate with serial ports
 	serialHub *serialhub
+
+	serialPortList *SerialPortList
 }
 
-func NewHub() *hub {
-	return &hub{
-		broadcast:    make(chan []byte, 1000),
-		broadcastSys: make(chan []byte, 1000),
-		register:     make(chan *connection),
-		unregister:   make(chan *connection),
-		connections:  make(map[*connection]bool),
-		serialHub:    NewSerialHub(),
+func NewHub(serialhub *serialhub, serialList *SerialPortList) *hub {
+	hub := &hub{
+		broadcast:      make(chan []byte, 1000),
+		broadcastSys:   make(chan []byte, 1000),
+		register:       make(chan *connection),
+		unregister:     make(chan *connection),
+		connections:    make(map[*connection]bool),
+		serialHub:      serialhub,
+		serialPortList: serialList,
+	}
+
+	hub.serialHub.OnRegister = func(port *serport) {
+		hub.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}")
+	}
+
+	hub.serialHub.OnUnregister = func(port *serport) {
+		hub.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
 	}
+
+	hub.serialPortList.OnList = func(data []byte) {
+		hub.broadcastSys <- data
+	}
+
+	hub.serialPortList.OnErr = func(err string) {
+		hub.broadcastSys <- []byte("{\"Error\":\"" + err + "\"}")
+	}
+
+	return hub
 }
 
 const commands = `{
@@ -138,18 +159,18 @@ func (h *hub) checkCmd(m []byte) {
 
 		args := strings.Split(s, " ")
 		if len(args) < 3 {
-			go spErr("You did not specify a port and baud rate in your open cmd")
+			go h.spErr("You did not specify a port and baud rate in your open cmd")
 			return
 		}
 		if len(args[1]) < 1 {
-			go spErr("You did not specify a serial port")
+			go h.spErr("You did not specify a serial port")
 			return
 		}
 
 		baudStr := strings.Replace(args[2], "\n", "", -1)
 		baud, err := strconv.Atoi(baudStr)
 		if err != nil {
-			go spErr("Problem converting baud rate " + args[2])
+			go h.spErr("Problem converting baud rate " + args[2])
 			return
 		}
 		// pass in buffer type now as string. if user does not
@@ -166,9 +187,9 @@ func (h *hub) checkCmd(m []byte) {
 
 		args := strings.Split(s, " ")
 		if len(args) > 1 {
-			go spClose(args[1])
+			go h.spClose(args[1])
 		} else {
-			go spErr("You did not specify a port to close")
+			go h.spErr("You did not specify a port to close")
 		}
 
 	} else if strings.HasPrefix(sl, "killupload") {
@@ -181,9 +202,9 @@ func (h *hub) checkCmd(m []byte) {
 
 	} else if strings.HasPrefix(sl, "send") {
 		// will catch send and sendnobuf and sendraw
-		go spWrite(s)
+		go h.spWrite(s)
 	} else if strings.HasPrefix(sl, "list") {
-		go serialPorts.List()
+		go h.serialPortList.List()
 	} else if strings.HasPrefix(sl, "downloadtool") {
 		go func() {
 			args := strings.Split(s, " ")
@@ -242,15 +263,16 @@ func (h *hub) checkCmd(m []byte) {
 	} else if strings.HasPrefix(sl, "version") {
 		h.getVersion()
 	} else {
-		go spErr("Could not understand command.")
+		go h.spErr("Could not understand command.")
 	}
 }
 
 func logAction(sl string) {
 	if strings.HasPrefix(sl, "log on") {
 		*logDump = "on"
-		multiWriter := io.MultiWriter(&loggerWs, os.Stderr)
-		log.SetOutput(multiWriter)
+		//TODO: pass the loggerSw in the constructor and enable again the log e
+		// multiWriter := io.MultiWriter(&loggerWs, os.Stderr)
+		// log.SetOutput(multiWriter)
 	} else if strings.HasPrefix(sl, "log off") {
 		*logDump = "off"
 		log.SetOutput(os.Stderr)
diff --git a/info.go b/info.go
index 7da0f6986..b7a3585e4 100644
--- a/info.go
+++ b/info.go
@@ -41,12 +41,12 @@ func infoHandler(c *gin.Context) {
 	})
 }
 
-func PauseHandler(s *systray.Systray) func(c *gin.Context) {
+func PauseHandler(h *hub, s *systray.Systray) func(c *gin.Context) {
 	return func(c *gin.Context) {
 		go func() {
 			ports, _ := serial.GetPortsList()
 			for _, element := range ports {
-				spClose(element)
+				h.spClose(element)
 			}
 			*hibernate = true
 			s.Pause()
diff --git a/main.go b/main.go
index 4510d1bba..a17bb5dca 100755
--- a/main.go
+++ b/main.go
@@ -115,8 +115,6 @@ var (
 // 	return len(p), nil
 // }
 
-// var loggerWs logWriter
-
 func homeHandler(c *gin.Context) {
 	homeTemplate.Execute(c.Writer, c.Request.Host)
 }
@@ -192,7 +190,13 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 		os.Exit(0)
 	}
 
-	h := NewHub()
+	// serialPorts contains the ports attached to the machine
+	serialPorts := NewSerialPortList()
+	serialHub := NewSerialHub()
+
+	// var loggerWs logWriter
+
+	h := NewHub(serialHub, serialPorts)
 
 	logger := func(msg string) {
 		mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg}
@@ -438,7 +442,7 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 	r.Handle("WS", "/socket.io/", socketHandler)
 	r.Handle("WSS", "/socket.io/", socketHandler)
 	r.GET("/info", infoHandler)
-	r.POST("/pause", PauseHandler(stray))
+	r.POST("/pause", PauseHandler(h, stray))
 	r.POST("/update", UpdateHandler(stray))
 
 	// Mount goa handlers
diff --git a/serial.go b/serial.go
index b404f8f23..3333701a0 100755
--- a/serial.go
+++ b/serial.go
@@ -19,8 +19,8 @@ package main
 
 import (
 	"encoding/json"
+	"fmt"
 	"slices"
-	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -33,6 +33,9 @@ type serialhub struct {
 	// Opened serial ports.
 	ports map[*serport]bool
 	mu    sync.Mutex
+
+	OnRegister   func(port *serport)
+	OnUnregister func(port *serport)
 }
 
 func NewSerialHub() *serialhub {
@@ -45,6 +48,13 @@ func NewSerialHub() *serialhub {
 type SerialPortList struct {
 	Ports     []*SpPortItem
 	portsLock sync.Mutex
+
+	OnList func([]byte) `json:"-"`
+	OnErr  func(string) `json:"-"`
+}
+
+func NewSerialPortList() *SerialPortList {
+	return &SerialPortList{}
 }
 
 // SpPortItem is the serial port item
@@ -61,14 +71,11 @@ type SpPortItem struct {
 	ProductID       string
 }
 
-// serialPorts contains the ports attached to the machine
-var serialPorts SerialPortList
-
 // Register serial ports from the connections.
 func (sh *serialhub) Register(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Registering a port: ", p.portConf.Name)
-	sh.hub.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}")
+	sh.OnRegister(port)
 	sh.ports[port] = true
 	sh.mu.Unlock()
 }
@@ -77,7 +84,7 @@ func (sh *serialhub) Register(port *serport) {
 func (sh *serialhub) Unregister(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Unregistering a port: ", p.portConf.Name)
-	sh.hub.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
+	sh.OnUnregister(port)
 	delete(sh.ports, port)
 	close(port.sendBuffered)
 	close(port.sendNoBuf)
@@ -105,11 +112,9 @@ func (sp *SerialPortList) List() {
 	sp.portsLock.Unlock()
 
 	if err != nil {
-		//log.Println(err)
-		sh.hub.broadcastSys <- []byte("Error creating json on port list " +
-			err.Error())
+		sp.OnErr("Error creating json on port list " + err.Error())
 	} else {
-		sh.hub.broadcastSys <- ls
+		sp.OnList(ls)
 	}
 }
 
@@ -196,6 +201,7 @@ func (sp *SerialPortList) add(addedPort *discovery.Port) {
 
 	// If the port is already in the list, just update the metadata...
 	for _, oldPort := range sp.Ports {
+		fmt.Println("oldPort.Name: ", oldPort.Name)
 		if oldPort.Name == addedPort.Address {
 			oldPort.SerialNumber = props.Get("serialNumber")
 			oldPort.VendorID = vid
@@ -256,22 +262,22 @@ func (sp *SerialPortList) getPortByName(portname string) *SpPortItem {
 	return nil
 }
 
-func spErr(err string) {
+func (h *hub) spErr(err string) {
 	//log.Println("Sending err back: ", err)
 	//sh.hub.broadcastSys <- []byte(err)
-	sh.hub.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
+	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
 }
 
-func spClose(portname string) {
-	if myport, ok := sh.FindPortByName(portname); ok {
-		sh.hub.broadcastSys <- []byte("Closing serial port " + portname)
+func (h *hub) spClose(portname string) {
+	if myport, ok := h.serialHub.FindPortByName(portname); ok {
+		h.broadcastSys <- []byte("Closing serial port " + portname)
 		myport.Close()
 	} else {
-		spErr("We could not find the serial port " + portname + " that you were trying to close.")
+		h.spErr("We could not find the serial port " + portname + " that you were trying to close.")
 	}
 }
 
-func spWrite(arg string) {
+func (h *hub) spWrite(arg string) {
 	// we will get a string of comXX asdf asdf asdf
 	//log.Println("Inside spWrite arg: " + arg)
 	arg = strings.TrimPrefix(arg, " ")
@@ -280,7 +286,7 @@ func spWrite(arg string) {
 	if len(args) != 3 {
 		errstr := "Could not parse send command: " + arg
 		//log.Println(errstr)
-		spErr(errstr)
+		h.spErr(errstr)
 		return
 	}
 	bufferingMode := args[0]
@@ -291,10 +297,10 @@ func spWrite(arg string) {
 	//log.Println("The data is:" + data + "---")
 
 	// See if we have this port open
-	port, ok := sh.FindPortByName(portname)
+	port, ok := h.serialHub.FindPortByName(portname)
 	if !ok {
 		// we couldn't find the port, so send err
-		spErr("We could not find the serial port " + portname + " that you were trying to write to.")
+		h.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
 		return
 	}
 
@@ -303,7 +309,7 @@ func spWrite(arg string) {
 	case "send", "sendnobuf", "sendraw":
 		// valid buffering mode, go ahead
 	default:
-		spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
+		h.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
 		return
 	}
 
diff --git a/serialport.go b/serialport.go
index 1ee7bbf46..06ea0aa64 100755
--- a/serialport.go
+++ b/serialport.go
@@ -61,6 +61,10 @@ type serport struct {
 	BufferType string
 	//bufferwatcher *BufferflowDummypause
 	bufferwatcher Bufferflow
+
+	// TODO: to remove global
+	OnMessage func([]byte)
+	OnClose   func(*serport)
 }
 
 // SpPortMessage is the serial port message
@@ -89,7 +93,8 @@ func (p *serport) reader(buftype string) {
 		if p.isClosing.Load() {
 			strmsg := "Shutting down reader on " + p.portConf.Name
 			log.Println(strmsg)
-			h.broadcastSys <- []byte(strmsg)
+			// h.broadcastSys <- ([]byte(strmsg)
+			p.OnMessage([]byte(strmsg))
 			break
 		}
 
@@ -143,15 +148,19 @@ func (p *serport) reader(buftype string) {
 			if err == io.EOF || err == io.ErrUnexpectedEOF {
 				// hit end of file
 				log.Println("Hit end of file on serial port")
-				h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}")
+				// h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}")
+				p.OnMessage([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}"))
 
 			}
 
 			if err != nil {
 				log.Println(err)
-				h.broadcastSys <- []byte("Error reading on " + p.portConf.Name + " " +
-					err.Error() + " Closing port.")
-				h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}")
+				// h.broadcastSys <- []byte("Error reading on " + p.portConf.Name + " " +
+				// 	err.Error() + " Closing port.")
+				p.OnMessage([]byte("Error reading on " + p.portConf.Name + " " + err.Error() + " Closing port."))
+
+				// h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}")
+				p.OnMessage([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}"))
 				p.isClosingDueToError = true
 				break
 			}
@@ -209,7 +218,8 @@ func (p *serport) writerBuffered() {
 	}
 	msgstr := "writerBuffered just got closed. make sure you make a new one. port:" + p.portConf.Name
 	log.Println(msgstr)
-	h.broadcastSys <- []byte(msgstr)
+	// h.broadcastSys <- []byte(msgstr)
+	p.OnMessage([]byte(msgstr))
 }
 
 // this method runs as its own thread because it's instantiated
@@ -230,15 +240,18 @@ func (p *serport) writerNoBuf() {
 		if err != nil {
 			errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port."
 			log.Print(errstr)
-			h.broadcastSys <- []byte(errstr)
+			// h.broadcastSys <- []byte(errstr)
+			p.OnMessage([]byte(errstr))
 			break
 		}
 	}
 	msgstr := "Shutting down writer on " + p.portConf.Name
 	log.Println(msgstr)
-	h.broadcastSys <- []byte(msgstr)
+	// h.broadcastSys <- []byte(msgstr)
+	p.OnMessage([]byte(msgstr))
 	p.portIo.Close()
-	serialPorts.List()
+	// TODO: is this needed ?
+	// serialPorts.List()
 }
 
 // this method runs as its own thread because it's instantiated
@@ -270,7 +283,8 @@ func (p *serport) writerRaw() {
 	}
 	msgstr := "writerRaw just got closed. make sure you make a new one. port:" + p.portConf.Name
 	log.Println(msgstr)
-	h.broadcastSys <- []byte(msgstr)
+	// h.broadcastSys <- []byte(msgstr)
+	p.OnMessage([]byte(msgstr))
 }
 
 func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
@@ -312,7 +326,16 @@ func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 		portConf:     conf,
 		portIo:       sp,
 		portName:     portname,
-		BufferType:   buftype}
+		BufferType:   buftype,
+	}
+
+	p.OnMessage = func(msg []byte) {
+		h.broadcastSys <- msg
+	}
+	p.OnClose = func(port *serport) {
+		h.serialPortList.MarkPortAsClosed(p.portName)
+		h.serialPortList.List()
+	}
 
 	var bw Bufferflow
 
@@ -333,8 +356,8 @@ func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 	h.serialHub.Register(p)
 	defer h.serialHub.Unregister(p)
 
-	serialPorts.MarkPortAsOpened(portname)
-	serialPorts.List()
+	h.serialPortList.MarkPortAsOpened(portname)
+	h.serialPortList.List()
 
 	// this is internally buffered thread to not send to serial port if blocked
 	go p.writerBuffered()
@@ -345,7 +368,7 @@ func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	p.reader(buftype)
 
-	serialPorts.List()
+	h.serialPortList.List()
 }
 
 func (p *serport) Close() {
@@ -353,6 +376,5 @@ func (p *serport) Close() {
 
 	p.bufferwatcher.Close()
 	p.portIo.Close()
-	serialPorts.MarkPortAsClosed(p.portName)
-	serialPorts.List()
+	p.OnClose(p)
 }

From 640e6449d79634bf8b3a983058b4e03083c94043 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Mon, 31 Mar 2025 17:06:28 +0200
Subject: [PATCH 10/50] refactor: rename variable 'h' to 'hub' for clarity in
 main.go and update related references

---
 main.go      | 12 ++++++------
 main_test.go |  5 +++--
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/main.go b/main.go
index e04af0439..419ebd8ef 100755
--- a/main.go
+++ b/main.go
@@ -196,12 +196,12 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 
 	// var loggerWs logWriter
 
-	h := NewHub(serialHub, serialPorts)
+	hub := NewHub(serialHub, serialPorts)
 
 	logger := func(msg string) {
 		mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg}
 		mapB, _ := json.Marshal(mapD)
-		h.broadcastSys <- mapB
+		hub.broadcastSys <- mapB
 	}
 
 	// if the default browser is Safari, prompt the user to install HTTPS certificates
@@ -399,13 +399,13 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 	// launch the discoveries for the running system
 	go serialPorts.Run()
 	// launch the hub routine which is the singleton for the websocket server
-	go h.run()
+	go hub.run()
 	// launch our dummy data routine
 	//go d.run()
 
 	r := gin.New()
 
-	socketHandler := wsHandler(h).ServeHTTP
+	socketHandler := wsHandler(hub).ServeHTTP
 
 	extraOrigins := []string{
 		"https://create.arduino.cc",
@@ -444,13 +444,13 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 	r.LoadHTMLFiles("templates/nofirefox.html")
 
 	r.GET("/", homeHandler)
-	r.POST("/upload", uploadHandler(h, signaturePubKey))
+	r.POST("/upload", uploadHandler(hub, signaturePubKey))
 	r.GET("/socket.io/", socketHandler)
 	r.POST("/socket.io/", socketHandler)
 	r.Handle("WS", "/socket.io/", socketHandler)
 	r.Handle("WSS", "/socket.io/", socketHandler)
 	r.GET("/info", infoHandler)
-	r.POST("/pause", PauseHandler(h, stray))
+	r.POST("/pause", PauseHandler(hub, stray))
 	r.POST("/update", UpdateHandler(stray))
 
 	// Mount goa handlers
diff --git a/main_test.go b/main_test.go
index 1387fd221..67cb91a1d 100644
--- a/main_test.go
+++ b/main_test.go
@@ -56,7 +56,7 @@ func TestValidSignatureKey(t *testing.T) {
 
 func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 	r := gin.New()
-	r.POST("/", uploadHandler(utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
+	r.POST("/", uploadHandler(NewHub(NewSerialHub(), NewSerialPortList()), utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
 	ts := httptest.NewServer(r)
 
 	uploadEvilFileName := Upload{
@@ -92,7 +92,8 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 
 func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) {
 	r := gin.New()
-	r.POST("/", uploadHandler(utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
+
+	r.POST("/", uploadHandler(NewHub(NewSerialHub(), NewSerialPortList()), utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
 	ts := httptest.NewServer(r)
 	defer ts.Close()
 

From 99f8235a12c0492f014502c28848b203d61c165c Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Mon, 31 Mar 2025 17:39:43 +0200
Subject: [PATCH 11/50] docs: add comments to clarify functions and handlers in
 config, hub, serial, and update files

---
 config/config.go | 5 +++++
 hub.go           | 2 ++
 info.go          | 2 +-
 main.go          | 4 ++--
 serial.go        | 3 +++
 update.go        | 2 +-
 6 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/config/config.go b/config/config.go
index 9919bfbbc..82edecf41 100644
--- a/config/config.go
+++ b/config/config.go
@@ -143,6 +143,11 @@ func SetInstallCertsIni(filename string, value string) error {
 	return nil
 }
 
+// GetConfigPath returns the full path to the Arduino Create Agent configuration file.
+// It will check if the config file exists in the default location
+// and if not, it will generate a new one.
+// It will also check if the ARDUINO_CREATE_AGENT_CONFIG environment variable is set,
+// and if so, it will use that path instead of the default one.
 func GetConfigPath() *paths.Path {
 	// Let's handle the config
 	configDir := GetDefaultConfigDir()
diff --git a/hub.go b/hub.go
index 2af0e8845..0c95a2a22 100755
--- a/hub.go
+++ b/hub.go
@@ -52,6 +52,8 @@ type hub struct {
 	serialPortList *SerialPortList
 }
 
+// NewHub creates a hub that acts as a central hub for handling
+// WebSocket connections, broadcasting messages, and processing commands.
 func NewHub(serialhub *serialhub, serialList *SerialPortList) *hub {
 	hub := &hub{
 		broadcast:      make(chan []byte, 1000),
diff --git a/info.go b/info.go
index b7a3585e4..bf04713e3 100644
--- a/info.go
+++ b/info.go
@@ -41,7 +41,7 @@ func infoHandler(c *gin.Context) {
 	})
 }
 
-func PauseHandler(h *hub, s *systray.Systray) func(c *gin.Context) {
+func pauseHandler(h *hub, s *systray.Systray) func(c *gin.Context) {
 	return func(c *gin.Context) {
 		go func() {
 			ports, _ := serial.GetPortsList()
diff --git a/main.go b/main.go
index 419ebd8ef..c23050119 100755
--- a/main.go
+++ b/main.go
@@ -450,8 +450,8 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 	r.Handle("WS", "/socket.io/", socketHandler)
 	r.Handle("WSS", "/socket.io/", socketHandler)
 	r.GET("/info", infoHandler)
-	r.POST("/pause", PauseHandler(hub, stray))
-	r.POST("/update", UpdateHandler(stray))
+	r.POST("/pause", pauseHandler(hub, stray))
+	r.POST("/update", updateHandler(stray))
 
 	// Mount goa handlers
 	goa := v2.Server(config.GetDataDir().String(), Index, signaturePubKey)
diff --git a/serial.go b/serial.go
index 3333701a0..da959ad75 100755
--- a/serial.go
+++ b/serial.go
@@ -38,6 +38,8 @@ type serialhub struct {
 	OnUnregister func(port *serport)
 }
 
+// NewSerialHub creates a new serial hub
+// It is used to manage serial ports and their connections.
 func NewSerialHub() *serialhub {
 	return &serialhub{
 		ports: make(map[*serport]bool),
@@ -53,6 +55,7 @@ type SerialPortList struct {
 	OnErr  func(string) `json:"-"`
 }
 
+// NewSerialPortList creates a new serial port list
 func NewSerialPortList() *SerialPortList {
 	return &SerialPortList{}
 }
diff --git a/update.go b/update.go
index 0065a09b5..a12f1feb2 100644
--- a/update.go
+++ b/update.go
@@ -35,7 +35,7 @@ import (
 	"github.com/gin-gonic/gin"
 )
 
-func UpdateHandler(s *systray.Systray) func(c *gin.Context) {
+func updateHandler(s *systray.Systray) func(c *gin.Context) {
 	return func(c *gin.Context) {
 		restartPath, err := updater.CheckForUpdates(version, *updateURL, *appName)
 		if err != nil {

From 5e2161e1eea2de99e6fb8344099d6d11f16f58f9 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Mon, 31 Mar 2025 17:41:40 +0200
Subject: [PATCH 12/50] refactor: rename 'hub' and 'serialhub' types to 'Hub'
 and 'Serialhub' for consistency

---
 conn.go       |  8 ++++----
 hub.go        | 24 ++++++++++++------------
 info.go       |  2 +-
 serial.go     | 18 +++++++++---------
 serialport.go |  2 +-
 5 files changed, 27 insertions(+), 27 deletions(-)

diff --git a/conn.go b/conn.go
index 584e1328f..f307f0c75 100644
--- a/conn.go
+++ b/conn.go
@@ -80,7 +80,7 @@ type Upload struct {
 
 var uploadStatusStr = "ProgrammerStatus"
 
-func uploadHandler(h *hub, pubKey *rsa.PublicKey) func(*gin.Context) {
+func uploadHandler(h *Hub, pubKey *rsa.PublicKey) func(*gin.Context) {
 	return func(c *gin.Context) {
 		data := new(Upload)
 		if err := c.BindJSON(data); err != nil {
@@ -193,7 +193,7 @@ func uploadHandler(h *hub, pubKey *rsa.PublicKey) func(*gin.Context) {
 // PLogger sends the info from the upload to the websocket
 type PLogger struct {
 	Verbose bool
-	h       *hub
+	h       *Hub
 }
 
 // Debug only sends messages if verbose is true (always true for now)
@@ -210,12 +210,12 @@ func (l PLogger) Info(args ...interface{}) {
 	send(l.h, map[string]string{uploadStatusStr: "Busy", "Msg": output})
 }
 
-func send(h *hub, args map[string]string) {
+func send(h *Hub, args map[string]string) {
 	mapB, _ := json.Marshal(args)
 	h.broadcastSys <- mapB
 }
 
-func wsHandler(h *hub) *WsServer {
+func wsHandler(h *Hub) *WsServer {
 	server, err := socketio.NewServer(nil)
 	if err != nil {
 		log.Fatal(err)
diff --git a/hub.go b/hub.go
index 0c95a2a22..4013dfb09 100755
--- a/hub.go
+++ b/hub.go
@@ -29,7 +29,7 @@ import (
 	log "github.com/sirupsen/logrus"
 )
 
-type hub struct {
+type Hub struct {
 	// Registered connections.
 	connections map[*connection]bool
 
@@ -47,15 +47,15 @@ type hub struct {
 
 	//TODO globals clients
 	// Serial hub to communicate with serial ports
-	serialHub *serialhub
+	serialHub *Serialhub
 
 	serialPortList *SerialPortList
 }
 
 // NewHub creates a hub that acts as a central hub for handling
 // WebSocket connections, broadcasting messages, and processing commands.
-func NewHub(serialhub *serialhub, serialList *SerialPortList) *hub {
-	hub := &hub{
+func NewHub(serialhub *Serialhub, serialList *SerialPortList) *Hub {
+	hub := &Hub{
 		broadcast:      make(chan []byte, 1000),
 		broadcastSys:   make(chan []byte, 1000),
 		register:       make(chan *connection),
@@ -102,7 +102,7 @@ const commands = `{
   ]
 }`
 
-func (h *hub) unregisterConnection(c *connection) {
+func (h *Hub) unregisterConnection(c *connection) {
 	if _, contains := h.connections[c]; !contains {
 		return
 	}
@@ -110,7 +110,7 @@ func (h *hub) unregisterConnection(c *connection) {
 	close(c.send)
 }
 
-func (h *hub) sendToRegisteredConnections(data []byte) {
+func (h *Hub) sendToRegisteredConnections(data []byte) {
 	for c := range h.connections {
 		select {
 		case c.send <- data:
@@ -123,7 +123,7 @@ func (h *hub) sendToRegisteredConnections(data []byte) {
 	}
 }
 
-func (h *hub) run() {
+func (h *Hub) run() {
 	for {
 		select {
 		case c := <-h.register:
@@ -146,7 +146,7 @@ func (h *hub) run() {
 	}
 }
 
-func (h *hub) checkCmd(m []byte) {
+func (h *Hub) checkCmd(m []byte) {
 	//log.Print("Inside checkCmd")
 	s := string(m[:])
 
@@ -284,7 +284,7 @@ func logAction(sl string) {
 	}
 }
 
-func (h *hub) memoryStats() {
+func (h *Hub) memoryStats() {
 	var memStats runtime.MemStats
 	runtime.ReadMemStats(&memStats)
 	json, _ := json.Marshal(memStats)
@@ -292,15 +292,15 @@ func (h *hub) memoryStats() {
 	h.broadcastSys <- json
 }
 
-func (h *hub) getHostname() {
+func (h *Hub) getHostname() {
 	h.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
 }
 
-func (h *hub) getVersion() {
+func (h *Hub) getVersion() {
 	h.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
 }
 
-func (h *hub) garbageCollection() {
+func (h *Hub) garbageCollection() {
 	log.Printf("Starting garbageCollection()\n")
 	h.broadcastSys <- []byte("{\"gc\":\"starting\"}")
 	h.memoryStats()
diff --git a/info.go b/info.go
index bf04713e3..ba77cf401 100644
--- a/info.go
+++ b/info.go
@@ -41,7 +41,7 @@ func infoHandler(c *gin.Context) {
 	})
 }
 
-func pauseHandler(h *hub, s *systray.Systray) func(c *gin.Context) {
+func pauseHandler(h *Hub, s *systray.Systray) func(c *gin.Context) {
 	return func(c *gin.Context) {
 		go func() {
 			ports, _ := serial.GetPortsList()
diff --git a/serial.go b/serial.go
index da959ad75..8c9589991 100755
--- a/serial.go
+++ b/serial.go
@@ -29,7 +29,7 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-type serialhub struct {
+type Serialhub struct {
 	// Opened serial ports.
 	ports map[*serport]bool
 	mu    sync.Mutex
@@ -40,8 +40,8 @@ type serialhub struct {
 
 // NewSerialHub creates a new serial hub
 // It is used to manage serial ports and their connections.
-func NewSerialHub() *serialhub {
-	return &serialhub{
+func NewSerialHub() *Serialhub {
+	return &Serialhub{
 		ports: make(map[*serport]bool),
 	}
 }
@@ -75,7 +75,7 @@ type SpPortItem struct {
 }
 
 // Register serial ports from the connections.
-func (sh *serialhub) Register(port *serport) {
+func (sh *Serialhub) Register(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Registering a port: ", p.portConf.Name)
 	sh.OnRegister(port)
@@ -84,7 +84,7 @@ func (sh *serialhub) Register(port *serport) {
 }
 
 // Unregister requests from connections.
-func (sh *serialhub) Unregister(port *serport) {
+func (sh *Serialhub) Unregister(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Unregistering a port: ", p.portConf.Name)
 	sh.OnUnregister(port)
@@ -94,7 +94,7 @@ func (sh *serialhub) Unregister(port *serport) {
 	sh.mu.Unlock()
 }
 
-func (sh *serialhub) FindPortByName(portname string) (*serport, bool) {
+func (sh *Serialhub) FindPortByName(portname string) (*serport, bool) {
 	sh.mu.Lock()
 	defer sh.mu.Unlock()
 
@@ -265,13 +265,13 @@ func (sp *SerialPortList) getPortByName(portname string) *SpPortItem {
 	return nil
 }
 
-func (h *hub) spErr(err string) {
+func (h *Hub) spErr(err string) {
 	//log.Println("Sending err back: ", err)
 	//sh.hub.broadcastSys <- []byte(err)
 	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
 }
 
-func (h *hub) spClose(portname string) {
+func (h *Hub) spClose(portname string) {
 	if myport, ok := h.serialHub.FindPortByName(portname); ok {
 		h.broadcastSys <- []byte("Closing serial port " + portname)
 		myport.Close()
@@ -280,7 +280,7 @@ func (h *hub) spClose(portname string) {
 	}
 }
 
-func (h *hub) spWrite(arg string) {
+func (h *Hub) spWrite(arg string) {
 	// we will get a string of comXX asdf asdf asdf
 	//log.Println("Inside spWrite arg: " + arg)
 	arg = strings.TrimPrefix(arg, " ")
diff --git a/serialport.go b/serialport.go
index 06ea0aa64..06b16ff41 100755
--- a/serialport.go
+++ b/serialport.go
@@ -287,7 +287,7 @@ func (p *serport) writerRaw() {
 	p.OnMessage([]byte(msgstr))
 }
 
-func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
+func (h *Hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	log.Print("Inside spHandler")
 

From 910bac22cf515a1eed175c31a9d9770b52e17793 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Mon, 31 Mar 2025 17:48:44 +0200
Subject: [PATCH 13/50] refactor: rename 'Hub' and 'Serialhub' types to 'hub'
 and 'serialhub' for consistency

---
 conn.go       |  8 ++++----
 hub.go        | 28 +++++++++++++---------------
 info.go       |  2 +-
 main.go       |  6 +++---
 main_test.go  |  4 ++--
 serial.go     | 46 +++++++++++++++++++++-------------------------
 serialport.go |  2 +-
 7 files changed, 45 insertions(+), 51 deletions(-)

diff --git a/conn.go b/conn.go
index f307f0c75..584e1328f 100644
--- a/conn.go
+++ b/conn.go
@@ -80,7 +80,7 @@ type Upload struct {
 
 var uploadStatusStr = "ProgrammerStatus"
 
-func uploadHandler(h *Hub, pubKey *rsa.PublicKey) func(*gin.Context) {
+func uploadHandler(h *hub, pubKey *rsa.PublicKey) func(*gin.Context) {
 	return func(c *gin.Context) {
 		data := new(Upload)
 		if err := c.BindJSON(data); err != nil {
@@ -193,7 +193,7 @@ func uploadHandler(h *Hub, pubKey *rsa.PublicKey) func(*gin.Context) {
 // PLogger sends the info from the upload to the websocket
 type PLogger struct {
 	Verbose bool
-	h       *Hub
+	h       *hub
 }
 
 // Debug only sends messages if verbose is true (always true for now)
@@ -210,12 +210,12 @@ func (l PLogger) Info(args ...interface{}) {
 	send(l.h, map[string]string{uploadStatusStr: "Busy", "Msg": output})
 }
 
-func send(h *Hub, args map[string]string) {
+func send(h *hub, args map[string]string) {
 	mapB, _ := json.Marshal(args)
 	h.broadcastSys <- mapB
 }
 
-func wsHandler(h *Hub) *WsServer {
+func wsHandler(h *hub) *WsServer {
 	server, err := socketio.NewServer(nil)
 	if err != nil {
 		log.Fatal(err)
diff --git a/hub.go b/hub.go
index 4013dfb09..d812d6b80 100755
--- a/hub.go
+++ b/hub.go
@@ -29,7 +29,7 @@ import (
 	log "github.com/sirupsen/logrus"
 )
 
-type Hub struct {
+type hub struct {
 	// Registered connections.
 	connections map[*connection]bool
 
@@ -47,15 +47,13 @@ type Hub struct {
 
 	//TODO globals clients
 	// Serial hub to communicate with serial ports
-	serialHub *Serialhub
+	serialHub *serialhub
 
-	serialPortList *SerialPortList
+	serialPortList *serialPortList
 }
 
-// NewHub creates a hub that acts as a central hub for handling
-// WebSocket connections, broadcasting messages, and processing commands.
-func NewHub(serialhub *Serialhub, serialList *SerialPortList) *Hub {
-	hub := &Hub{
+func newHub(serialhub *serialhub, serialList *serialPortList) *hub {
+	hub := &hub{
 		broadcast:      make(chan []byte, 1000),
 		broadcastSys:   make(chan []byte, 1000),
 		register:       make(chan *connection),
@@ -102,7 +100,7 @@ const commands = `{
   ]
 }`
 
-func (h *Hub) unregisterConnection(c *connection) {
+func (h *hub) unregisterConnection(c *connection) {
 	if _, contains := h.connections[c]; !contains {
 		return
 	}
@@ -110,7 +108,7 @@ func (h *Hub) unregisterConnection(c *connection) {
 	close(c.send)
 }
 
-func (h *Hub) sendToRegisteredConnections(data []byte) {
+func (h *hub) sendToRegisteredConnections(data []byte) {
 	for c := range h.connections {
 		select {
 		case c.send <- data:
@@ -123,7 +121,7 @@ func (h *Hub) sendToRegisteredConnections(data []byte) {
 	}
 }
 
-func (h *Hub) run() {
+func (h *hub) run() {
 	for {
 		select {
 		case c := <-h.register:
@@ -146,7 +144,7 @@ func (h *Hub) run() {
 	}
 }
 
-func (h *Hub) checkCmd(m []byte) {
+func (h *hub) checkCmd(m []byte) {
 	//log.Print("Inside checkCmd")
 	s := string(m[:])
 
@@ -284,7 +282,7 @@ func logAction(sl string) {
 	}
 }
 
-func (h *Hub) memoryStats() {
+func (h *hub) memoryStats() {
 	var memStats runtime.MemStats
 	runtime.ReadMemStats(&memStats)
 	json, _ := json.Marshal(memStats)
@@ -292,15 +290,15 @@ func (h *Hub) memoryStats() {
 	h.broadcastSys <- json
 }
 
-func (h *Hub) getHostname() {
+func (h *hub) getHostname() {
 	h.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
 }
 
-func (h *Hub) getVersion() {
+func (h *hub) getVersion() {
 	h.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
 }
 
-func (h *Hub) garbageCollection() {
+func (h *hub) garbageCollection() {
 	log.Printf("Starting garbageCollection()\n")
 	h.broadcastSys <- []byte("{\"gc\":\"starting\"}")
 	h.memoryStats()
diff --git a/info.go b/info.go
index ba77cf401..bf04713e3 100644
--- a/info.go
+++ b/info.go
@@ -41,7 +41,7 @@ func infoHandler(c *gin.Context) {
 	})
 }
 
-func pauseHandler(h *Hub, s *systray.Systray) func(c *gin.Context) {
+func pauseHandler(h *hub, s *systray.Systray) func(c *gin.Context) {
 	return func(c *gin.Context) {
 		go func() {
 			ports, _ := serial.GetPortsList()
diff --git a/main.go b/main.go
index c23050119..a325c2348 100755
--- a/main.go
+++ b/main.go
@@ -191,12 +191,12 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 	}
 
 	// serialPorts contains the ports attached to the machine
-	serialPorts := NewSerialPortList()
-	serialHub := NewSerialHub()
+	serialPorts := newSerialPortList()
+	serialHub := newSerialHub()
 
 	// var loggerWs logWriter
 
-	hub := NewHub(serialHub, serialPorts)
+	hub := newHub(serialHub, serialPorts)
 
 	logger := func(msg string) {
 		mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg}
diff --git a/main_test.go b/main_test.go
index 67cb91a1d..d30498669 100644
--- a/main_test.go
+++ b/main_test.go
@@ -56,7 +56,7 @@ func TestValidSignatureKey(t *testing.T) {
 
 func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 	r := gin.New()
-	r.POST("/", uploadHandler(NewHub(NewSerialHub(), NewSerialPortList()), utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
+	r.POST("/", uploadHandler(newHub(newSerialHub(), newSerialPortList()), utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
 	ts := httptest.NewServer(r)
 
 	uploadEvilFileName := Upload{
@@ -93,7 +93,7 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) {
 	r := gin.New()
 
-	r.POST("/", uploadHandler(NewHub(NewSerialHub(), NewSerialPortList()), utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
+	r.POST("/", uploadHandler(newHub(newSerialHub(), newSerialPortList()), utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
 	ts := httptest.NewServer(r)
 	defer ts.Close()
 
diff --git a/serial.go b/serial.go
index 8c9589991..c3f1b38bf 100755
--- a/serial.go
+++ b/serial.go
@@ -29,7 +29,7 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-type Serialhub struct {
+type serialhub struct {
 	// Opened serial ports.
 	ports map[*serport]bool
 	mu    sync.Mutex
@@ -38,16 +38,13 @@ type Serialhub struct {
 	OnUnregister func(port *serport)
 }
 
-// NewSerialHub creates a new serial hub
-// It is used to manage serial ports and their connections.
-func NewSerialHub() *Serialhub {
-	return &Serialhub{
+func newSerialHub() *serialhub {
+	return &serialhub{
 		ports: make(map[*serport]bool),
 	}
 }
 
-// SerialPortList is the serial port list
-type SerialPortList struct {
+type serialPortList struct {
 	Ports     []*SpPortItem
 	portsLock sync.Mutex
 
@@ -55,9 +52,8 @@ type SerialPortList struct {
 	OnErr  func(string) `json:"-"`
 }
 
-// NewSerialPortList creates a new serial port list
-func NewSerialPortList() *SerialPortList {
-	return &SerialPortList{}
+func newSerialPortList() *serialPortList {
+	return &serialPortList{}
 }
 
 // SpPortItem is the serial port item
@@ -75,7 +71,7 @@ type SpPortItem struct {
 }
 
 // Register serial ports from the connections.
-func (sh *Serialhub) Register(port *serport) {
+func (sh *serialhub) Register(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Registering a port: ", p.portConf.Name)
 	sh.OnRegister(port)
@@ -84,7 +80,7 @@ func (sh *Serialhub) Register(port *serport) {
 }
 
 // Unregister requests from connections.
-func (sh *Serialhub) Unregister(port *serport) {
+func (sh *serialhub) Unregister(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Unregistering a port: ", p.portConf.Name)
 	sh.OnUnregister(port)
@@ -94,7 +90,7 @@ func (sh *Serialhub) Unregister(port *serport) {
 	sh.mu.Unlock()
 }
 
-func (sh *Serialhub) FindPortByName(portname string) (*serport, bool) {
+func (sh *serialhub) FindPortByName(portname string) (*serport, bool) {
 	sh.mu.Lock()
 	defer sh.mu.Unlock()
 
@@ -109,7 +105,7 @@ func (sh *Serialhub) FindPortByName(portname string) (*serport, bool) {
 }
 
 // List broadcasts a Json representation of the ports found
-func (sp *SerialPortList) List() {
+func (sp *serialPortList) List() {
 	sp.portsLock.Lock()
 	ls, err := json.MarshalIndent(sp, "", "\t")
 	sp.portsLock.Unlock()
@@ -122,7 +118,7 @@ func (sp *SerialPortList) List() {
 }
 
 // Run is the main loop for port discovery and management
-func (sp *SerialPortList) Run() {
+func (sp *serialPortList) Run() {
 	for retries := 0; retries < 10; retries++ {
 		sp.runSerialDiscovery()
 
@@ -132,7 +128,7 @@ func (sp *SerialPortList) Run() {
 	logrus.Errorf("Failed restarting serial discovery. Giving up...")
 }
 
-func (sp *SerialPortList) runSerialDiscovery() {
+func (sp *serialPortList) runSerialDiscovery() {
 	// First ensure that all the discoveries are available
 	if err := Tools.Download("builtin", "serial-discovery", "latest", "keep"); err != nil {
 		logrus.Errorf("Error downloading serial-discovery: %s", err)
@@ -176,13 +172,13 @@ func (sp *SerialPortList) runSerialDiscovery() {
 	logrus.Errorf("Serial discovery stopped.")
 }
 
-func (sp *SerialPortList) reset() {
+func (sp *serialPortList) reset() {
 	sp.portsLock.Lock()
 	defer sp.portsLock.Unlock()
 	sp.Ports = []*SpPortItem{}
 }
 
-func (sp *SerialPortList) add(addedPort *discovery.Port) {
+func (sp *serialPortList) add(addedPort *discovery.Port) {
 	if addedPort.Protocol != "serial" {
 		return
 	}
@@ -226,7 +222,7 @@ func (sp *SerialPortList) add(addedPort *discovery.Port) {
 	})
 }
 
-func (sp *SerialPortList) remove(removedPort *discovery.Port) {
+func (sp *serialPortList) remove(removedPort *discovery.Port) {
 	sp.portsLock.Lock()
 	defer sp.portsLock.Unlock()
 
@@ -237,7 +233,7 @@ func (sp *SerialPortList) remove(removedPort *discovery.Port) {
 }
 
 // MarkPortAsOpened marks a port as opened by the user
-func (sp *SerialPortList) MarkPortAsOpened(portname string) {
+func (sp *serialPortList) MarkPortAsOpened(portname string) {
 	sp.portsLock.Lock()
 	defer sp.portsLock.Unlock()
 	port := sp.getPortByName(portname)
@@ -247,7 +243,7 @@ func (sp *SerialPortList) MarkPortAsOpened(portname string) {
 }
 
 // MarkPortAsClosed marks a port as no more opened by the user
-func (sp *SerialPortList) MarkPortAsClosed(portname string) {
+func (sp *serialPortList) MarkPortAsClosed(portname string) {
 	sp.portsLock.Lock()
 	defer sp.portsLock.Unlock()
 	port := sp.getPortByName(portname)
@@ -256,7 +252,7 @@ func (sp *SerialPortList) MarkPortAsClosed(portname string) {
 	}
 }
 
-func (sp *SerialPortList) getPortByName(portname string) *SpPortItem {
+func (sp *serialPortList) getPortByName(portname string) *SpPortItem {
 	for _, port := range sp.Ports {
 		if port.Name == portname {
 			return port
@@ -265,13 +261,13 @@ func (sp *SerialPortList) getPortByName(portname string) *SpPortItem {
 	return nil
 }
 
-func (h *Hub) spErr(err string) {
+func (h *hub) spErr(err string) {
 	//log.Println("Sending err back: ", err)
 	//sh.hub.broadcastSys <- []byte(err)
 	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
 }
 
-func (h *Hub) spClose(portname string) {
+func (h *hub) spClose(portname string) {
 	if myport, ok := h.serialHub.FindPortByName(portname); ok {
 		h.broadcastSys <- []byte("Closing serial port " + portname)
 		myport.Close()
@@ -280,7 +276,7 @@ func (h *Hub) spClose(portname string) {
 	}
 }
 
-func (h *Hub) spWrite(arg string) {
+func (h *hub) spWrite(arg string) {
 	// we will get a string of comXX asdf asdf asdf
 	//log.Println("Inside spWrite arg: " + arg)
 	arg = strings.TrimPrefix(arg, " ")
diff --git a/serialport.go b/serialport.go
index 06b16ff41..06ea0aa64 100755
--- a/serialport.go
+++ b/serialport.go
@@ -287,7 +287,7 @@ func (p *serport) writerRaw() {
 	p.OnMessage([]byte(msgstr))
 }
 
-func (h *Hub) spHandlerOpen(portname string, baud int, buftype string) {
+func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	log.Print("Inside spHandler")
 

From 17ed6b3e7f9ac1701b76c2c494cd97fcaada90ed Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Mon, 31 Mar 2025 17:54:08 +0200
Subject: [PATCH 14/50] revert config changes

---
 config/config-default.ini                     | 10 --
 config/config.go                              | 45 +--------
 config/config_test.go                         | 94 -------------------
 config/testdata/fromdefault/.gitignore        |  1 -
 config/testdata/fromenv/config.ini            |  8 --
 .../.config/ArduinoCreateAgent/config.ini     |  8 --
 config/testdata/fromlegacy/.gitignore         |  2 -
 .../fromxdghome/ArduinoCreateAgent/config.ini | 10 --
 .../.config/ArduinoCreateAgent/config.ini     |  8 --
 .../.config/ArduinoCreateAgent/config.ini     | 10 --
 10 files changed, 1 insertion(+), 195 deletions(-)
 delete mode 100644 config/config-default.ini
 delete mode 100644 config/config_test.go
 delete mode 100644 config/testdata/fromdefault/.gitignore
 delete mode 100644 config/testdata/fromenv/config.ini
 delete mode 100644 config/testdata/fromhome/.config/ArduinoCreateAgent/config.ini
 delete mode 100644 config/testdata/fromlegacy/.gitignore
 delete mode 100644 config/testdata/fromxdghome/ArduinoCreateAgent/config.ini
 delete mode 100644 config/testdata/home/.config/ArduinoCreateAgent/config.ini
 delete mode 100644 config/testdata/noconfig/.config/ArduinoCreateAgent/config.ini

diff --git a/config/config-default.ini b/config/config-default.ini
deleted file mode 100644
index f63377db5..000000000
--- a/config/config-default.ini
+++ /dev/null
@@ -1,10 +0,0 @@
-gc = std  # Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)
-hostname = unknown-hostname  # Override the hostname we get from the OS
-regex = usb|acm|com  # Regular expression to filter serial port list
-v = true  # show debug logging
-appName = CreateAgent/Stable
-updateUrl = https://downloads.arduino.cc/
-origins = https://local.arduino.cc:8000
-#httpProxy = http://your.proxy:port # Proxy server for HTTP requests
-crashreport = false # enable crashreport logging
-autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
\ No newline at end of file
diff --git a/config/config.go b/config/config.go
index 82edecf41..69d29eeee 100644
--- a/config/config.go
+++ b/config/config.go
@@ -108,7 +108,7 @@ func GetDefaultHomeDir() *paths.Path {
 	return paths.New(homeDir)
 }
 
-//go:embed config-default.ini
+//go:embed config.ini
 var configContent []byte
 
 // GenerateConfig function will take a directory path as an input
@@ -142,46 +142,3 @@ func SetInstallCertsIni(filename string, value string) error {
 	}
 	return nil
 }
-
-// GetConfigPath returns the full path to the Arduino Create Agent configuration file.
-// It will check if the config file exists in the default location
-// and if not, it will generate a new one.
-// It will also check if the ARDUINO_CREATE_AGENT_CONFIG environment variable is set,
-// and if so, it will use that path instead of the default one.
-func GetConfigPath() *paths.Path {
-	// Let's handle the config
-	configDir := GetDefaultConfigDir()
-	var configPath *paths.Path
-
-	// see if the env var is defined, if it is take the config from there, this will override the default path
-	if envConfig := os.Getenv("ARDUINO_CREATE_AGENT_CONFIG"); envConfig != "" {
-		configPath = paths.New(envConfig)
-		if configPath.NotExist() {
-			log.Panicf("config from env var %s does not exists", envConfig)
-		}
-		log.Infof("using config from env variable: %s", configPath)
-	} else if defaultConfigPath := configDir.Join("config.ini"); defaultConfigPath.Exist() {
-		// by default take the config from the ~/.arduino-create/config.ini file
-		configPath = defaultConfigPath
-		log.Infof("using config from default: %s", configPath)
-	} else {
-		// Fall back to the old config.ini location
-		src, _ := os.Executable()
-		oldConfigPath := paths.New(src).Parent().Join("config.ini")
-		if oldConfigPath.Exist() {
-			err := oldConfigPath.CopyTo(defaultConfigPath)
-			if err != nil {
-				log.Errorf("cannot copy old %s, to %s, generating new config", oldConfigPath, configPath)
-			} else {
-				configPath = defaultConfigPath
-				log.Infof("copied old %s, to %s", oldConfigPath, configPath)
-			}
-		}
-	}
-	if configPath == nil {
-		configPath = GenerateConfig(configDir)
-	}
-
-	return configPath
-
-}
diff --git a/config/config_test.go b/config/config_test.go
deleted file mode 100644
index 447f3793e..000000000
--- a/config/config_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package config
-
-import (
-	"os"
-	"testing"
-	"time"
-
-	"github.com/arduino/go-paths-helper"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestGetConfigPathFromXDG_CONFIG_HOME(t *testing.T) {
-	// read config from $XDG_CONFIG_HOME/ArduinoCreateAgent/config.ini
-	os.Setenv("XDG_CONFIG_HOME", "./testdata/fromxdghome")
-	defer os.Unsetenv("XDG_CONFIG_HOME")
-	configPath := GetConfigPath()
-	assert.Equal(t, "testdata/fromxdghome/ArduinoCreateAgent/config.ini", configPath.String())
-}
-
-func TestGetConfigPathFromHOME(t *testing.T) {
-	// Test case 2: read config from $HOME/.config/ArduinoCreateAgent/config.ini "
-	os.Setenv("HOME", "./testdata/fromhome")
-	defer os.Unsetenv("HOME")
-	configPath := GetConfigPath()
-	assert.Equal(t, "testdata/fromhome/.config/ArduinoCreateAgent/config.ini", configPath.String())
-
-}
-
-func TestGetConfigPathFromARDUINO_CREATE_AGENT_CONFIG(t *testing.T) {
-	// read config from ARDUINO_CREATE_AGENT_CONFIG/config.ini"
-	os.Setenv("HOME", "./fromhome")
-	os.Setenv("ARDUINO_CREATE_AGENT_CONFIG", "./testdata/fromenv/config.ini")
-	defer os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
-
-	configPath := GetConfigPath()
-	assert.Equal(t, "./testdata/fromenv/config.ini", configPath.String())
-}
-
-func TestGetConfigPathFromLegacyConfig(t *testing.T) {
-	// If no config is found, copy the legacy config to the new location
-	src, err := os.Executable()
-	if err != nil {
-		t.Fatal(err)
-	}
-	legacyConfigPath, err := paths.New(src).Parent().Join("config.ini").Create()
-	if err != nil {
-		t.Fatal(err)
-	}
-	// adding a timestamp to the content to make it unique
-	legacyContent := "hostname = legacy-config-file-" + time.Now().String()
-	n, err := legacyConfigPath.WriteString(legacyContent)
-	if err != nil || n <= 0 {
-		t.Fatalf("Failed to write legacy config file: %v", err)
-	}
-
-	// remove any existing config.ini in the into the location pointed by $HOME
-	err = os.Remove("./testdata/fromlegacy/.config/ArduinoCreateAgent/config.ini")
-	if err != nil && !os.IsNotExist(err) {
-		t.Fatal(err)
-	}
-
-	// Expectation: it copies the "legacy" config.ini into the location pointed by $HOME
-	os.Setenv("HOME", "./testdata/fromlegacy")
-	defer os.Unsetenv("HOME")
-
-	configPath := GetConfigPath()
-	assert.Equal(t, "testdata/fromlegacy/.config/ArduinoCreateAgent/config.ini", configPath.String())
-
-	given, err := paths.New(configPath.String()).ReadFile()
-	assert.Nil(t, err)
-	assert.Equal(t, legacyContent, string(given))
-}
-
-// func TestGetConfigPathCreateDefaultConfig(t *testing.T) {
-// 	os.Setenv("HOME", "./testdata/noconfig")
-// 	os.Unsetenv("ARDUINO_CREATE_AGENT_CONFIG")
-
-// 	// ensure the config.ini does not exist in HOME directory
-// 	os.Remove("./testdata/noconfig/.config/ArduinoCreateAgent/config.ini")
-// 	// ensure the config.ini does not exist in target directory
-// 	os.Remove("./testdata/fromdefault/.config/ArduinoCreateAgent/config.ini")
-
-// 	configPath := GetConfigPath()
-
-// 	assert.Equal(t, "testdata/fromdefault/.config/ArduinoCreateAgent/config.ini", configPath.String())
-
-// 	givenContent, err := paths.New(configPath.String()).ReadFile()
-// 	if err != nil {
-// 		t.Fatal(err)
-// 	}
-
-// 	assert.Equal(t, string(configContent), string(givenContent))
-
-// }
diff --git a/config/testdata/fromdefault/.gitignore b/config/testdata/fromdefault/.gitignore
deleted file mode 100644
index 2fa7ce7c4..000000000
--- a/config/testdata/fromdefault/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-config.ini
diff --git a/config/testdata/fromenv/config.ini b/config/testdata/fromenv/config.ini
deleted file mode 100644
index cfd58d812..000000000
--- a/config/testdata/fromenv/config.ini
+++ /dev/null
@@ -1,8 +0,0 @@
-gc = std
-hostname = this-is-a-config-file-from-home-dir-from-ARDUINO_CREATE_AGENT_CONFIG-env
-regex = usb|acm|com
-v = true
-appName = CreateAgent/Stable
-updateUrl = https://downloads.arduino.cc/
-origins = https://local.arduino.cc:8000, https://local.arduino.cc:8001, https://create-dev.arduino.cc, https://*.sparklyunicorn.cc, https://*.iot-cloud-arduino-cc.pages.dev, https://cloud.oniudra.cc, https://app.oniudra.cc,https://*.iot-cloud-arduino-cc.pages.dev
-crashreport = false
diff --git a/config/testdata/fromhome/.config/ArduinoCreateAgent/config.ini b/config/testdata/fromhome/.config/ArduinoCreateAgent/config.ini
deleted file mode 100644
index a023d62d1..000000000
--- a/config/testdata/fromhome/.config/ArduinoCreateAgent/config.ini
+++ /dev/null
@@ -1,8 +0,0 @@
-gc = std
-hostname = this-is-a-config-file-from-home-dir
-regex = usb|acm|com
-v = true
-appName = config-from-home-dir
-updateUrl = https://downloads.arduino.cc/
-origins = https://local.arduino.cc:8000, https://local.arduino.cc:8001, https://*.iot-cloud-arduino-cc.pages.dev
-crashreport = false
diff --git a/config/testdata/fromlegacy/.gitignore b/config/testdata/fromlegacy/.gitignore
deleted file mode 100644
index f67b7f089..000000000
--- a/config/testdata/fromlegacy/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# ingore because this config file
-config.ini
diff --git a/config/testdata/fromxdghome/ArduinoCreateAgent/config.ini b/config/testdata/fromxdghome/ArduinoCreateAgent/config.ini
deleted file mode 100644
index f63377db5..000000000
--- a/config/testdata/fromxdghome/ArduinoCreateAgent/config.ini
+++ /dev/null
@@ -1,10 +0,0 @@
-gc = std  # Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)
-hostname = unknown-hostname  # Override the hostname we get from the OS
-regex = usb|acm|com  # Regular expression to filter serial port list
-v = true  # show debug logging
-appName = CreateAgent/Stable
-updateUrl = https://downloads.arduino.cc/
-origins = https://local.arduino.cc:8000
-#httpProxy = http://your.proxy:port # Proxy server for HTTP requests
-crashreport = false # enable crashreport logging
-autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
\ No newline at end of file
diff --git a/config/testdata/home/.config/ArduinoCreateAgent/config.ini b/config/testdata/home/.config/ArduinoCreateAgent/config.ini
deleted file mode 100644
index a023d62d1..000000000
--- a/config/testdata/home/.config/ArduinoCreateAgent/config.ini
+++ /dev/null
@@ -1,8 +0,0 @@
-gc = std
-hostname = this-is-a-config-file-from-home-dir
-regex = usb|acm|com
-v = true
-appName = config-from-home-dir
-updateUrl = https://downloads.arduino.cc/
-origins = https://local.arduino.cc:8000, https://local.arduino.cc:8001, https://*.iot-cloud-arduino-cc.pages.dev
-crashreport = false
diff --git a/config/testdata/noconfig/.config/ArduinoCreateAgent/config.ini b/config/testdata/noconfig/.config/ArduinoCreateAgent/config.ini
deleted file mode 100644
index f63377db5..000000000
--- a/config/testdata/noconfig/.config/ArduinoCreateAgent/config.ini
+++ /dev/null
@@ -1,10 +0,0 @@
-gc = std  # Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)
-hostname = unknown-hostname  # Override the hostname we get from the OS
-regex = usb|acm|com  # Regular expression to filter serial port list
-v = true  # show debug logging
-appName = CreateAgent/Stable
-updateUrl = https://downloads.arduino.cc/
-origins = https://local.arduino.cc:8000
-#httpProxy = http://your.proxy:port # Proxy server for HTTP requests
-crashreport = false # enable crashreport logging
-autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
\ No newline at end of file

From 96f0f5134a69edbd30135ddbacbbe2c2cc394432 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Mon, 31 Mar 2025 18:09:50 +0200
Subject: [PATCH 15/50] refactor: update logger handling and improve config
 path management

---
 hub.go  |  2 +-
 main.go | 67 ++++++++++++++++++++++++++++++++++++++-------------------
 2 files changed, 46 insertions(+), 23 deletions(-)

diff --git a/hub.go b/hub.go
index d812d6b80..150c6d943 100755
--- a/hub.go
+++ b/hub.go
@@ -270,7 +270,7 @@ func (h *hub) checkCmd(m []byte) {
 func logAction(sl string) {
 	if strings.HasPrefix(sl, "log on") {
 		*logDump = "on"
-		//TODO: pass the loggerSw in the constructor and enable again the log e
+		// FIXME: pass the loggerSw in the constructor and enable again the log e
 		// multiWriter := io.MultiWriter(&loggerWs, os.Stderr)
 		// log.SetOutput(multiWriter)
 	} else if strings.HasPrefix(sl, "log off") {
diff --git a/main.go b/main.go
index a325c2348..1c7d85de4 100755
--- a/main.go
+++ b/main.go
@@ -22,7 +22,6 @@ import (
 	_ "embed"
 	"encoding/json"
 	"flag"
-	"fmt"
 	"html/template"
 	"io"
 	"os"
@@ -103,11 +102,12 @@ var homeTemplateHTML string
 
 // global clients
 var (
-	Tools *tools.Tools
-	Index *index.Resource
+	Tools   *tools.Tools
+	Systray systray.Systray
+	Index   *index.Resource
 )
 
-// TODO: enable it
+// FIXME; the loggerWS is useind in the multiwrite in the hub
 // type logWriter struct{}
 
 // func (u *logWriter) Write(p []byte) (n int, err error) {
@@ -115,6 +115,8 @@ var (
 // 	return len(p), nil
 // }
 
+// var loggerWs logWriter
+
 func homeHandler(c *gin.Context) {
 	homeTemplate.Execute(c.Writer, c.Request.Host)
 }
@@ -140,13 +142,9 @@ func main() {
 	// Check if certificates made with Agent <=1.2.7 needs to be moved over the new location
 	cert.MigrateCertificatesGeneratedWithOldAgentVersions(config.GetCertificatesDir())
 
-	configPath := config.GetConfigPath()
-	fmt.Println("configPath: ", configPath)
-
 	// SetupSystray is the main thread
 	configDir := config.GetDefaultConfigDir()
-
-	stray := &systray.Systray{
+	stray := systray.Systray{
 		Hibernate: *hibernate,
 		Version:   version + "-" + commit,
 		DebugURL: func() string {
@@ -155,29 +153,24 @@ func main() {
 		AdditionalConfig: *additionalConfig,
 		ConfigDir:        configDir,
 	}
-	stray.SetCurrentConfigFile(configPath)
 
 	// Launch main loop in a goroutine
-	go loop(stray, configPath)
+	go loop(&stray)
 
 	if src, err := os.Executable(); err != nil {
 		panic(err)
 	} else if restartPath := updater.Start(src); restartPath != "" {
-		stray.RestartWith(restartPath)
+		Systray.RestartWith(restartPath)
 	} else {
-		stray.Start()
+		Systray.Start()
 	}
 }
 
-func loop(stray *systray.Systray, configPath *paths.Path) {
+func loop(stray *systray.Systray) {
 	if *hibernate {
 		return
 	}
 
-	if configPath == nil {
-		log.Panic("configPath is nil")
-	}
-
 	log.SetLevel(log.InfoLevel)
 	log.SetOutput(os.Stdout)
 
@@ -190,12 +183,8 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 		os.Exit(0)
 	}
 
-	// serialPorts contains the ports attached to the machine
 	serialPorts := newSerialPortList()
 	serialHub := newSerialHub()
-
-	// var loggerWs logWriter
-
 	hub := newHub(serialHub, serialPorts)
 
 	logger := func(msg string) {
@@ -204,6 +193,39 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 		hub.broadcastSys <- mapB
 	}
 
+	// Let's handle the config
+	configDir := config.GetDefaultConfigDir()
+	var configPath *paths.Path
+
+	// see if the env var is defined, if it is take the config from there, this will override the default path
+	if envConfig := os.Getenv("ARDUINO_CREATE_AGENT_CONFIG"); envConfig != "" {
+		configPath = paths.New(envConfig)
+		if configPath.NotExist() {
+			log.Panicf("config from env var %s does not exists", envConfig)
+		}
+		log.Infof("using config from env variable: %s", configPath)
+	} else if defaultConfigPath := configDir.Join("config.ini"); defaultConfigPath.Exist() {
+		// by default take the config from the ~/.arduino-create/config.ini file
+		configPath = defaultConfigPath
+		log.Infof("using config from default: %s", configPath)
+	} else {
+		// Fall back to the old config.ini location
+		src, _ := os.Executable()
+		oldConfigPath := paths.New(src).Parent().Join("config.ini")
+		if oldConfigPath.Exist() {
+			err := oldConfigPath.CopyTo(defaultConfigPath)
+			if err != nil {
+				log.Errorf("cannot copy old %s, to %s, generating new config", oldConfigPath, configPath)
+			} else {
+				configPath = defaultConfigPath
+				log.Infof("copied old %s, to %s", oldConfigPath, configPath)
+			}
+		}
+	}
+	if configPath == nil {
+		configPath = config.GenerateConfig(configDir)
+	}
+
 	// if the default browser is Safari, prompt the user to install HTTPS certificates
 	// and eventually install them
 	if runtime.GOOS == "darwin" {
@@ -241,6 +263,7 @@ func loop(stray *systray.Systray, configPath *paths.Path) {
 	if err != nil {
 		log.Panicf("cannot parse arguments: %s", err)
 	}
+	Systray.SetCurrentConfigFile(configPath)
 
 	// Parse additional ini config if defined
 	if len(*additionalConfig) > 0 {

From 36af1edac648aa0fe6c3a444ef186f7dffe76b59 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Mon, 31 Mar 2025 18:46:38 +0200
Subject: [PATCH 16/50] remove globas tools and index

---
 config/config.ini      |  2 +-
 conn.go                |  5 +++--
 hub.go                 | 13 +++++++++--
 main.go                | 50 +++++++++++++++---------------------------
 main_test.go           | 34 ++++++++++++++++++++--------
 serial.go              | 12 ++++++----
 tools/download.go      | 14 ++++++------
 tools/download_test.go |  8 +++----
 tools/tools.go         |  4 +---
 9 files changed, 78 insertions(+), 64 deletions(-)

diff --git a/config/config.ini b/config/config.ini
index f63377db5..65de6012f 100644
--- a/config/config.ini
+++ b/config/config.ini
@@ -7,4 +7,4 @@ updateUrl = https://downloads.arduino.cc/
 origins = https://local.arduino.cc:8000
 #httpProxy = http://your.proxy:port # Proxy server for HTTP requests
 crashreport = false # enable crashreport logging
-autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
\ No newline at end of file
+autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
diff --git a/conn.go b/conn.go
index 584e1328f..d99f24edd 100644
--- a/conn.go
+++ b/conn.go
@@ -27,6 +27,7 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/arduino/arduino-create-agent/tools"
 	"github.com/arduino/arduino-create-agent/upload"
 	"github.com/arduino/arduino-create-agent/utilities"
 	"github.com/gin-gonic/gin"
@@ -80,7 +81,7 @@ type Upload struct {
 
 var uploadStatusStr = "ProgrammerStatus"
 
-func uploadHandler(h *hub, pubKey *rsa.PublicKey) func(*gin.Context) {
+func uploadHandler(h *hub, pubKey *rsa.PublicKey, tools *tools.Tools) func(*gin.Context) {
 	return func(c *gin.Context) {
 		data := new(Upload)
 		if err := c.BindJSON(data); err != nil {
@@ -162,7 +163,7 @@ func uploadHandler(h *hub, pubKey *rsa.PublicKey) func(*gin.Context) {
 
 		go func() {
 			// Resolve commandline
-			commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, Tools)
+			commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, tools)
 			if err != nil {
 				send(h, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
 				return
diff --git a/hub.go b/hub.go
index 150c6d943..825cbb101 100755
--- a/hub.go
+++ b/hub.go
@@ -25,6 +25,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/arduino/arduino-create-agent/tools"
 	"github.com/arduino/arduino-create-agent/upload"
 	log "github.com/sirupsen/logrus"
 )
@@ -50,9 +51,11 @@ type hub struct {
 	serialHub *serialhub
 
 	serialPortList *serialPortList
+
+	tools *tools.Tools
 }
 
-func newHub(serialhub *serialhub, serialList *serialPortList) *hub {
+func newHub(serialhub *serialhub, serialList *serialPortList, tools *tools.Tools) *hub {
 	hub := &hub{
 		broadcast:      make(chan []byte, 1000),
 		broadcastSys:   make(chan []byte, 1000),
@@ -61,6 +64,7 @@ func newHub(serialhub *serialhub, serialList *serialPortList) *hub {
 		connections:    make(map[*connection]bool),
 		serialHub:      serialhub,
 		serialPortList: serialList,
+		tools:          tools,
 	}
 
 	hub.serialHub.OnRegister = func(port *serport) {
@@ -235,7 +239,12 @@ func (h *hub) checkCmd(m []byte) {
 				behaviour = args[4]
 			}
 
-			err := Tools.Download(pack, tool, toolVersion, behaviour)
+			reportPendingProgress := func(msg string) {
+				mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg}
+				mapB, _ := json.Marshal(mapD)
+				h.broadcastSys <- mapB
+			}
+			err := h.tools.Download(pack, tool, toolVersion, behaviour, reportPendingProgress)
 			if err != nil {
 				mapD := map[string]string{"DownloadStatus": "Error", "Msg": err.Error()}
 				mapB, _ := json.Marshal(mapD)
diff --git a/main.go b/main.go
index 1c7d85de4..fc1f2007b 100755
--- a/main.go
+++ b/main.go
@@ -20,7 +20,6 @@ package main
 
 import (
 	_ "embed"
-	"encoding/json"
 	"flag"
 	"html/template"
 	"io"
@@ -100,13 +99,6 @@ var homeTemplate = template.Must(template.New("home").Parse(homeTemplateHTML))
 //go:embed home.html
 var homeTemplateHTML string
 
-// global clients
-var (
-	Tools   *tools.Tools
-	Systray systray.Systray
-	Index   *index.Resource
-)
-
 // FIXME; the loggerWS is useind in the multiwrite in the hub
 // type logWriter struct{}
 
@@ -160,9 +152,9 @@ func main() {
 	if src, err := os.Executable(); err != nil {
 		panic(err)
 	} else if restartPath := updater.Start(src); restartPath != "" {
-		Systray.RestartWith(restartPath)
+		stray.RestartWith(restartPath)
 	} else {
-		Systray.Start()
+		stray.Start()
 	}
 }
 
@@ -183,15 +175,21 @@ func loop(stray *systray.Systray) {
 		os.Exit(0)
 	}
 
-	serialPorts := newSerialPortList()
+	// Instantiate Index and Tools
+	index := index.Init(*indexURL, config.GetDataDir())
+	if signatureKey == nil || len(*signatureKey) == 0 {
+		log.Panicf("signature public key should be set")
+	}
+	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
+	if err != nil {
+		log.Panicf("cannot parse signature key '%s'. %s", *signatureKey, err)
+	}
+	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
+
+	serialPorts := newSerialPortList(tools)
 	serialHub := newSerialHub()
-	hub := newHub(serialHub, serialPorts)
 
-	logger := func(msg string) {
-		mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg}
-		mapB, _ := json.Marshal(mapD)
-		hub.broadcastSys <- mapB
-	}
+	hub := newHub(serialHub, serialPorts, tools)
 
 	// Let's handle the config
 	configDir := config.GetDefaultConfigDir()
@@ -263,7 +261,7 @@ func loop(stray *systray.Systray) {
 	if err != nil {
 		log.Panicf("cannot parse arguments: %s", err)
 	}
-	Systray.SetCurrentConfigFile(configPath)
+	stray.SetCurrentConfigFile(configPath)
 
 	// Parse additional ini config if defined
 	if len(*additionalConfig) > 0 {
@@ -283,18 +281,6 @@ func loop(stray *systray.Systray) {
 		}
 	}
 
-	if signatureKey == nil || len(*signatureKey) == 0 {
-		log.Panicf("signature public key should be set")
-	}
-	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
-	if err != nil {
-		log.Panicf("cannot parse signature key '%s'. %s", *signatureKey, err)
-	}
-
-	// Instantiate Index and Tools
-	Index = index.Init(*indexURL, config.GetDataDir())
-	Tools = tools.New(config.GetDataDir(), Index, logger, signaturePubKey)
-
 	// see if we are supposed to wait 5 seconds
 	if *isLaunchSelf {
 		launchSelfLater()
@@ -467,7 +453,7 @@ func loop(stray *systray.Systray) {
 	r.LoadHTMLFiles("templates/nofirefox.html")
 
 	r.GET("/", homeHandler)
-	r.POST("/upload", uploadHandler(hub, signaturePubKey))
+	r.POST("/upload", uploadHandler(hub, signaturePubKey, tools))
 	r.GET("/socket.io/", socketHandler)
 	r.POST("/socket.io/", socketHandler)
 	r.Handle("WS", "/socket.io/", socketHandler)
@@ -477,7 +463,7 @@ func loop(stray *systray.Systray) {
 	r.POST("/update", updateHandler(stray))
 
 	// Mount goa handlers
-	goa := v2.Server(config.GetDataDir().String(), Index, signaturePubKey)
+	goa := v2.Server(config.GetDataDir().String(), index, signaturePubKey)
 	r.Any("/v2/*path", gin.WrapH(goa))
 
 	go func() {
diff --git a/main_test.go b/main_test.go
index d30498669..8adc1443b 100644
--- a/main_test.go
+++ b/main_test.go
@@ -29,9 +29,10 @@ import (
 	"testing"
 
 	"github.com/arduino/arduino-create-agent/config"
-	"github.com/arduino/arduino-create-agent/gen/tools"
+	genTools "github.com/arduino/arduino-create-agent/gen/tools"
 	"github.com/arduino/arduino-create-agent/globals"
 	"github.com/arduino/arduino-create-agent/index"
+	"github.com/arduino/arduino-create-agent/tools"
 	"github.com/arduino/arduino-create-agent/upload"
 	"github.com/arduino/arduino-create-agent/utilities"
 	v2 "github.com/arduino/arduino-create-agent/v2"
@@ -56,7 +57,15 @@ func TestValidSignatureKey(t *testing.T) {
 
 func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 	r := gin.New()
-	r.POST("/", uploadHandler(newHub(newSerialHub(), newSerialPortList()), utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
+
+	index := index.Init(*indexURL, config.GetDataDir())
+	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
+	require.NoError(t, err)
+	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
+	hub := newHub(newSerialHub(), newSerialPortList(tools), tools)
+	pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))
+
+	r.POST("/", uploadHandler(hub, pubkey, tools))
 	ts := httptest.NewServer(r)
 
 	uploadEvilFileName := Upload{
@@ -93,7 +102,14 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) {
 	r := gin.New()
 
-	r.POST("/", uploadHandler(newHub(newSerialHub(), newSerialPortList()), utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))))
+	index := index.Init(*indexURL, config.GetDataDir())
+	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
+	require.NoError(t, err)
+	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
+	hub := newHub(newSerialHub(), newSerialPortList(tools), tools)
+	pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))
+
+	r.POST("/", uploadHandler(hub, pubkey, tools))
 	ts := httptest.NewServer(r)
 	defer ts.Close()
 
@@ -127,7 +143,7 @@ func TestInstallToolV2(t *testing.T) {
 	ts := httptest.NewServer(r)
 
 	type test struct {
-		request      tools.ToolPayload
+		request      genTools.ToolPayload
 		responseCode int
 		responseBody string
 	}
@@ -135,7 +151,7 @@ func TestInstallToolV2(t *testing.T) {
 	bossacURL := "http://downloads.arduino.cc/tools/bossac-1.7.0-arduino3-linux64.tar.gz"
 	bossacChecksum := "SHA-256:1ae54999c1f97234a5c603eb99ad39313b11746a4ca517269a9285afa05f9100"
 	bossacSignature := "382898a97b5a86edd74208f10107d2fecbf7059ffe9cc856e045266fb4db4e98802728a0859cfdcda1c0b9075ec01e42dbea1f430b813530d5a6ae1766dfbba64c3e689b59758062dc2ab2e32b2a3491dc2b9a80b9cda4ae514fbe0ec5af210111b6896976053ab76bac55bcecfcececa68adfa3299e3cde6b7f117b3552a7d80ca419374bb497e3c3f12b640cf5b20875416b45e662fc6150b99b178f8e41d6982b4c0a255925ea39773683f9aa9201dc5768b6fc857c87ff602b6a93452a541b8ec10ca07f166e61a9e9d91f0a6090bd2038ed4427af6251039fb9fe8eb62ec30d7b0f3df38bc9de7204dec478fb86f8eb3f71543710790ee169dce039d3e0"
-	bossacInstallURLOK := tools.ToolPayload{
+	bossacInstallURLOK := genTools.ToolPayload{
 		Name:      "bossac",
 		Version:   "1.7.0-arduino3",
 		Packager:  "arduino",
@@ -147,7 +163,7 @@ func TestInstallToolV2(t *testing.T) {
 	esptoolURL := "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-3/x86_64-linux-gnu.esptool-f80ae31.tar.gz"
 	esptoolChecksum := "SHA-256:bded1dca953377838b6086a9bcd40a1dc5286ba5f69f2372c22a1d1819baad24"
 	esptoolSignature := "852b58871419ce5e5633ecfaa72c0f0fa890ceb51164b362b8133bc0e3e003a21cec48935b8cdc078f4031219cbf17fb7edd9d7c9ca8ed85492911c9ca6353c9aa4691eb91fda99563a6bd49aeca0d9981fb05ec76e45c6024f8a6822862ad1e34ddc652fbbf4fa909887a255d4f087398ec386577efcec523c21203be3d10fc9e9b0f990a7536875a77dc2bc5cbffea7734b62238e31719111b718bacccebffc9be689545540e81d23b81caa66214376f58a0d6a45cf7efc5d3af62ab932b371628162fffe403906f41d5534921e5be081c5ac2ecc9db5caec03a105cc44b00ce19a95ad079843501eb8182e0717ce327867380c0e39d2b48698547fc1d0d66"
-	esptoolInstallURLOK := tools.ToolPayload{
+	esptoolInstallURLOK := genTools.ToolPayload{
 		Name:      "esptool",
 		Version:   "2.5.0-3-20ed2b9",
 		Packager:  "esp8266",
@@ -157,7 +173,7 @@ func TestInstallToolV2(t *testing.T) {
 	}
 
 	wrongSignature := "wr0ngs1gn4tur3"
-	bossacInstallWrongSig := tools.ToolPayload{
+	bossacInstallWrongSig := genTools.ToolPayload{
 		Name:      "bossac",
 		Version:   "1.7.0-arduino3",
 		Packager:  "arduino",
@@ -167,7 +183,7 @@ func TestInstallToolV2(t *testing.T) {
 	}
 
 	wrongChecksum := "wr0ngch3cksum"
-	bossacInstallWrongCheck := tools.ToolPayload{
+	bossacInstallWrongCheck := genTools.ToolPayload{
 		Name:      "bossac",
 		Version:   "1.7.0-arduino3",
 		Packager:  "arduino",
@@ -176,7 +192,7 @@ func TestInstallToolV2(t *testing.T) {
 		Signature: &bossacSignature,
 	}
 
-	bossacInstallNoURL := tools.ToolPayload{
+	bossacInstallNoURL := genTools.ToolPayload{
 		Name:     "bossac",
 		Version:  "1.7.0-arduino3",
 		Packager: "arduino",
diff --git a/serial.go b/serial.go
index c3f1b38bf..7fe9ae1f9 100755
--- a/serial.go
+++ b/serial.go
@@ -25,6 +25,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/arduino/arduino-create-agent/tools"
 	discovery "github.com/arduino/pluggable-discovery-protocol-handler/v2"
 	"github.com/sirupsen/logrus"
 )
@@ -45,6 +46,8 @@ func newSerialHub() *serialhub {
 }
 
 type serialPortList struct {
+	tools *tools.Tools
+
 	Ports     []*SpPortItem
 	portsLock sync.Mutex
 
@@ -52,8 +55,8 @@ type serialPortList struct {
 	OnErr  func(string) `json:"-"`
 }
 
-func newSerialPortList() *serialPortList {
-	return &serialPortList{}
+func newSerialPortList(tools *tools.Tools) *serialPortList {
+	return &serialPortList{tools: tools}
 }
 
 // SpPortItem is the serial port item
@@ -130,11 +133,12 @@ func (sp *serialPortList) Run() {
 
 func (sp *serialPortList) runSerialDiscovery() {
 	// First ensure that all the discoveries are available
-	if err := Tools.Download("builtin", "serial-discovery", "latest", "keep"); err != nil {
+	noOpProgress := func(msg string) {}
+	if err := sp.tools.Download("builtin", "serial-discovery", "latest", "keep", noOpProgress); err != nil {
 		logrus.Errorf("Error downloading serial-discovery: %s", err)
 		panic(err)
 	}
-	sd, err := Tools.GetLocation("serial-discovery")
+	sd, err := sp.tools.GetLocation("serial-discovery")
 	if err != nil {
 		logrus.Errorf("Error downloading serial-discovery: %s", err)
 		panic(err)
diff --git a/tools/download.go b/tools/download.go
index 8c4a37a6c..da7df6806 100644
--- a/tools/download.go
+++ b/tools/download.go
@@ -42,7 +42,7 @@ import (
 // If version is not "latest" and behaviour is "replace", it will download the
 // version again. If instead behaviour is "keep" it will not download the version
 // if it already exists.
-func (t *Tools) Download(pack, name, version, behaviour string) error {
+func (t *Tools) Download(pack, name, version, behaviour string, report func(msg string)) error {
 
 	t.tools.SetBehaviour(behaviour)
 	_, err := t.tools.Install(context.Background(), &tools.ToolPayload{Name: name, Version: version, Packager: pack})
@@ -58,16 +58,16 @@ func (t *Tools) Download(pack, name, version, behaviour string) error {
 
 	// if the tool contains a post_install script, run it: it means it is a tool that needs to install drivers
 	// AFAIK this is only the case for the windows-driver tool
-	err = t.installDrivers(safePath)
+	err = t.installDrivers(safePath, report)
 	if err != nil {
 		return err
 	}
 
 	// Ensure that the files are executable
-	t.logger("Ensure that the files are executable")
+	report("Ensure that the files are executable")
 
 	// Update the tool map
-	t.logger("Updating map with location " + safePath)
+	report("Updating map with location " + safePath)
 
 	t.setMapValue(name, safePath)
 	t.setMapValue(name+"-"+version, safePath)
@@ -75,7 +75,7 @@ func (t *Tools) Download(pack, name, version, behaviour string) error {
 	return nil
 }
 
-func (t *Tools) installDrivers(location string) error {
+func (t *Tools) installDrivers(location string, report func(msg string)) error {
 	OkPressed := 6
 	extension := ".bat"
 	// add .\ to force locality
@@ -86,11 +86,11 @@ func (t *Tools) installDrivers(location string) error {
 		preamble = "./"
 	}
 	if _, err := os.Stat(filepath.Join(location, "post_install"+extension)); err == nil {
-		t.logger("Installing drivers")
+		report("Installing drivers")
 		ok := MessageBox("Installing drivers", "We are about to install some drivers needed to use Arduino/Genuino boards\nDo you want to continue?")
 		if ok == OkPressed {
 			os.Chdir(location)
-			t.logger(preamble + "post_install" + extension)
+			report(preamble + "post_install" + extension)
 			oscmd := exec.Command(preamble + "post_install" + extension)
 			if runtime.GOOS != "linux" {
 				// spawning a shell could be the only way to let the user type his password
diff --git a/tools/download_test.go b/tools/download_test.go
index 96a105fd7..b99f10773 100644
--- a/tools/download_test.go
+++ b/tools/download_test.go
@@ -130,12 +130,12 @@ func TestDownload(t *testing.T) {
 		IndexFile:   *paths.New("testdata", "test_tool_index.json"),
 		LastRefresh: time.Now(),
 	}
-	testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))
+	testTools := New(tempDirPath, &testIndex, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))
 
 	for _, tc := range testCases {
 		t.Run(tc.name+"-"+tc.version, func(t *testing.T) {
 			// Download the tool
-			err := testTools.Download("arduino-test", tc.name, tc.version, "replace")
+			err := testTools.Download("arduino-test", tc.name, tc.version, "replace", func(msg string) { t.Log(msg) })
 			require.NoError(t, err)
 
 			// Check that the tool has been downloaded
@@ -177,8 +177,8 @@ func TestCorruptedInstalled(t *testing.T) {
 	defer fileJSON.Close()
 	_, err = fileJSON.Write([]byte("Hello"))
 	require.NoError(t, err)
-	testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))
+	testTools := New(tempDirPath, &testIndex, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))
 	// Download the tool
-	err = testTools.Download("arduino-test", "avrdude", "6.3.0-arduino17", "keep")
+	err = testTools.Download("arduino-test", "avrdude", "6.3.0-arduino17", "keep", func(msg string) { t.Log(msg) })
 	require.NoError(t, err)
 }
diff --git a/tools/tools.go b/tools/tools.go
index f371126b5..0ad95763a 100644
--- a/tools/tools.go
+++ b/tools/tools.go
@@ -46,7 +46,6 @@ import (
 type Tools struct {
 	directory *paths.Path
 	index     *index.Resource
-	logger    func(msg string)
 	installed map[string]string
 	mutex     sync.RWMutex
 	tools     *pkgs.Tools
@@ -56,11 +55,10 @@ type Tools struct {
 // The New functions accept the directory to use to host the tools,
 // an index (used to download the tools),
 // and a logger to log the operations
-func New(directory *paths.Path, index *index.Resource, logger func(msg string), signPubKey *rsa.PublicKey) *Tools {
+func New(directory *paths.Path, index *index.Resource, signPubKey *rsa.PublicKey) *Tools {
 	t := &Tools{
 		directory: directory,
 		index:     index,
-		logger:    logger,
 		installed: map[string]string{},
 		mutex:     sync.RWMutex{},
 		tools:     pkgs.New(index, directory.String(), "replace", signPubKey),

From f939e32abc6ff665322b8cdc45ec0381543cb631 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 1 Apr 2025 14:52:44 +0200
Subject: [PATCH 17/50] refactor: enhance logging mechanism by implementing
 logWriter and updating logAction method

---
 hub.go | 25 ++++++++++++++++++++-----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/hub.go b/hub.go
index 825cbb101..2251043a3 100755
--- a/hub.go
+++ b/hub.go
@@ -19,6 +19,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"html"
+	"io"
 	"os"
 	"runtime"
 	"runtime/debug"
@@ -256,7 +257,7 @@ func (h *hub) checkCmd(m []byte) {
 			}
 		}()
 	} else if strings.HasPrefix(sl, "log") {
-		go logAction(sl)
+		go h.logAction(sl)
 	} else if strings.HasPrefix(sl, "restart") {
 		log.Println("Received restart from the daemon. Why? Boh")
 		// TODO enable them
@@ -276,12 +277,26 @@ func (h *hub) checkCmd(m []byte) {
 	}
 }
 
-func logAction(sl string) {
+type logWriter struct {
+	onWrite func([]byte)
+}
+
+func (u *logWriter) Write(p []byte) (n int, err error) {
+	u.onWrite(p)
+	return len(p), nil
+}
+
+func (h *hub) logAction(sl string) {
 	if strings.HasPrefix(sl, "log on") {
 		*logDump = "on"
-		// FIXME: pass the loggerSw in the constructor and enable again the log e
-		// multiWriter := io.MultiWriter(&loggerWs, os.Stderr)
-		// log.SetOutput(multiWriter)
+
+		logWriter := logWriter{}
+		logWriter.onWrite = func(p []byte) {
+			h.broadcastSys <- p
+		}
+
+		multiWriter := io.MultiWriter(&logWriter, os.Stderr)
+		log.SetOutput(multiWriter)
 	} else if strings.HasPrefix(sl, "log off") {
 		*logDump = "off"
 		log.SetOutput(os.Stderr)

From e22281f8cf08a075727cef60147b70809b0847a6 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 1 Apr 2025 14:53:02 +0200
Subject: [PATCH 18/50] remove log websocket

---
 main.go | 10 ----------
 1 file changed, 10 deletions(-)

diff --git a/main.go b/main.go
index fc1f2007b..819816efa 100755
--- a/main.go
+++ b/main.go
@@ -99,16 +99,6 @@ var homeTemplate = template.Must(template.New("home").Parse(homeTemplateHTML))
 //go:embed home.html
 var homeTemplateHTML string
 
-// FIXME; the loggerWS is useind in the multiwrite in the hub
-// type logWriter struct{}
-
-// func (u *logWriter) Write(p []byte) (n int, err error) {
-// 	h.broadcastSys <- p
-// 	return len(p), nil
-// }
-
-// var loggerWs logWriter
-
 func homeHandler(c *gin.Context) {
 	homeTemplate.Execute(c.Writer, c.Request.Host)
 }

From 3bbf371bccf972fc46aa079d4b197aa0174dd437 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 1 Apr 2025 15:03:19 +0200
Subject: [PATCH 19/50] remove unused global clients comment from hub struct

---
 hub.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/hub.go b/hub.go
index 2251043a3..2a36b3249 100755
--- a/hub.go
+++ b/hub.go
@@ -47,7 +47,6 @@ type hub struct {
 	// Unregister requests from connections.
 	unregister chan *connection
 
-	//TODO globals clients
 	// Serial hub to communicate with serial ports
 	serialHub *serialhub
 

From 0773f99774382a8942316fac17ce49818a17866b Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 1 Apr 2025 15:27:52 +0200
Subject: [PATCH 20/50] refactor: integrate systray into hub structure and
 update newHub function

---
 hub.go  | 11 +++++++----
 main.go |  2 +-
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/hub.go b/hub.go
index 2a36b3249..8aa6d5c38 100755
--- a/hub.go
+++ b/hub.go
@@ -26,6 +26,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/arduino/arduino-create-agent/systray"
 	"github.com/arduino/arduino-create-agent/tools"
 	"github.com/arduino/arduino-create-agent/upload"
 	log "github.com/sirupsen/logrus"
@@ -53,9 +54,11 @@ type hub struct {
 	serialPortList *serialPortList
 
 	tools *tools.Tools
+
+	systray *systray.Systray
 }
 
-func newHub(serialhub *serialhub, serialList *serialPortList, tools *tools.Tools) *hub {
+func newHub(serialhub *serialhub, serialList *serialPortList, tools *tools.Tools, systray *systray.Systray) *hub {
 	hub := &hub{
 		broadcast:      make(chan []byte, 1000),
 		broadcastSys:   make(chan []byte, 1000),
@@ -65,6 +68,7 @@ func newHub(serialhub *serialhub, serialList *serialPortList, tools *tools.Tools
 		serialHub:      serialhub,
 		serialPortList: serialList,
 		tools:          tools,
+		systray:        systray,
 	}
 
 	hub.serialHub.OnRegister = func(port *serport) {
@@ -259,10 +263,9 @@ func (h *hub) checkCmd(m []byte) {
 		go h.logAction(sl)
 	} else if strings.HasPrefix(sl, "restart") {
 		log.Println("Received restart from the daemon. Why? Boh")
-		// TODO enable them
-		// Systray.Restart()
+		h.systray.Restart()
 	} else if strings.HasPrefix(sl, "exit") {
-		// Systray.Quit()
+		h.systray.Quit()
 	} else if strings.HasPrefix(sl, "memstats") {
 		h.memoryStats()
 	} else if strings.HasPrefix(sl, "gc") {
diff --git a/main.go b/main.go
index 819816efa..9a1db15af 100755
--- a/main.go
+++ b/main.go
@@ -179,7 +179,7 @@ func loop(stray *systray.Systray) {
 	serialPorts := newSerialPortList(tools)
 	serialHub := newSerialHub()
 
-	hub := newHub(serialHub, serialPorts, tools)
+	hub := newHub(serialHub, serialPorts, tools, stray)
 
 	// Let's handle the config
 	configDir := config.GetDefaultConfigDir()

From 9c2ca547816c73b486f728a1b50e9fa58fd98e77 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 1 Apr 2025 15:30:56 +0200
Subject: [PATCH 21/50] pass tests

---
 main_test.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/main_test.go b/main_test.go
index 8adc1443b..a24c29c6d 100644
--- a/main_test.go
+++ b/main_test.go
@@ -32,6 +32,7 @@ import (
 	genTools "github.com/arduino/arduino-create-agent/gen/tools"
 	"github.com/arduino/arduino-create-agent/globals"
 	"github.com/arduino/arduino-create-agent/index"
+	"github.com/arduino/arduino-create-agent/systray"
 	"github.com/arduino/arduino-create-agent/tools"
 	"github.com/arduino/arduino-create-agent/upload"
 	"github.com/arduino/arduino-create-agent/utilities"
@@ -62,7 +63,7 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
 	require.NoError(t, err)
 	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
-	hub := newHub(newSerialHub(), newSerialPortList(tools), tools)
+	hub := newHub(newSerialHub(), newSerialPortList(tools), tools, &systray.Systray{})
 	pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))
 
 	r.POST("/", uploadHandler(hub, pubkey, tools))
@@ -106,7 +107,7 @@ func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) {
 	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
 	require.NoError(t, err)
 	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
-	hub := newHub(newSerialHub(), newSerialPortList(tools), tools)
+	hub := newHub(newSerialHub(), newSerialPortList(tools), tools, &systray.Systray{})
 	pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))
 
 	r.POST("/", uploadHandler(hub, pubkey, tools))

From fc511946ece9424d0025f2db0b06cec6ca4a0e14 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Tue, 1 Apr 2025 15:56:14 +0200
Subject: [PATCH 22/50] chore: add comment regarding potential removal of
 sysStray dependencies

---
 hub.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/hub.go b/hub.go
index 8aa6d5c38..6e320e3cc 100755
--- a/hub.go
+++ b/hub.go
@@ -262,6 +262,7 @@ func (h *hub) checkCmd(m []byte) {
 	} else if strings.HasPrefix(sl, "log") {
 		go h.logAction(sl)
 	} else if strings.HasPrefix(sl, "restart") {
+		// potentially, the sysStray dependencies can be removed  https://github.com/arduino/arduino-create-agent/issues/1013
 		log.Println("Received restart from the daemon. Why? Boh")
 		h.systray.Restart()
 	} else if strings.HasPrefix(sl, "exit") {

From 8106c84186a1d77783668afe98c0a0456500b2fd Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 14:30:55 +0200
Subject: [PATCH 23/50] feat: move serial port into new file

---
 serial.go         | 187 -------------------------------------------
 serialport.go     |  16 +---
 serialportlist.go | 197 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 200 insertions(+), 200 deletions(-)
 create mode 100644 serialportlist.go

diff --git a/serial.go b/serial.go
index 7fe9ae1f9..d98a6cb73 100755
--- a/serial.go
+++ b/serial.go
@@ -19,7 +19,6 @@ package main
 
 import (
 	"encoding/json"
-	"fmt"
 	"slices"
 	"strings"
 	"sync"
@@ -45,34 +44,6 @@ func newSerialHub() *serialhub {
 	}
 }
 
-type serialPortList struct {
-	tools *tools.Tools
-
-	Ports     []*SpPortItem
-	portsLock sync.Mutex
-
-	OnList func([]byte) `json:"-"`
-	OnErr  func(string) `json:"-"`
-}
-
-func newSerialPortList(tools *tools.Tools) *serialPortList {
-	return &serialPortList{tools: tools}
-}
-
-// SpPortItem is the serial port item
-type SpPortItem struct {
-	Name            string
-	SerialNumber    string
-	DeviceClass     string
-	IsOpen          bool
-	IsPrimary       bool
-	Baud            int
-	BufferAlgorithm string
-	Ver             string
-	VendorID        string
-	ProductID       string
-}
-
 // Register serial ports from the connections.
 func (sh *serialhub) Register(port *serport) {
 	sh.mu.Lock()
@@ -107,164 +78,6 @@ func (sh *serialhub) FindPortByName(portname string) (*serport, bool) {
 	return nil, false
 }
 
-// List broadcasts a Json representation of the ports found
-func (sp *serialPortList) List() {
-	sp.portsLock.Lock()
-	ls, err := json.MarshalIndent(sp, "", "\t")
-	sp.portsLock.Unlock()
-
-	if err != nil {
-		sp.OnErr("Error creating json on port list " + err.Error())
-	} else {
-		sp.OnList(ls)
-	}
-}
-
-// Run is the main loop for port discovery and management
-func (sp *serialPortList) Run() {
-	for retries := 0; retries < 10; retries++ {
-		sp.runSerialDiscovery()
-
-		logrus.Errorf("Serial discovery stopped working, restarting it in 10 seconds...")
-		time.Sleep(10 * time.Second)
-	}
-	logrus.Errorf("Failed restarting serial discovery. Giving up...")
-}
-
-func (sp *serialPortList) runSerialDiscovery() {
-	// First ensure that all the discoveries are available
-	noOpProgress := func(msg string) {}
-	if err := sp.tools.Download("builtin", "serial-discovery", "latest", "keep", noOpProgress); err != nil {
-		logrus.Errorf("Error downloading serial-discovery: %s", err)
-		panic(err)
-	}
-	sd, err := sp.tools.GetLocation("serial-discovery")
-	if err != nil {
-		logrus.Errorf("Error downloading serial-discovery: %s", err)
-		panic(err)
-	}
-	d := discovery.NewClient("serial", sd+"/serial-discovery")
-	dLogger := logrus.WithField("discovery", "serial")
-	if *verbose {
-		d.SetLogger(dLogger)
-	}
-	d.SetUserAgent("arduino-create-agent/" + version)
-	if err := d.Run(); err != nil {
-		logrus.Errorf("Error running serial-discovery: %s", err)
-		panic(err)
-	}
-	defer d.Quit()
-
-	events, err := d.StartSync(10)
-	if err != nil {
-		logrus.Errorf("Error starting event watcher on serial-discovery: %s", err)
-		panic(err)
-	}
-
-	logrus.Infof("Serial discovery started, watching for events")
-	for ev := range events {
-		logrus.WithField("event", ev).Debugf("Serial discovery event")
-		switch ev.Type {
-		case "add":
-			sp.add(ev.Port)
-		case "remove":
-			sp.remove(ev.Port)
-		}
-	}
-
-	sp.reset()
-	logrus.Errorf("Serial discovery stopped.")
-}
-
-func (sp *serialPortList) reset() {
-	sp.portsLock.Lock()
-	defer sp.portsLock.Unlock()
-	sp.Ports = []*SpPortItem{}
-}
-
-func (sp *serialPortList) add(addedPort *discovery.Port) {
-	if addedPort.Protocol != "serial" {
-		return
-	}
-	props := addedPort.Properties
-	if !props.ContainsKey("vid") {
-		return
-	}
-	vid, pid := props.Get("vid"), props.Get("pid")
-	if vid == "0x0000" || pid == "0x0000" {
-		return
-	}
-	if portsFilter != nil && !portsFilter.MatchString(addedPort.Address) {
-		logrus.Debugf("ignoring port not matching filter. port: %v\n", addedPort.Address)
-		return
-	}
-
-	sp.portsLock.Lock()
-	defer sp.portsLock.Unlock()
-
-	// If the port is already in the list, just update the metadata...
-	for _, oldPort := range sp.Ports {
-		fmt.Println("oldPort.Name: ", oldPort.Name)
-		if oldPort.Name == addedPort.Address {
-			oldPort.SerialNumber = props.Get("serialNumber")
-			oldPort.VendorID = vid
-			oldPort.ProductID = pid
-			return
-		}
-	}
-	// ...otherwise, add it to the list
-	sp.Ports = append(sp.Ports, &SpPortItem{
-		Name:            addedPort.Address,
-		SerialNumber:    props.Get("serialNumber"),
-		VendorID:        vid,
-		ProductID:       pid,
-		Ver:             version,
-		IsOpen:          false,
-		IsPrimary:       false,
-		Baud:            0,
-		BufferAlgorithm: "",
-	})
-}
-
-func (sp *serialPortList) remove(removedPort *discovery.Port) {
-	sp.portsLock.Lock()
-	defer sp.portsLock.Unlock()
-
-	// Remove the port from the list
-	sp.Ports = slices.DeleteFunc(sp.Ports, func(oldPort *SpPortItem) bool {
-		return oldPort.Name == removedPort.Address
-	})
-}
-
-// MarkPortAsOpened marks a port as opened by the user
-func (sp *serialPortList) MarkPortAsOpened(portname string) {
-	sp.portsLock.Lock()
-	defer sp.portsLock.Unlock()
-	port := sp.getPortByName(portname)
-	if port != nil {
-		port.IsOpen = true
-	}
-}
-
-// MarkPortAsClosed marks a port as no more opened by the user
-func (sp *serialPortList) MarkPortAsClosed(portname string) {
-	sp.portsLock.Lock()
-	defer sp.portsLock.Unlock()
-	port := sp.getPortByName(portname)
-	if port != nil {
-		port.IsOpen = false
-	}
-}
-
-func (sp *serialPortList) getPortByName(portname string) *SpPortItem {
-	for _, port := range sp.Ports {
-		if port.Name == portname {
-			return port
-		}
-	}
-	return nil
-}
-
 func (h *hub) spErr(err string) {
 	//log.Println("Sending err back: ", err)
 	//sh.hub.broadcastSys <- []byte(err)
diff --git a/serialport.go b/serialport.go
index 06ea0aa64..dea3ed9d0 100755
--- a/serialport.go
+++ b/serialport.go
@@ -62,7 +62,6 @@ type serport struct {
 	//bufferwatcher *BufferflowDummypause
 	bufferwatcher Bufferflow
 
-	// TODO: to remove global
 	OnMessage func([]byte)
 	OnClose   func(*serport)
 }
@@ -93,7 +92,6 @@ func (p *serport) reader(buftype string) {
 		if p.isClosing.Load() {
 			strmsg := "Shutting down reader on " + p.portConf.Name
 			log.Println(strmsg)
-			// h.broadcastSys <- ([]byte(strmsg)
 			p.OnMessage([]byte(strmsg))
 			break
 		}
@@ -148,18 +146,13 @@ func (p *serport) reader(buftype string) {
 			if err == io.EOF || err == io.ErrUnexpectedEOF {
 				// hit end of file
 				log.Println("Hit end of file on serial port")
-				// h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}")
 				p.OnMessage([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}"))
 
 			}
 
 			if err != nil {
 				log.Println(err)
-				// h.broadcastSys <- []byte("Error reading on " + p.portConf.Name + " " +
-				// 	err.Error() + " Closing port.")
 				p.OnMessage([]byte("Error reading on " + p.portConf.Name + " " + err.Error() + " Closing port."))
-
-				// h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}")
 				p.OnMessage([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}"))
 				p.isClosingDueToError = true
 				break
@@ -218,7 +211,6 @@ func (p *serport) writerBuffered() {
 	}
 	msgstr := "writerBuffered just got closed. make sure you make a new one. port:" + p.portConf.Name
 	log.Println(msgstr)
-	// h.broadcastSys <- []byte(msgstr)
 	p.OnMessage([]byte(msgstr))
 }
 
@@ -240,18 +232,17 @@ func (p *serport) writerNoBuf() {
 		if err != nil {
 			errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port."
 			log.Print(errstr)
-			// h.broadcastSys <- []byte(errstr)
 			p.OnMessage([]byte(errstr))
 			break
 		}
 	}
 	msgstr := "Shutting down writer on " + p.portConf.Name
 	log.Println(msgstr)
-	// h.broadcastSys <- []byte(msgstr)
 	p.OnMessage([]byte(msgstr))
+
 	p.portIo.Close()
-	// TODO: is this needed ?
-	// serialPorts.List()
+	// serialPorts.List(
+
 }
 
 // this method runs as its own thread because it's instantiated
@@ -283,7 +274,6 @@ func (p *serport) writerRaw() {
 	}
 	msgstr := "writerRaw just got closed. make sure you make a new one. port:" + p.portConf.Name
 	log.Println(msgstr)
-	// h.broadcastSys <- []byte(msgstr)
 	p.OnMessage([]byte(msgstr))
 }
 
diff --git a/serialportlist.go b/serialportlist.go
new file mode 100644
index 000000000..64f10debb
--- /dev/null
+++ b/serialportlist.go
@@ -0,0 +1,197 @@
+package main
+
+import (
+	"encoding/json"
+	"slices"
+	"sync"
+	"time"
+
+	"github.com/arduino/arduino-create-agent/tools"
+	discovery "github.com/arduino/pluggable-discovery-protocol-handler/v2"
+	"github.com/sirupsen/logrus"
+)
+
+type serialPortList struct {
+	tools *tools.Tools
+
+	Ports     []*SpPortItem
+	portsLock sync.Mutex
+
+	OnList func([]byte) `json:"-"`
+	OnErr  func(string) `json:"-"`
+}
+
+// SpPortItem is the serial port item
+type SpPortItem struct {
+	Name            string
+	SerialNumber    string
+	DeviceClass     string
+	IsOpen          bool
+	IsPrimary       bool
+	Baud            int
+	BufferAlgorithm string
+	Ver             string
+	VendorID        string
+	ProductID       string
+}
+
+func newSerialPortList(tools *tools.Tools) *serialPortList {
+	return &serialPortList{tools: tools}
+}
+
+// List broadcasts a Json representation of the ports found
+func (sp *serialPortList) List() {
+	sp.portsLock.Lock()
+	ls, err := json.MarshalIndent(sp, "", "\t")
+	sp.portsLock.Unlock()
+
+	if err != nil {
+		sp.OnErr("Error creating json on port list " + err.Error())
+	} else {
+		sp.OnList(ls)
+	}
+}
+
+// MarkPortAsOpened marks a port as opened by the user
+func (sp *serialPortList) MarkPortAsOpened(portname string) {
+	sp.portsLock.Lock()
+	defer sp.portsLock.Unlock()
+	port := sp.getPortByName(portname)
+	if port != nil {
+		port.IsOpen = true
+	}
+}
+
+// MarkPortAsClosed marks a port as no more opened by the user
+func (sp *serialPortList) MarkPortAsClosed(portname string) {
+	sp.portsLock.Lock()
+	defer sp.portsLock.Unlock()
+	port := sp.getPortByName(portname)
+	if port != nil {
+		port.IsOpen = false
+	}
+}
+
+// Run is the main loop for port discovery and management
+func (sp *serialPortList) Run() {
+	for retries := 0; retries < 10; retries++ {
+		sp.runSerialDiscovery()
+
+		logrus.Errorf("Serial discovery stopped working, restarting it in 10 seconds...")
+		time.Sleep(10 * time.Second)
+	}
+	logrus.Errorf("Failed restarting serial discovery. Giving up...")
+}
+
+func (sp *serialPortList) runSerialDiscovery() {
+	// First ensure that all the discoveries are available
+	noOpProgress := func(msg string) {}
+	if err := sp.tools.Download("builtin", "serial-discovery", "latest", "keep", noOpProgress); err != nil {
+		logrus.Errorf("Error downloading serial-discovery: %s", err)
+		panic(err)
+	}
+	sd, err := sp.tools.GetLocation("serial-discovery")
+	if err != nil {
+		logrus.Errorf("Error downloading serial-discovery: %s", err)
+		panic(err)
+	}
+	d := discovery.NewClient("serial", sd+"/serial-discovery")
+	dLogger := logrus.WithField("discovery", "serial")
+	if *verbose {
+		d.SetLogger(dLogger)
+	}
+	d.SetUserAgent("arduino-create-agent/" + version)
+	if err := d.Run(); err != nil {
+		logrus.Errorf("Error running serial-discovery: %s", err)
+		panic(err)
+	}
+	defer d.Quit()
+
+	events, err := d.StartSync(10)
+	if err != nil {
+		logrus.Errorf("Error starting event watcher on serial-discovery: %s", err)
+		panic(err)
+	}
+
+	logrus.Infof("Serial discovery started, watching for events")
+	for ev := range events {
+		logrus.WithField("event", ev).Debugf("Serial discovery event")
+		switch ev.Type {
+		case "add":
+			sp.add(ev.Port)
+		case "remove":
+			sp.remove(ev.Port)
+		}
+	}
+
+	sp.reset()
+	logrus.Errorf("Serial discovery stopped.")
+}
+
+func (sp *serialPortList) reset() {
+	sp.portsLock.Lock()
+	defer sp.portsLock.Unlock()
+	sp.Ports = []*SpPortItem{}
+}
+
+func (sp *serialPortList) add(addedPort *discovery.Port) {
+	if addedPort.Protocol != "serial" {
+		return
+	}
+	props := addedPort.Properties
+	if !props.ContainsKey("vid") {
+		return
+	}
+	vid, pid := props.Get("vid"), props.Get("pid")
+	if vid == "0x0000" || pid == "0x0000" {
+		return
+	}
+	if portsFilter != nil && !portsFilter.MatchString(addedPort.Address) {
+		logrus.Debugf("ignoring port not matching filter. port: %v\n", addedPort.Address)
+		return
+	}
+
+	sp.portsLock.Lock()
+	defer sp.portsLock.Unlock()
+
+	// If the port is already in the list, just update the metadata...
+	for _, oldPort := range sp.Ports {
+		if oldPort.Name == addedPort.Address {
+			oldPort.SerialNumber = props.Get("serialNumber")
+			oldPort.VendorID = vid
+			oldPort.ProductID = pid
+			return
+		}
+	}
+	// ...otherwise, add it to the list
+	sp.Ports = append(sp.Ports, &SpPortItem{
+		Name:            addedPort.Address,
+		SerialNumber:    props.Get("serialNumber"),
+		VendorID:        vid,
+		ProductID:       pid,
+		Ver:             version,
+		IsOpen:          false,
+		IsPrimary:       false,
+		Baud:            0,
+		BufferAlgorithm: "",
+	})
+}
+
+func (sp *serialPortList) remove(removedPort *discovery.Port) {
+	sp.portsLock.Lock()
+	defer sp.portsLock.Unlock()
+
+	// Remove the port from the list
+	sp.Ports = slices.DeleteFunc(sp.Ports, func(oldPort *SpPortItem) bool {
+		return oldPort.Name == removedPort.Address
+	})
+}
+
+func (sp *serialPortList) getPortByName(portname string) *SpPortItem {
+	for _, port := range sp.Ports {
+		if port.Name == portname {
+			return port
+		}
+	}
+	return nil
+}

From 4dba1a8436f678af64267438c3c3247cd9d72902 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 14:32:46 +0200
Subject: [PATCH 24/50] feat: add serialhub implementation for managing serial
 ports

---
 serial.go => serialhub.go | 7 -------
 1 file changed, 7 deletions(-)
 rename serial.go => serialhub.go (94%)

diff --git a/serial.go b/serialhub.go
similarity index 94%
rename from serial.go
rename to serialhub.go
index d98a6cb73..d248bc0a2 100755
--- a/serial.go
+++ b/serialhub.go
@@ -18,15 +18,8 @@
 package main
 
 import (
-	"encoding/json"
-	"slices"
 	"strings"
 	"sync"
-	"time"
-
-	"github.com/arduino/arduino-create-agent/tools"
-	discovery "github.com/arduino/pluggable-discovery-protocol-handler/v2"
-	"github.com/sirupsen/logrus"
 )
 
 type serialhub struct {

From 7ea21c02794483d4aabb5c210acdbbf66eb405dc Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 14:45:23 +0200
Subject: [PATCH 25/50] refactor: remove serialhub parameter from newHub
 function and instantiate newSerialHub within it

---
 hub.go  | 4 ++--
 main.go | 3 +--
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/hub.go b/hub.go
index 6e320e3cc..881d9ca7f 100755
--- a/hub.go
+++ b/hub.go
@@ -58,14 +58,14 @@ type hub struct {
 	systray *systray.Systray
 }
 
-func newHub(serialhub *serialhub, serialList *serialPortList, tools *tools.Tools, systray *systray.Systray) *hub {
+func newHub(serialList *serialPortList, tools *tools.Tools, systray *systray.Systray) *hub {
 	hub := &hub{
 		broadcast:      make(chan []byte, 1000),
 		broadcastSys:   make(chan []byte, 1000),
 		register:       make(chan *connection),
 		unregister:     make(chan *connection),
 		connections:    make(map[*connection]bool),
-		serialHub:      serialhub,
+		serialHub:      newSerialHub(),
 		serialPortList: serialList,
 		tools:          tools,
 		systray:        systray,
diff --git a/main.go b/main.go
index 9a1db15af..dfb217c26 100755
--- a/main.go
+++ b/main.go
@@ -177,9 +177,8 @@ func loop(stray *systray.Systray) {
 	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
 
 	serialPorts := newSerialPortList(tools)
-	serialHub := newSerialHub()
 
-	hub := newHub(serialHub, serialPorts, tools, stray)
+	hub := newHub(serialPorts, tools, stray)
 
 	// Let's handle the config
 	configDir := config.GetDefaultConfigDir()

From a152b028c7c6dc79dc36e0bb87917b9d4d76cf85 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 14:50:00 +0200
Subject: [PATCH 26/50] feat: move `spErr` `spClose` `spWrite` into hub (from
 serialhub)

---
 hub.go       | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++
 serialhub.go | 55 ----------------------------------------------------
 2 files changed, 54 insertions(+), 55 deletions(-)

diff --git a/hub.go b/hub.go
index 881d9ca7f..047134ffc 100755
--- a/hub.go
+++ b/hub.go
@@ -336,3 +336,57 @@ func (h *hub) garbageCollection() {
 	h.broadcastSys <- []byte("{\"gc\":\"done\"}")
 	h.memoryStats()
 }
+
+func (h *hub) spErr(err string) {
+	//log.Println("Sending err back: ", err)
+	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
+}
+
+func (h *hub) spClose(portname string) {
+	if myport, ok := h.serialHub.FindPortByName(portname); ok {
+		h.broadcastSys <- []byte("Closing serial port " + portname)
+		myport.Close()
+	} else {
+		h.spErr("We could not find the serial port " + portname + " that you were trying to close.")
+	}
+}
+
+func (h *hub) spWrite(arg string) {
+	// we will get a string of comXX asdf asdf asdf
+	//log.Println("Inside spWrite arg: " + arg)
+	arg = strings.TrimPrefix(arg, " ")
+	//log.Println("arg after trim: " + arg)
+	args := strings.SplitN(arg, " ", 3)
+	if len(args) != 3 {
+		errstr := "Could not parse send command: " + arg
+		//log.Println(errstr)
+		h.spErr(errstr)
+		return
+	}
+	bufferingMode := args[0]
+	portname := strings.Trim(args[1], " ")
+	data := args[2]
+
+	//log.Println("The port to write to is:" + portname + "---")
+	//log.Println("The data is:" + data + "---")
+
+	// See if we have this port open
+	port, ok := h.serialHub.FindPortByName(portname)
+	if !ok {
+		// we couldn't find the port, so send err
+		h.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
+		return
+	}
+
+	// see if bufferingMode is valid
+	switch bufferingMode {
+	case "send", "sendnobuf", "sendraw":
+		// valid buffering mode, go ahead
+	default:
+		h.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
+		return
+	}
+
+	// send it to the write channel
+	port.Write(data, bufferingMode)
+}
diff --git a/serialhub.go b/serialhub.go
index d248bc0a2..6c00718e6 100755
--- a/serialhub.go
+++ b/serialhub.go
@@ -70,58 +70,3 @@ func (sh *serialhub) FindPortByName(portname string) (*serport, bool) {
 	}
 	return nil, false
 }
-
-func (h *hub) spErr(err string) {
-	//log.Println("Sending err back: ", err)
-	//sh.hub.broadcastSys <- []byte(err)
-	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
-}
-
-func (h *hub) spClose(portname string) {
-	if myport, ok := h.serialHub.FindPortByName(portname); ok {
-		h.broadcastSys <- []byte("Closing serial port " + portname)
-		myport.Close()
-	} else {
-		h.spErr("We could not find the serial port " + portname + " that you were trying to close.")
-	}
-}
-
-func (h *hub) spWrite(arg string) {
-	// we will get a string of comXX asdf asdf asdf
-	//log.Println("Inside spWrite arg: " + arg)
-	arg = strings.TrimPrefix(arg, " ")
-	//log.Println("arg after trim: " + arg)
-	args := strings.SplitN(arg, " ", 3)
-	if len(args) != 3 {
-		errstr := "Could not parse send command: " + arg
-		//log.Println(errstr)
-		h.spErr(errstr)
-		return
-	}
-	bufferingMode := args[0]
-	portname := strings.Trim(args[1], " ")
-	data := args[2]
-
-	//log.Println("The port to write to is:" + portname + "---")
-	//log.Println("The data is:" + data + "---")
-
-	// See if we have this port open
-	port, ok := h.serialHub.FindPortByName(portname)
-	if !ok {
-		// we couldn't find the port, so send err
-		h.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
-		return
-	}
-
-	// see if bufferingMode is valid
-	switch bufferingMode {
-	case "send", "sendnobuf", "sendraw":
-		// valid buffering mode, go ahead
-	default:
-		h.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
-		return
-	}
-
-	// send it to the write channel
-	port.Write(data, bufferingMode)
-}

From fac0b109f761606a5fcedf082841011eb8118f14 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 14:51:28 +0200
Subject: [PATCH 27/50] refactor: remove newSerialHub instantiation from hub
 initialization in tests

---
 main_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/main_test.go b/main_test.go
index a24c29c6d..bbede14fd 100644
--- a/main_test.go
+++ b/main_test.go
@@ -63,7 +63,7 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
 	require.NoError(t, err)
 	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
-	hub := newHub(newSerialHub(), newSerialPortList(tools), tools, &systray.Systray{})
+	hub := newHub(newSerialPortList(tools), tools, &systray.Systray{})
 	pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))
 
 	r.POST("/", uploadHandler(hub, pubkey, tools))
@@ -107,7 +107,7 @@ func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) {
 	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
 	require.NoError(t, err)
 	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
-	hub := newHub(newSerialHub(), newSerialPortList(tools), tools, &systray.Systray{})
+	hub := newHub(newSerialPortList(tools), tools, &systray.Systray{})
 	pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))
 
 	r.POST("/", uploadHandler(hub, pubkey, tools))

From f04d5cc4c50abfc3c6029c1115442fbeedf14038 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 15:10:48 +0200
Subject: [PATCH 28/50] move `spHandlerOpen` to hub from serialport.go

---
 hub.go        | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++
 serialport.go | 84 -------------------------------------------------
 2 files changed, 86 insertions(+), 84 deletions(-)

diff --git a/hub.go b/hub.go
index 047134ffc..96ab3e1b6 100755
--- a/hub.go
+++ b/hub.go
@@ -16,6 +16,7 @@
 package main
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"html"
@@ -30,6 +31,7 @@ import (
 	"github.com/arduino/arduino-create-agent/tools"
 	"github.com/arduino/arduino-create-agent/upload"
 	log "github.com/sirupsen/logrus"
+	"go.bug.st/serial"
 )
 
 type hub struct {
@@ -280,6 +282,90 @@ func (h *hub) checkCmd(m []byte) {
 	}
 }
 
+func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
+
+	log.Print("Inside spHandler")
+
+	var out bytes.Buffer
+
+	out.WriteString("Opening serial port ")
+	out.WriteString(portname)
+	out.WriteString(" at ")
+	out.WriteString(strconv.Itoa(baud))
+	out.WriteString(" baud")
+	log.Print(out.String())
+
+	conf := &SerialConfig{Name: portname, Baud: baud, RtsOn: true}
+
+	mode := &serial.Mode{
+		BaudRate: baud,
+	}
+
+	sp, err := serial.Open(portname, mode)
+	log.Print("Just tried to open port")
+	if err != nil {
+		//log.Fatal(err)
+		log.Print("Error opening port " + err.Error())
+		//h.broadcastSys <- []byte("Error opening port. " + err.Error())
+		h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}")
+
+		return
+	}
+	log.Print("Opened port successfully")
+	//p := &serport{send: make(chan []byte, 256), portConf: conf, portIo: sp}
+	// we can go up to 256,000 lines of gcode in the buffer
+	p := &serport{
+		sendBuffered: make(chan string, 256000),
+		sendNoBuf:    make(chan []byte),
+		sendRaw:      make(chan string),
+		portConf:     conf,
+		portIo:       sp,
+		portName:     portname,
+		BufferType:   buftype,
+	}
+
+	p.OnMessage = func(msg []byte) {
+		h.broadcastSys <- msg
+	}
+	p.OnClose = func(port *serport) {
+		h.serialPortList.MarkPortAsClosed(p.portName)
+		h.serialPortList.List()
+	}
+
+	var bw Bufferflow
+
+	switch buftype {
+	case "timed":
+		bw = NewBufferflowTimed(portname, h.broadcastSys)
+	case "timedraw":
+		bw = NewBufferflowTimedRaw(portname, h.broadcastSys)
+	case "default":
+		bw = NewBufferflowDefault(portname, h.broadcastSys)
+	default:
+		log.Panicf("unknown buffer type: %s", buftype)
+	}
+
+	bw.Init()
+	p.bufferwatcher = bw
+
+	h.serialHub.Register(p)
+	defer h.serialHub.Unregister(p)
+
+	h.serialPortList.MarkPortAsOpened(portname)
+	h.serialPortList.List()
+
+	// this is internally buffered thread to not send to serial port if blocked
+	go p.writerBuffered()
+	// this is thread to send to serial port regardless of block
+	go p.writerNoBuf()
+	// this is thread to send to serial port but with base64 decoding
+	go p.writerRaw()
+
+	p.reader(buftype)
+
+	h.serialPortList.List()
+}
+
 type logWriter struct {
 	onWrite func([]byte)
 }
diff --git a/serialport.go b/serialport.go
index dea3ed9d0..77f155d16 100755
--- a/serialport.go
+++ b/serialport.go
@@ -277,90 +277,6 @@ func (p *serport) writerRaw() {
 	p.OnMessage([]byte(msgstr))
 }
 
-func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
-
-	log.Print("Inside spHandler")
-
-	var out bytes.Buffer
-
-	out.WriteString("Opening serial port ")
-	out.WriteString(portname)
-	out.WriteString(" at ")
-	out.WriteString(strconv.Itoa(baud))
-	out.WriteString(" baud")
-	log.Print(out.String())
-
-	conf := &SerialConfig{Name: portname, Baud: baud, RtsOn: true}
-
-	mode := &serial.Mode{
-		BaudRate: baud,
-	}
-
-	sp, err := serial.Open(portname, mode)
-	log.Print("Just tried to open port")
-	if err != nil {
-		//log.Fatal(err)
-		log.Print("Error opening port " + err.Error())
-		//h.broadcastSys <- []byte("Error opening port. " + err.Error())
-		h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}")
-
-		return
-	}
-	log.Print("Opened port successfully")
-	//p := &serport{send: make(chan []byte, 256), portConf: conf, portIo: sp}
-	// we can go up to 256,000 lines of gcode in the buffer
-	p := &serport{
-		sendBuffered: make(chan string, 256000),
-		sendNoBuf:    make(chan []byte),
-		sendRaw:      make(chan string),
-		portConf:     conf,
-		portIo:       sp,
-		portName:     portname,
-		BufferType:   buftype,
-	}
-
-	p.OnMessage = func(msg []byte) {
-		h.broadcastSys <- msg
-	}
-	p.OnClose = func(port *serport) {
-		h.serialPortList.MarkPortAsClosed(p.portName)
-		h.serialPortList.List()
-	}
-
-	var bw Bufferflow
-
-	switch buftype {
-	case "timed":
-		bw = NewBufferflowTimed(portname, h.broadcastSys)
-	case "timedraw":
-		bw = NewBufferflowTimedRaw(portname, h.broadcastSys)
-	case "default":
-		bw = NewBufferflowDefault(portname, h.broadcastSys)
-	default:
-		log.Panicf("unknown buffer type: %s", buftype)
-	}
-
-	bw.Init()
-	p.bufferwatcher = bw
-
-	h.serialHub.Register(p)
-	defer h.serialHub.Unregister(p)
-
-	h.serialPortList.MarkPortAsOpened(portname)
-	h.serialPortList.List()
-
-	// this is internally buffered thread to not send to serial port if blocked
-	go p.writerBuffered()
-	// this is thread to send to serial port regardless of block
-	go p.writerNoBuf()
-	// this is thread to send to serial port but with base64 decoding
-	go p.writerRaw()
-
-	p.reader(buftype)
-
-	h.serialPortList.List()
-}
-
 func (p *serport) Close() {
 	p.isClosing.Store(true)
 

From 47213d120d25d2dbbe508eb5b3f1ef242db44066 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 15:14:55 +0200
Subject: [PATCH 29/50] refactor: remove unused serial package import from
 serialport.go

---
 serialport.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/serialport.go b/serialport.go
index 77f155d16..5fadd87e0 100755
--- a/serialport.go
+++ b/serialport.go
@@ -25,7 +25,6 @@ import (
 	"unicode/utf8"
 
 	log "github.com/sirupsen/logrus"
-	serial "go.bug.st/serial"
 )
 
 // SerialConfig is the serial port configuration

From 14ddd43c93200a31ba907dcb230022ce5b65a2e8 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 15:26:48 +0200
Subject: [PATCH 30/50] renamed h into hub

---
 conn.go |  26 +++---
 hub.go  | 246 ++++++++++++++++++++++++++++----------------------------
 info.go |   4 +-
 3 files changed, 138 insertions(+), 138 deletions(-)

diff --git a/conn.go b/conn.go
index d99f24edd..ad27c8fa6 100644
--- a/conn.go
+++ b/conn.go
@@ -81,7 +81,7 @@ type Upload struct {
 
 var uploadStatusStr = "ProgrammerStatus"
 
-func uploadHandler(h *hub, pubKey *rsa.PublicKey, tools *tools.Tools) func(*gin.Context) {
+func uploadHandler(hub *hub, pubKey *rsa.PublicKey, tools *tools.Tools) func(*gin.Context) {
 	return func(c *gin.Context) {
 		data := new(Upload)
 		if err := c.BindJSON(data); err != nil {
@@ -165,7 +165,7 @@ func uploadHandler(h *hub, pubKey *rsa.PublicKey, tools *tools.Tools) func(*gin.
 			// Resolve commandline
 			commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, tools)
 			if err != nil {
-				send(h, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
+				send(hub, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
 				return
 			}
 
@@ -175,16 +175,16 @@ func uploadHandler(h *hub, pubKey *rsa.PublicKey, tools *tools.Tools) func(*gin.
 			if data.Extra.Network {
 				err = errors.New("network upload is not supported anymore, pease use OTA instead")
 			} else {
-				send(h, map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"})
+				send(hub, map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"})
 				err = upload.Serial(data.Port, commandline, data.Extra, l)
 			}
 
 			// Handle result
 			if err != nil {
-				send(h, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
+				send(hub, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()})
 				return
 			}
-			send(h, map[string]string{uploadStatusStr: "Done", "Flash": "Ok"})
+			send(hub, map[string]string{uploadStatusStr: "Done", "Flash": "Ok"})
 		}()
 
 		c.String(http.StatusAccepted, "")
@@ -194,7 +194,7 @@ func uploadHandler(h *hub, pubKey *rsa.PublicKey, tools *tools.Tools) func(*gin.
 // PLogger sends the info from the upload to the websocket
 type PLogger struct {
 	Verbose bool
-	h       *hub
+	hub     *hub
 }
 
 // Debug only sends messages if verbose is true (always true for now)
@@ -208,15 +208,15 @@ func (l PLogger) Debug(args ...interface{}) {
 func (l PLogger) Info(args ...interface{}) {
 	output := fmt.Sprint(args...)
 	log.Println(output)
-	send(l.h, map[string]string{uploadStatusStr: "Busy", "Msg": output})
+	send(l.hub, map[string]string{uploadStatusStr: "Busy", "Msg": output})
 }
 
-func send(h *hub, args map[string]string) {
+func send(hub *hub, args map[string]string) {
 	mapB, _ := json.Marshal(args)
-	h.broadcastSys <- mapB
+	hub.broadcastSys <- mapB
 }
 
-func wsHandler(h *hub) *WsServer {
+func wsHandler(hub *hub) *WsServer {
 	server, err := socketio.NewServer(nil)
 	if err != nil {
 		log.Fatal(err)
@@ -224,13 +224,13 @@ func wsHandler(h *hub) *WsServer {
 
 	server.On("connection", func(so socketio.Socket) {
 		c := &connection{send: make(chan []byte, 256*10), ws: so}
-		h.register <- c
+		hub.register <- c
 		so.On("command", func(message string) {
-			h.broadcast <- []byte(message)
+			hub.broadcast <- []byte(message)
 		})
 
 		so.On("disconnection", func() {
-			h.unregister <- c
+			hub.unregister <- c
 		})
 		go c.writer()
 	})
diff --git a/hub.go b/hub.go
index 96ab3e1b6..495f851c1 100755
--- a/hub.go
+++ b/hub.go
@@ -110,51 +110,51 @@ const commands = `{
   ]
 }`
 
-func (h *hub) unregisterConnection(c *connection) {
-	if _, contains := h.connections[c]; !contains {
+func (hub *hub) unregisterConnection(c *connection) {
+	if _, contains := hub.connections[c]; !contains {
 		return
 	}
-	delete(h.connections, c)
+	delete(hub.connections, c)
 	close(c.send)
 }
 
-func (h *hub) sendToRegisteredConnections(data []byte) {
-	for c := range h.connections {
+func (hub *hub) sendToRegisteredConnections(data []byte) {
+	for c := range hub.connections {
 		select {
 		case c.send <- data:
 			//log.Print("did broadcast to ")
 			//log.Print(c.ws.RemoteAddr())
 			//c.send <- []byte("hello world")
 		default:
-			h.unregisterConnection(c)
+			hub.unregisterConnection(c)
 		}
 	}
 }
 
-func (h *hub) run() {
+func (hub *hub) run() {
 	for {
 		select {
-		case c := <-h.register:
-			h.connections[c] = true
+		case c := <-hub.register:
+			hub.connections[c] = true
 			// send supported commands
 			c.send <- []byte(fmt.Sprintf(`{"Version" : "%s"} `, version))
 			c.send <- []byte(html.EscapeString(commands))
 			c.send <- []byte(fmt.Sprintf(`{"Hostname" : "%s"} `, *hostname))
 			c.send <- []byte(fmt.Sprintf(`{"OS" : "%s"} `, runtime.GOOS))
-		case c := <-h.unregister:
-			h.unregisterConnection(c)
-		case m := <-h.broadcast:
+		case c := <-hub.unregister:
+			hub.unregisterConnection(c)
+		case m := <-hub.broadcast:
 			if len(m) > 0 {
-				h.checkCmd(m)
-				h.sendToRegisteredConnections(m)
+				hub.checkCmd(m)
+				hub.sendToRegisteredConnections(m)
 			}
-		case m := <-h.broadcastSys:
-			h.sendToRegisteredConnections(m)
+		case m := <-hub.broadcastSys:
+			hub.sendToRegisteredConnections(m)
 		}
 	}
 }
 
-func (h *hub) checkCmd(m []byte) {
+func (hub *hub) checkCmd(m []byte) {
 	//log.Print("Inside checkCmd")
 	s := string(m[:])
 
@@ -169,18 +169,18 @@ func (h *hub) checkCmd(m []byte) {
 
 		args := strings.Split(s, " ")
 		if len(args) < 3 {
-			go h.spErr("You did not specify a port and baud rate in your open cmd")
+			go hub.spErr("You did not specify a port and baud rate in your open cmd")
 			return
 		}
 		if len(args[1]) < 1 {
-			go h.spErr("You did not specify a serial port")
+			go hub.spErr("You did not specify a serial port")
 			return
 		}
 
 		baudStr := strings.Replace(args[2], "\n", "", -1)
 		baud, err := strconv.Atoi(baudStr)
 		if err != nil {
-			go h.spErr("Problem converting baud rate " + args[2])
+			go hub.spErr("Problem converting baud rate " + args[2])
 			return
 		}
 		// pass in buffer type now as string. if user does not
@@ -191,30 +191,30 @@ func (h *hub) checkCmd(m []byte) {
 			buftype := strings.Replace(args[3], "\n", "", -1)
 			bufferAlgorithm = buftype
 		}
-		go h.spHandlerOpen(args[1], baud, bufferAlgorithm)
+		go hub.spHandlerOpen(args[1], baud, bufferAlgorithm)
 
 	} else if strings.HasPrefix(sl, "close") {
 
 		args := strings.Split(s, " ")
 		if len(args) > 1 {
-			go h.spClose(args[1])
+			go hub.spClose(args[1])
 		} else {
-			go h.spErr("You did not specify a port to close")
+			go hub.spErr("You did not specify a port to close")
 		}
 
 	} else if strings.HasPrefix(sl, "killupload") {
 		// kill the running process (assumes singleton for now)
 		go func() {
 			upload.Kill()
-			h.broadcastSys <- []byte("{\"uploadStatus\": \"Killed\"}")
+			hub.broadcastSys <- []byte("{\"uploadStatus\": \"Killed\"}")
 			log.Println("{\"uploadStatus\": \"Killed\"}")
 		}()
 
 	} else if strings.HasPrefix(sl, "send") {
 		// will catch send and sendnobuf and sendraw
-		go h.spWrite(s)
+		go hub.spWrite(s)
 	} else if strings.HasPrefix(sl, "list") {
-		go h.serialPortList.List()
+		go hub.serialPortList.List()
 	} else if strings.HasPrefix(sl, "downloadtool") {
 		go func() {
 			args := strings.Split(s, " ")
@@ -225,7 +225,7 @@ func (h *hub) checkCmd(m []byte) {
 			if len(args) <= 1 {
 				mapD := map[string]string{"DownloadStatus": "Error", "Msg": "Not enough arguments"}
 				mapB, _ := json.Marshal(mapD)
-				h.broadcastSys <- mapB
+				hub.broadcastSys <- mapB
 				return
 			}
 			if len(args) > 1 {
@@ -248,41 +248,41 @@ func (h *hub) checkCmd(m []byte) {
 			reportPendingProgress := func(msg string) {
 				mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg}
 				mapB, _ := json.Marshal(mapD)
-				h.broadcastSys <- mapB
+				hub.broadcastSys <- mapB
 			}
-			err := h.tools.Download(pack, tool, toolVersion, behaviour, reportPendingProgress)
+			err := hub.tools.Download(pack, tool, toolVersion, behaviour, reportPendingProgress)
 			if err != nil {
 				mapD := map[string]string{"DownloadStatus": "Error", "Msg": err.Error()}
 				mapB, _ := json.Marshal(mapD)
-				h.broadcastSys <- mapB
+				hub.broadcastSys <- mapB
 			} else {
 				mapD := map[string]string{"DownloadStatus": "Success", "Msg": "Map Updated"}
 				mapB, _ := json.Marshal(mapD)
-				h.broadcastSys <- mapB
+				hub.broadcastSys <- mapB
 			}
 		}()
 	} else if strings.HasPrefix(sl, "log") {
-		go h.logAction(sl)
+		go hub.logAction(sl)
 	} else if strings.HasPrefix(sl, "restart") {
 		// potentially, the sysStray dependencies can be removed  https://github.com/arduino/arduino-create-agent/issues/1013
 		log.Println("Received restart from the daemon. Why? Boh")
-		h.systray.Restart()
+		hub.systray.Restart()
 	} else if strings.HasPrefix(sl, "exit") {
-		h.systray.Quit()
+		hub.systray.Quit()
 	} else if strings.HasPrefix(sl, "memstats") {
-		h.memoryStats()
+		hub.memoryStats()
 	} else if strings.HasPrefix(sl, "gc") {
-		h.garbageCollection()
+		hub.garbageCollection()
 	} else if strings.HasPrefix(sl, "hostname") {
-		h.getHostname()
+		hub.getHostname()
 	} else if strings.HasPrefix(sl, "version") {
-		h.getVersion()
+		hub.getVersion()
 	} else {
-		go h.spErr("Could not understand command.")
+		go hub.spErr("Could not understand command.")
 	}
 }
 
-func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
+func (hub *hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	log.Print("Inside spHandler")
 
@@ -306,8 +306,8 @@ func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 	if err != nil {
 		//log.Fatal(err)
 		log.Print("Error opening port " + err.Error())
-		//h.broadcastSys <- []byte("Error opening port. " + err.Error())
-		h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}")
+		//hub.broadcastSys <- []byte("Error opening port. " + err.Error())
+		hub.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}")
 
 		return
 	}
@@ -325,22 +325,22 @@ func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 	}
 
 	p.OnMessage = func(msg []byte) {
-		h.broadcastSys <- msg
+		hub.broadcastSys <- msg
 	}
 	p.OnClose = func(port *serport) {
-		h.serialPortList.MarkPortAsClosed(p.portName)
-		h.serialPortList.List()
+		hub.serialPortList.MarkPortAsClosed(p.portName)
+		hub.serialPortList.List()
 	}
 
 	var bw Bufferflow
 
 	switch buftype {
 	case "timed":
-		bw = NewBufferflowTimed(portname, h.broadcastSys)
+		bw = NewBufferflowTimed(portname, hub.broadcastSys)
 	case "timedraw":
-		bw = NewBufferflowTimedRaw(portname, h.broadcastSys)
+		bw = NewBufferflowTimedRaw(portname, hub.broadcastSys)
 	case "default":
-		bw = NewBufferflowDefault(portname, h.broadcastSys)
+		bw = NewBufferflowDefault(portname, hub.broadcastSys)
 	default:
 		log.Panicf("unknown buffer type: %s", buftype)
 	}
@@ -348,11 +348,11 @@ func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 	bw.Init()
 	p.bufferwatcher = bw
 
-	h.serialHub.Register(p)
-	defer h.serialHub.Unregister(p)
+	hub.serialHub.Register(p)
+	defer hub.serialHub.Unregister(p)
 
-	h.serialPortList.MarkPortAsOpened(portname)
-	h.serialPortList.List()
+	hub.serialPortList.MarkPortAsOpened(portname)
+	hub.serialPortList.List()
 
 	// this is internally buffered thread to not send to serial port if blocked
 	go p.writerBuffered()
@@ -363,25 +363,69 @@ func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	p.reader(buftype)
 
-	h.serialPortList.List()
+	hub.serialPortList.List()
 }
 
-type logWriter struct {
-	onWrite func([]byte)
+func (hub *hub) spClose(portname string) {
+	if myport, ok := hub.serialHub.FindPortByName(portname); ok {
+		hub.broadcastSys <- []byte("Closing serial port " + portname)
+		myport.Close()
+	} else {
+		hub.spErr("We could not find the serial port " + portname + " that you were trying to close.")
+	}
 }
 
-func (u *logWriter) Write(p []byte) (n int, err error) {
-	u.onWrite(p)
-	return len(p), nil
+func (hub *hub) spWrite(arg string) {
+	// we will get a string of comXX asdf asdf asdf
+	//log.Println("Inside spWrite arg: " + arg)
+	arg = strings.TrimPrefix(arg, " ")
+	//log.Println("arg after trim: " + arg)
+	args := strings.SplitN(arg, " ", 3)
+	if len(args) != 3 {
+		errstr := "Could not parse send command: " + arg
+		//log.Println(errstr)
+		hub.spErr(errstr)
+		return
+	}
+	bufferingMode := args[0]
+	portname := strings.Trim(args[1], " ")
+	data := args[2]
+
+	//log.Println("The port to write to is:" + portname + "---")
+	//log.Println("The data is:" + data + "---")
+
+	// See if we have this port open
+	port, ok := hub.serialHub.FindPortByName(portname)
+	if !ok {
+		// we couldn't find the port, so send err
+		hub.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
+		return
+	}
+
+	// see if bufferingMode is valid
+	switch bufferingMode {
+	case "send", "sendnobuf", "sendraw":
+		// valid buffering mode, go ahead
+	default:
+		hub.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
+		return
+	}
+
+	// send it to the write channel
+	port.Write(data, bufferingMode)
+}
+
+type logWriter struct {
+	onWrite func([]byte)
 }
 
-func (h *hub) logAction(sl string) {
+func (hub *hub) logAction(sl string) {
 	if strings.HasPrefix(sl, "log on") {
 		*logDump = "on"
 
 		logWriter := logWriter{}
 		logWriter.onWrite = func(p []byte) {
-			h.broadcastSys <- p
+			hub.broadcastSys <- p
 		}
 
 		multiWriter := io.MultiWriter(&logWriter, os.Stderr)
@@ -391,88 +435,44 @@ func (h *hub) logAction(sl string) {
 		log.SetOutput(os.Stderr)
 		// } else if strings.HasPrefix(sl, "log show") {
 		// TODO: send all the saved log to websocket
-		//h.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *logDump + "\"}")
+		//hub.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *logDump + "\"}")
 	}
 }
 
-func (h *hub) memoryStats() {
+func (u *logWriter) Write(p []byte) (n int, err error) {
+	u.onWrite(p)
+	return len(p), nil
+}
+
+func (hub *hub) memoryStats() {
 	var memStats runtime.MemStats
 	runtime.ReadMemStats(&memStats)
 	json, _ := json.Marshal(memStats)
 	log.Printf("memStats:%v\n", string(json))
-	h.broadcastSys <- json
+	hub.broadcastSys <- json
 }
 
-func (h *hub) getHostname() {
-	h.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
+func (hub *hub) getHostname() {
+	hub.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
 }
 
-func (h *hub) getVersion() {
-	h.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
+func (hub *hub) getVersion() {
+	hub.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
 }
 
-func (h *hub) garbageCollection() {
+func (hub *hub) garbageCollection() {
 	log.Printf("Starting garbageCollection()\n")
-	h.broadcastSys <- []byte("{\"gc\":\"starting\"}")
-	h.memoryStats()
+	hub.broadcastSys <- []byte("{\"gc\":\"starting\"}")
+	hub.memoryStats()
 	debug.SetGCPercent(100)
 	debug.FreeOSMemory()
 	debug.SetGCPercent(-1)
 	log.Printf("Done with garbageCollection()\n")
-	h.broadcastSys <- []byte("{\"gc\":\"done\"}")
-	h.memoryStats()
+	hub.broadcastSys <- []byte("{\"gc\":\"done\"}")
+	hub.memoryStats()
 }
 
-func (h *hub) spErr(err string) {
+func (hub *hub) spErr(err string) {
 	//log.Println("Sending err back: ", err)
-	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
-}
-
-func (h *hub) spClose(portname string) {
-	if myport, ok := h.serialHub.FindPortByName(portname); ok {
-		h.broadcastSys <- []byte("Closing serial port " + portname)
-		myport.Close()
-	} else {
-		h.spErr("We could not find the serial port " + portname + " that you were trying to close.")
-	}
-}
-
-func (h *hub) spWrite(arg string) {
-	// we will get a string of comXX asdf asdf asdf
-	//log.Println("Inside spWrite arg: " + arg)
-	arg = strings.TrimPrefix(arg, " ")
-	//log.Println("arg after trim: " + arg)
-	args := strings.SplitN(arg, " ", 3)
-	if len(args) != 3 {
-		errstr := "Could not parse send command: " + arg
-		//log.Println(errstr)
-		h.spErr(errstr)
-		return
-	}
-	bufferingMode := args[0]
-	portname := strings.Trim(args[1], " ")
-	data := args[2]
-
-	//log.Println("The port to write to is:" + portname + "---")
-	//log.Println("The data is:" + data + "---")
-
-	// See if we have this port open
-	port, ok := h.serialHub.FindPortByName(portname)
-	if !ok {
-		// we couldn't find the port, so send err
-		h.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
-		return
-	}
-
-	// see if bufferingMode is valid
-	switch bufferingMode {
-	case "send", "sendnobuf", "sendraw":
-		// valid buffering mode, go ahead
-	default:
-		h.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
-		return
-	}
-
-	// send it to the write channel
-	port.Write(data, bufferingMode)
+	hub.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
 }
diff --git a/info.go b/info.go
index bf04713e3..2d6296717 100644
--- a/info.go
+++ b/info.go
@@ -41,12 +41,12 @@ func infoHandler(c *gin.Context) {
 	})
 }
 
-func pauseHandler(h *hub, s *systray.Systray) func(c *gin.Context) {
+func pauseHandler(hub *hub, s *systray.Systray) func(c *gin.Context) {
 	return func(c *gin.Context) {
 		go func() {
 			ports, _ := serial.GetPortsList()
 			for _, element := range ports {
-				h.spClose(element)
+				hub.spClose(element)
 			}
 			*hibernate = true
 			s.Pause()

From 1f9b999256c2cbf3ef5a9ca615ec772a810f8399 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 15:28:09 +0200
Subject: [PATCH 31/50] fix: add hub reference to PLogger initialization in
 uploadHandler

---
 conn.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/conn.go b/conn.go
index ad27c8fa6..42b573bb5 100644
--- a/conn.go
+++ b/conn.go
@@ -169,7 +169,7 @@ func uploadHandler(hub *hub, pubKey *rsa.PublicKey, tools *tools.Tools) func(*gi
 				return
 			}
 
-			l := PLogger{Verbose: true}
+			l := PLogger{Verbose: true, hub: hub}
 
 			// Upload
 			if data.Extra.Network {

From 4585769d630056aca8f67576a86475d64b27785d Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 15:32:35 +0200
Subject: [PATCH 32/50] move `spErr, spWrite, spClose spHandlerOpen at the end
 for redaibility

---
 hub.go | 114 ++++++++++++++++++++++++++++-----------------------------
 1 file changed, 57 insertions(+), 57 deletions(-)

diff --git a/hub.go b/hub.go
index 495f851c1..5d4b66fa2 100755
--- a/hub.go
+++ b/hub.go
@@ -282,6 +282,63 @@ func (hub *hub) checkCmd(m []byte) {
 	}
 }
 
+type logWriter struct {
+	onWrite func([]byte)
+}
+
+func (hub *hub) logAction(sl string) {
+	if strings.HasPrefix(sl, "log on") {
+		*logDump = "on"
+
+		logWriter := logWriter{}
+		logWriter.onWrite = func(p []byte) {
+			hub.broadcastSys <- p
+		}
+
+		multiWriter := io.MultiWriter(&logWriter, os.Stderr)
+		log.SetOutput(multiWriter)
+	} else if strings.HasPrefix(sl, "log off") {
+		*logDump = "off"
+		log.SetOutput(os.Stderr)
+		// } else if strings.HasPrefix(sl, "log show") {
+		// TODO: send all the saved log to websocket
+		//hub.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *logDump + "\"}")
+	}
+}
+
+func (u *logWriter) Write(p []byte) (n int, err error) {
+	u.onWrite(p)
+	return len(p), nil
+}
+
+func (hub *hub) memoryStats() {
+	var memStats runtime.MemStats
+	runtime.ReadMemStats(&memStats)
+	json, _ := json.Marshal(memStats)
+	log.Printf("memStats:%v\n", string(json))
+	hub.broadcastSys <- json
+}
+
+func (hub *hub) getHostname() {
+	hub.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
+}
+
+func (hub *hub) getVersion() {
+	hub.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
+}
+
+func (hub *hub) garbageCollection() {
+	log.Printf("Starting garbageCollection()\n")
+	hub.broadcastSys <- []byte("{\"gc\":\"starting\"}")
+	hub.memoryStats()
+	debug.SetGCPercent(100)
+	debug.FreeOSMemory()
+	debug.SetGCPercent(-1)
+	log.Printf("Done with garbageCollection()\n")
+	hub.broadcastSys <- []byte("{\"gc\":\"done\"}")
+	hub.memoryStats()
+}
+
 func (hub *hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	log.Print("Inside spHandler")
@@ -415,63 +472,6 @@ func (hub *hub) spWrite(arg string) {
 	port.Write(data, bufferingMode)
 }
 
-type logWriter struct {
-	onWrite func([]byte)
-}
-
-func (hub *hub) logAction(sl string) {
-	if strings.HasPrefix(sl, "log on") {
-		*logDump = "on"
-
-		logWriter := logWriter{}
-		logWriter.onWrite = func(p []byte) {
-			hub.broadcastSys <- p
-		}
-
-		multiWriter := io.MultiWriter(&logWriter, os.Stderr)
-		log.SetOutput(multiWriter)
-	} else if strings.HasPrefix(sl, "log off") {
-		*logDump = "off"
-		log.SetOutput(os.Stderr)
-		// } else if strings.HasPrefix(sl, "log show") {
-		// TODO: send all the saved log to websocket
-		//hub.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *logDump + "\"}")
-	}
-}
-
-func (u *logWriter) Write(p []byte) (n int, err error) {
-	u.onWrite(p)
-	return len(p), nil
-}
-
-func (hub *hub) memoryStats() {
-	var memStats runtime.MemStats
-	runtime.ReadMemStats(&memStats)
-	json, _ := json.Marshal(memStats)
-	log.Printf("memStats:%v\n", string(json))
-	hub.broadcastSys <- json
-}
-
-func (hub *hub) getHostname() {
-	hub.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
-}
-
-func (hub *hub) getVersion() {
-	hub.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
-}
-
-func (hub *hub) garbageCollection() {
-	log.Printf("Starting garbageCollection()\n")
-	hub.broadcastSys <- []byte("{\"gc\":\"starting\"}")
-	hub.memoryStats()
-	debug.SetGCPercent(100)
-	debug.FreeOSMemory()
-	debug.SetGCPercent(-1)
-	log.Printf("Done with garbageCollection()\n")
-	hub.broadcastSys <- []byte("{\"gc\":\"done\"}")
-	hub.memoryStats()
-}
-
 func (hub *hub) spErr(err string) {
 	//log.Println("Sending err back: ", err)
 	hub.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")

From 283064c82ebafb034cf7206333399d7ec2fc7c7b Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 16:15:15 +0200
Subject: [PATCH 33/50] refactor: clean up serialport.go and serialportlist.go
 for clarity

---
 serialport.go     | 5 ++++-
 serialportlist.go | 3 +--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/serialport.go b/serialport.go
index 5fadd87e0..eef4410cc 100755
--- a/serialport.go
+++ b/serialport.go
@@ -240,7 +240,10 @@ func (p *serport) writerNoBuf() {
 	p.OnMessage([]byte(msgstr))
 
 	p.portIo.Close()
-	// serialPorts.List(
+
+	// The effect of this call is to send in to all the we client the list of serial ports
+	// TODO: investigate if this is superfluous and it can be removed.
+	// serialPorts.List()
 
 }
 
diff --git a/serialportlist.go b/serialportlist.go
index 64f10debb..1c38a49ee 100644
--- a/serialportlist.go
+++ b/serialportlist.go
@@ -12,11 +12,10 @@ import (
 )
 
 type serialPortList struct {
-	tools *tools.Tools
-
 	Ports     []*SpPortItem
 	portsLock sync.Mutex
 
+	tools  *tools.Tools `json:"-"`
 	OnList func([]byte) `json:"-"`
 	OnErr  func(string) `json:"-"`
 }

From 14d72e84f97bf26252002380d8447abc0b9fa153 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 16:21:05 +0200
Subject: [PATCH 34/50] refactor: clarify the impact of removing
 serialPorts.List() in writerNoBuf

---
 serialport.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/serialport.go b/serialport.go
index eef4410cc..00ff1ec90 100755
--- a/serialport.go
+++ b/serialport.go
@@ -241,8 +241,9 @@ func (p *serport) writerNoBuf() {
 
 	p.portIo.Close()
 
-	// The effect of this call is to send in to all the we client the list of serial ports
-	// TODO: investigate if this is superfluous and it can be removed.
+	// NOTE:  by removing the 'serialPorts.List()' line, the list of serial ports are NOT sent to the websocket clients.
+	// after a write is completed. It should not be an issue also because the other two 'writerBuffered' and 'writerRaw' methods
+	// do not call it.
 	// serialPorts.List()
 
 }

From 9e13dcb7fc50f23a946dbdae073d1a54eb64faf6 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 17:04:49 +0200
Subject: [PATCH 35/50] move serialportlist inside hub

---
 hub.go        | 6 ++++--
 main.go       | 6 +-----
 serialport.go | 7 ++++---
 3 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/hub.go b/hub.go
index 5d4b66fa2..14d25bf8c 100755
--- a/hub.go
+++ b/hub.go
@@ -60,7 +60,7 @@ type hub struct {
 	systray *systray.Systray
 }
 
-func newHub(serialList *serialPortList, tools *tools.Tools, systray *systray.Systray) *hub {
+func newHub(tools *tools.Tools, systray *systray.Systray) *hub {
 	hub := &hub{
 		broadcast:      make(chan []byte, 1000),
 		broadcastSys:   make(chan []byte, 1000),
@@ -68,7 +68,7 @@ func newHub(serialList *serialPortList, tools *tools.Tools, systray *systray.Sys
 		unregister:     make(chan *connection),
 		connections:    make(map[*connection]bool),
 		serialHub:      newSerialHub(),
-		serialPortList: serialList,
+		serialPortList: newSerialPortList(tools),
 		tools:          tools,
 		systray:        systray,
 	}
@@ -132,6 +132,8 @@ func (hub *hub) sendToRegisteredConnections(data []byte) {
 }
 
 func (hub *hub) run() {
+	go hub.serialPortList.Run()
+
 	for {
 		select {
 		case c := <-hub.register:
diff --git a/main.go b/main.go
index dfb217c26..51a26450e 100755
--- a/main.go
+++ b/main.go
@@ -176,9 +176,7 @@ func loop(stray *systray.Systray) {
 	}
 	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
 
-	serialPorts := newSerialPortList(tools)
-
-	hub := newHub(serialPorts, tools, stray)
+	hub := newHub(tools, stray)
 
 	// Let's handle the config
 	configDir := config.GetDefaultConfigDir()
@@ -394,8 +392,6 @@ func loop(stray *systray.Systray) {
 		}
 	}
 
-	// launch the discoveries for the running system
-	go serialPorts.Run()
 	// launch the hub routine which is the singleton for the websocket server
 	go hub.run()
 	// launch our dummy data routine
diff --git a/serialport.go b/serialport.go
index 00ff1ec90..4385a3e86 100755
--- a/serialport.go
+++ b/serialport.go
@@ -241,9 +241,10 @@ func (p *serport) writerNoBuf() {
 
 	p.portIo.Close()
 
-	// NOTE:  by removing the 'serialPorts.List()' line, the list of serial ports are NOT sent to the websocket clients.
-	// after a write is completed. It should not be an issue also because the other two 'writerBuffered' and 'writerRaw' methods
-	// do not call it.
+	// NOTE:  by removing the 'serialPorts.List()' line,
+	// the list of serial ports are NOT sent to the websocket clients  after a write is completed.
+	// This should not be an issue since the list are periodically called.
+	// Note also that the 'writerBuffered' and 'writerRaw' methods do not call it.
 	// serialPorts.List()
 
 }

From a69c0dcf44dfa1bcb1788209f73be5c7752b7d7e Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 2 Apr 2025 17:37:14 +0200
Subject: [PATCH 36/50] refactor on callback

---
 hub.go            | 46 ++++++++++++++++++++++++----------------------
 serialhub.go      | 14 ++++++++------
 serialportlist.go |  9 +++++++--
 3 files changed, 39 insertions(+), 30 deletions(-)

diff --git a/hub.go b/hub.go
index 14d25bf8c..46ee81429 100755
--- a/hub.go
+++ b/hub.go
@@ -61,35 +61,35 @@ type hub struct {
 }
 
 func newHub(tools *tools.Tools, systray *systray.Systray) *hub {
-	hub := &hub{
-		broadcast:      make(chan []byte, 1000),
-		broadcastSys:   make(chan []byte, 1000),
-		register:       make(chan *connection),
-		unregister:     make(chan *connection),
-		connections:    make(map[*connection]bool),
-		serialHub:      newSerialHub(),
-		serialPortList: newSerialPortList(tools),
-		tools:          tools,
-		systray:        systray,
-	}
+	broadcastSys := make(chan []byte, 1000)
 
-	hub.serialHub.OnRegister = func(port *serport) {
-		hub.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}")
+	onRegister := func(port *serport) {
+		broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}")
 	}
-
-	hub.serialHub.OnUnregister = func(port *serport) {
-		hub.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
+	onUnregister := func(port *serport) {
+		broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
 	}
+	serialHub := newSerialHub(onRegister, onUnregister)
 
-	hub.serialPortList.OnList = func(data []byte) {
-		hub.broadcastSys <- data
+	onList := func(data []byte) {
+		broadcastSys <- data
 	}
-
-	hub.serialPortList.OnErr = func(err string) {
-		hub.broadcastSys <- []byte("{\"Error\":\"" + err + "\"}")
+	onErr := func(err string) {
+		broadcastSys <- []byte("{\"Error\":\"" + err + "\"}")
 	}
+	serialPortList := newSerialPortList(tools, onList, onErr)
 
-	return hub
+	return &hub{
+		broadcast:      make(chan []byte, 1000),
+		broadcastSys:   broadcastSys,
+		register:       make(chan *connection),
+		unregister:     make(chan *connection),
+		connections:    make(map[*connection]bool),
+		serialHub:      serialHub,
+		serialPortList: serialPortList,
+		tools:          tools,
+		systray:        systray,
+	}
 }
 
 const commands = `{
@@ -216,6 +216,8 @@ func (hub *hub) checkCmd(m []byte) {
 		// will catch send and sendnobuf and sendraw
 		go hub.spWrite(s)
 	} else if strings.HasPrefix(sl, "list") {
+		// ports :=  hub.serialPortList.List()
+		// send to websockets the ports
 		go hub.serialPortList.List()
 	} else if strings.HasPrefix(sl, "downloadtool") {
 		go func() {
diff --git a/serialhub.go b/serialhub.go
index 6c00718e6..06a29003c 100755
--- a/serialhub.go
+++ b/serialhub.go
@@ -27,13 +27,15 @@ type serialhub struct {
 	ports map[*serport]bool
 	mu    sync.Mutex
 
-	OnRegister   func(port *serport)
-	OnUnregister func(port *serport)
+	onRegister   func(port *serport)
+	onUnregister func(port *serport)
 }
 
-func newSerialHub() *serialhub {
+func newSerialHub(onRegister func(port *serport), onUnregister func(port *serport)) *serialhub {
 	return &serialhub{
-		ports: make(map[*serport]bool),
+		ports:        make(map[*serport]bool),
+		onRegister:   onRegister,
+		onUnregister: onUnregister,
 	}
 }
 
@@ -41,7 +43,7 @@ func newSerialHub() *serialhub {
 func (sh *serialhub) Register(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Registering a port: ", p.portConf.Name)
-	sh.OnRegister(port)
+	sh.onRegister(port)
 	sh.ports[port] = true
 	sh.mu.Unlock()
 }
@@ -50,7 +52,7 @@ func (sh *serialhub) Register(port *serport) {
 func (sh *serialhub) Unregister(port *serport) {
 	sh.mu.Lock()
 	//log.Print("Unregistering a port: ", p.portConf.Name)
-	sh.OnUnregister(port)
+	sh.onUnregister(port)
 	delete(sh.ports, port)
 	close(port.sendBuffered)
 	close(port.sendNoBuf)
diff --git a/serialportlist.go b/serialportlist.go
index 1c38a49ee..9b1fdfac6 100644
--- a/serialportlist.go
+++ b/serialportlist.go
@@ -34,8 +34,12 @@ type SpPortItem struct {
 	ProductID       string
 }
 
-func newSerialPortList(tools *tools.Tools) *serialPortList {
-	return &serialPortList{tools: tools}
+func newSerialPortList(tools *tools.Tools, onList func(data []byte), onErr func(err string)) *serialPortList {
+	return &serialPortList{
+		tools:  tools,
+		OnList: onList,
+		OnErr:  onErr,
+	}
 }
 
 // List broadcasts a Json representation of the ports found
@@ -111,6 +115,7 @@ func (sp *serialPortList) runSerialDiscovery() {
 		logrus.Errorf("Error starting event watcher on serial-discovery: %s", err)
 		panic(err)
 	}
+	d.List()
 
 	logrus.Infof("Serial discovery started, watching for events")
 	for ev := range events {

From 7e340712d400f9137f66645a6fd283ed3fd745ba Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Thu, 3 Apr 2025 10:15:42 +0200
Subject: [PATCH 37/50] refactor: simplify hub initialization in upload handler
 tests

---
 main_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/main_test.go b/main_test.go
index bbede14fd..4b0720e0d 100644
--- a/main_test.go
+++ b/main_test.go
@@ -63,7 +63,7 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
 	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
 	require.NoError(t, err)
 	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
-	hub := newHub(newSerialPortList(tools), tools, &systray.Systray{})
+	hub := newHub(tools, &systray.Systray{})
 	pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))
 
 	r.POST("/", uploadHandler(hub, pubkey, tools))
@@ -107,7 +107,7 @@ func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) {
 	signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey))
 	require.NoError(t, err)
 	tools := tools.New(config.GetDataDir(), index, signaturePubKey)
-	hub := newHub(newSerialPortList(tools), tools, &systray.Systray{})
+	hub := newHub(tools, &systray.Systray{})
 	pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))
 
 	r.POST("/", uploadHandler(hub, pubkey, tools))

From 815a7916fecd0153e8f972fa02409099bd7e4183 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Thu, 3 Apr 2025 10:59:39 +0200
Subject: [PATCH 38/50] refactor: remove unused port listing in checkCmd
 function

---
 hub.go | 2 --
 1 file changed, 2 deletions(-)

diff --git a/hub.go b/hub.go
index 46ee81429..9d15240f7 100755
--- a/hub.go
+++ b/hub.go
@@ -216,8 +216,6 @@ func (hub *hub) checkCmd(m []byte) {
 		// will catch send and sendnobuf and sendraw
 		go hub.spWrite(s)
 	} else if strings.HasPrefix(sl, "list") {
-		// ports :=  hub.serialPortList.List()
-		// send to websockets the ports
 		go hub.serialPortList.List()
 	} else if strings.HasPrefix(sl, "downloadtool") {
 		go func() {

From 9535905763ff78a6b43e2fef25a05c0d7754e11e Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:21:52 +0200
Subject: [PATCH 39/50] rename into `h` and revert split of serial.go

---
 hub.go                         | 171 ++++++++++++++++-----------------
 serialportlist.go => serial.go | 140 +++++++++++++++++++++------
 serialhub.go                   |  74 --------------
 3 files changed, 194 insertions(+), 191 deletions(-)
 rename serialportlist.go => serial.go (58%)
 delete mode 100755 serialhub.go

diff --git a/hub.go b/hub.go
index 9d15240f7..0e9ade88d 100755
--- a/hub.go
+++ b/hub.go
@@ -53,7 +53,7 @@ type hub struct {
 	// Serial hub to communicate with serial ports
 	serialHub *serialhub
 
-	serialPortList *serialPortList
+	serialPortList *SerialPortList
 
 	tools *tools.Tools
 
@@ -69,7 +69,7 @@ func newHub(tools *tools.Tools, systray *systray.Systray) *hub {
 	onUnregister := func(port *serport) {
 		broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
 	}
-	serialHub := newSerialHub(onRegister, onUnregister)
+	serialHubub := newSerialHub(onRegister, onUnregister)
 
 	onList := func(data []byte) {
 		broadcastSys <- data
@@ -85,7 +85,7 @@ func newHub(tools *tools.Tools, systray *systray.Systray) *hub {
 		register:       make(chan *connection),
 		unregister:     make(chan *connection),
 		connections:    make(map[*connection]bool),
-		serialHub:      serialHub,
+		serialHub:      serialHubub,
 		serialPortList: serialPortList,
 		tools:          tools,
 		systray:        systray,
@@ -110,53 +110,53 @@ const commands = `{
   ]
 }`
 
-func (hub *hub) unregisterConnection(c *connection) {
-	if _, contains := hub.connections[c]; !contains {
+func (h *hub) unregisterConnection(c *connection) {
+	if _, contains := h.connections[c]; !contains {
 		return
 	}
-	delete(hub.connections, c)
+	delete(h.connections, c)
 	close(c.send)
 }
 
-func (hub *hub) sendToRegisteredConnections(data []byte) {
-	for c := range hub.connections {
+func (h *hub) sendToRegisteredConnections(data []byte) {
+	for c := range h.connections {
 		select {
 		case c.send <- data:
 			//log.Print("did broadcast to ")
 			//log.Print(c.ws.RemoteAddr())
 			//c.send <- []byte("hello world")
 		default:
-			hub.unregisterConnection(c)
+			h.unregisterConnection(c)
 		}
 	}
 }
 
-func (hub *hub) run() {
-	go hub.serialPortList.Run()
+func (h *hub) run() {
+	go h.serialPortList.Run()
 
 	for {
 		select {
-		case c := <-hub.register:
-			hub.connections[c] = true
+		case c := <-h.register:
+			h.connections[c] = true
 			// send supported commands
 			c.send <- []byte(fmt.Sprintf(`{"Version" : "%s"} `, version))
 			c.send <- []byte(html.EscapeString(commands))
 			c.send <- []byte(fmt.Sprintf(`{"Hostname" : "%s"} `, *hostname))
 			c.send <- []byte(fmt.Sprintf(`{"OS" : "%s"} `, runtime.GOOS))
-		case c := <-hub.unregister:
-			hub.unregisterConnection(c)
-		case m := <-hub.broadcast:
+		case c := <-h.unregister:
+			h.unregisterConnection(c)
+		case m := <-h.broadcast:
 			if len(m) > 0 {
-				hub.checkCmd(m)
-				hub.sendToRegisteredConnections(m)
+				h.checkCmd(m)
+				h.sendToRegisteredConnections(m)
 			}
-		case m := <-hub.broadcastSys:
-			hub.sendToRegisteredConnections(m)
+		case m := <-h.broadcastSys:
+			h.sendToRegisteredConnections(m)
 		}
 	}
 }
 
-func (hub *hub) checkCmd(m []byte) {
+func (h *hub) checkCmd(m []byte) {
 	//log.Print("Inside checkCmd")
 	s := string(m[:])
 
@@ -171,18 +171,18 @@ func (hub *hub) checkCmd(m []byte) {
 
 		args := strings.Split(s, " ")
 		if len(args) < 3 {
-			go hub.spErr("You did not specify a port and baud rate in your open cmd")
+			go h.spErr("You did not specify a port and baud rate in your open cmd")
 			return
 		}
 		if len(args[1]) < 1 {
-			go hub.spErr("You did not specify a serial port")
+			go h.spErr("You did not specify a serial port")
 			return
 		}
 
 		baudStr := strings.Replace(args[2], "\n", "", -1)
 		baud, err := strconv.Atoi(baudStr)
 		if err != nil {
-			go hub.spErr("Problem converting baud rate " + args[2])
+			go h.spErr("Problem converting baud rate " + args[2])
 			return
 		}
 		// pass in buffer type now as string. if user does not
@@ -193,30 +193,30 @@ func (hub *hub) checkCmd(m []byte) {
 			buftype := strings.Replace(args[3], "\n", "", -1)
 			bufferAlgorithm = buftype
 		}
-		go hub.spHandlerOpen(args[1], baud, bufferAlgorithm)
+		go h.spHandlerOpen(args[1], baud, bufferAlgorithm)
 
 	} else if strings.HasPrefix(sl, "close") {
 
 		args := strings.Split(s, " ")
 		if len(args) > 1 {
-			go hub.spClose(args[1])
+			go h.spClose(args[1])
 		} else {
-			go hub.spErr("You did not specify a port to close")
+			go h.spErr("You did not specify a port to close")
 		}
 
 	} else if strings.HasPrefix(sl, "killupload") {
 		// kill the running process (assumes singleton for now)
 		go func() {
 			upload.Kill()
-			hub.broadcastSys <- []byte("{\"uploadStatus\": \"Killed\"}")
+			h.broadcastSys <- []byte("{\"uploadStatus\": \"Killed\"}")
 			log.Println("{\"uploadStatus\": \"Killed\"}")
 		}()
 
 	} else if strings.HasPrefix(sl, "send") {
 		// will catch send and sendnobuf and sendraw
-		go hub.spWrite(s)
+		go h.spWrite(s)
 	} else if strings.HasPrefix(sl, "list") {
-		go hub.serialPortList.List()
+		go h.serialPortList.List()
 	} else if strings.HasPrefix(sl, "downloadtool") {
 		go func() {
 			args := strings.Split(s, " ")
@@ -227,7 +227,7 @@ func (hub *hub) checkCmd(m []byte) {
 			if len(args) <= 1 {
 				mapD := map[string]string{"DownloadStatus": "Error", "Msg": "Not enough arguments"}
 				mapB, _ := json.Marshal(mapD)
-				hub.broadcastSys <- mapB
+				h.broadcastSys <- mapB
 				return
 			}
 			if len(args) > 1 {
@@ -250,37 +250,37 @@ func (hub *hub) checkCmd(m []byte) {
 			reportPendingProgress := func(msg string) {
 				mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg}
 				mapB, _ := json.Marshal(mapD)
-				hub.broadcastSys <- mapB
+				h.broadcastSys <- mapB
 			}
-			err := hub.tools.Download(pack, tool, toolVersion, behaviour, reportPendingProgress)
+			err := h.tools.Download(pack, tool, toolVersion, behaviour, reportPendingProgress)
 			if err != nil {
 				mapD := map[string]string{"DownloadStatus": "Error", "Msg": err.Error()}
 				mapB, _ := json.Marshal(mapD)
-				hub.broadcastSys <- mapB
+				h.broadcastSys <- mapB
 			} else {
 				mapD := map[string]string{"DownloadStatus": "Success", "Msg": "Map Updated"}
 				mapB, _ := json.Marshal(mapD)
-				hub.broadcastSys <- mapB
+				h.broadcastSys <- mapB
 			}
 		}()
 	} else if strings.HasPrefix(sl, "log") {
-		go hub.logAction(sl)
+		go h.logAction(sl)
 	} else if strings.HasPrefix(sl, "restart") {
-		// potentially, the sysStray dependencies can be removed  https://github.com/arduino/arduino-create-agent/issues/1013
+		// potentially, the sysStray dependencies can be removed  https://gith.com/arduino/arduino-create-agent/issues/1013
 		log.Println("Received restart from the daemon. Why? Boh")
-		hub.systray.Restart()
+		h.systray.Restart()
 	} else if strings.HasPrefix(sl, "exit") {
-		hub.systray.Quit()
+		h.systray.Quit()
 	} else if strings.HasPrefix(sl, "memstats") {
-		hub.memoryStats()
+		h.memoryStats()
 	} else if strings.HasPrefix(sl, "gc") {
-		hub.garbageCollection()
+		h.garbageCollection()
 	} else if strings.HasPrefix(sl, "hostname") {
-		hub.getHostname()
+		h.getHostname()
 	} else if strings.HasPrefix(sl, "version") {
-		hub.getVersion()
+		h.getVersion()
 	} else {
-		go hub.spErr("Could not understand command.")
+		go h.spErr("Could not understand command.")
 	}
 }
 
@@ -288,13 +288,13 @@ type logWriter struct {
 	onWrite func([]byte)
 }
 
-func (hub *hub) logAction(sl string) {
+func (h *hub) logAction(sl string) {
 	if strings.HasPrefix(sl, "log on") {
 		*logDump = "on"
 
 		logWriter := logWriter{}
 		logWriter.onWrite = func(p []byte) {
-			hub.broadcastSys <- p
+			h.broadcastSys <- p
 		}
 
 		multiWriter := io.MultiWriter(&logWriter, os.Stderr)
@@ -304,7 +304,7 @@ func (hub *hub) logAction(sl string) {
 		log.SetOutput(os.Stderr)
 		// } else if strings.HasPrefix(sl, "log show") {
 		// TODO: send all the saved log to websocket
-		//hub.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *logDump + "\"}")
+		//h.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *logDump + "\"}")
 	}
 }
 
@@ -313,35 +313,35 @@ func (u *logWriter) Write(p []byte) (n int, err error) {
 	return len(p), nil
 }
 
-func (hub *hub) memoryStats() {
+func (h *hub) memoryStats() {
 	var memStats runtime.MemStats
 	runtime.ReadMemStats(&memStats)
 	json, _ := json.Marshal(memStats)
 	log.Printf("memStats:%v\n", string(json))
-	hub.broadcastSys <- json
+	h.broadcastSys <- json
 }
 
-func (hub *hub) getHostname() {
-	hub.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
+func (h *hub) getHostname() {
+	h.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}")
 }
 
-func (hub *hub) getVersion() {
-	hub.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
+func (h *hub) getVersion() {
+	h.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}")
 }
 
-func (hub *hub) garbageCollection() {
+func (h *hub) garbageCollection() {
 	log.Printf("Starting garbageCollection()\n")
-	hub.broadcastSys <- []byte("{\"gc\":\"starting\"}")
-	hub.memoryStats()
+	h.broadcastSys <- []byte("{\"gc\":\"starting\"}")
+	h.memoryStats()
 	debug.SetGCPercent(100)
 	debug.FreeOSMemory()
 	debug.SetGCPercent(-1)
 	log.Printf("Done with garbageCollection()\n")
-	hub.broadcastSys <- []byte("{\"gc\":\"done\"}")
-	hub.memoryStats()
+	h.broadcastSys <- []byte("{\"gc\":\"done\"}")
+	h.memoryStats()
 }
 
-func (hub *hub) spHandlerOpen(portname string, baud int, buftype string) {
+func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	log.Print("Inside spHandler")
 
@@ -365,8 +365,8 @@ func (hub *hub) spHandlerOpen(portname string, baud int, buftype string) {
 	if err != nil {
 		//log.Fatal(err)
 		log.Print("Error opening port " + err.Error())
-		//hub.broadcastSys <- []byte("Error opening port. " + err.Error())
-		hub.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}")
+		//h.broadcastSys <- []byte("Error opening port. " + err.Error())
+		h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}")
 
 		return
 	}
@@ -384,22 +384,22 @@ func (hub *hub) spHandlerOpen(portname string, baud int, buftype string) {
 	}
 
 	p.OnMessage = func(msg []byte) {
-		hub.broadcastSys <- msg
+		h.broadcastSys <- msg
 	}
 	p.OnClose = func(port *serport) {
-		hub.serialPortList.MarkPortAsClosed(p.portName)
-		hub.serialPortList.List()
+		h.serialPortList.MarkPortAsClosed(p.portName)
+		h.serialPortList.List()
 	}
 
 	var bw Bufferflow
 
 	switch buftype {
 	case "timed":
-		bw = NewBufferflowTimed(portname, hub.broadcastSys)
+		bw = NewBufferflowTimed(portname, h.broadcastSys)
 	case "timedraw":
-		bw = NewBufferflowTimedRaw(portname, hub.broadcastSys)
+		bw = NewBufferflowTimedRaw(portname, h.broadcastSys)
 	case "default":
-		bw = NewBufferflowDefault(portname, hub.broadcastSys)
+		bw = NewBufferflowDefault(portname, h.broadcastSys)
 	default:
 		log.Panicf("unknown buffer type: %s", buftype)
 	}
@@ -407,11 +407,11 @@ func (hub *hub) spHandlerOpen(portname string, baud int, buftype string) {
 	bw.Init()
 	p.bufferwatcher = bw
 
-	hub.serialHub.Register(p)
-	defer hub.serialHub.Unregister(p)
+	h.serialHub.Register(p)
+	defer h.serialHub.Unregister(p)
 
-	hub.serialPortList.MarkPortAsOpened(portname)
-	hub.serialPortList.List()
+	h.serialPortList.MarkPortAsOpened(portname)
+	h.serialPortList.List()
 
 	// this is internally buffered thread to not send to serial port if blocked
 	go p.writerBuffered()
@@ -422,19 +422,23 @@ func (hub *hub) spHandlerOpen(portname string, baud int, buftype string) {
 
 	p.reader(buftype)
 
-	hub.serialPortList.List()
+	h.serialPortList.List()
 }
 
-func (hub *hub) spClose(portname string) {
-	if myport, ok := hub.serialHub.FindPortByName(portname); ok {
-		hub.broadcastSys <- []byte("Closing serial port " + portname)
+func (h *hub) spErr(err string) {
+	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
+}
+
+func (h *hub) spClose(portname string) {
+	if myport, ok := h.serialHub.FindPortByName(portname); ok {
+		h.broadcastSys <- []byte("Closing serial port " + portname)
 		myport.Close()
 	} else {
-		hub.spErr("We could not find the serial port " + portname + " that you were trying to close.")
+		h.spErr("We could not find the serial port " + portname + " that you were trying to close.")
 	}
 }
 
-func (hub *hub) spWrite(arg string) {
+func (h *hub) spWrite(arg string) {
 	// we will get a string of comXX asdf asdf asdf
 	//log.Println("Inside spWrite arg: " + arg)
 	arg = strings.TrimPrefix(arg, " ")
@@ -443,7 +447,7 @@ func (hub *hub) spWrite(arg string) {
 	if len(args) != 3 {
 		errstr := "Could not parse send command: " + arg
 		//log.Println(errstr)
-		hub.spErr(errstr)
+		h.spErr(errstr)
 		return
 	}
 	bufferingMode := args[0]
@@ -454,10 +458,10 @@ func (hub *hub) spWrite(arg string) {
 	//log.Println("The data is:" + data + "---")
 
 	// See if we have this port open
-	port, ok := hub.serialHub.FindPortByName(portname)
+	port, ok := h.serialHub.FindPortByName(portname)
 	if !ok {
 		// we couldn't find the port, so send err
-		hub.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
+		h.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
 		return
 	}
 
@@ -466,15 +470,10 @@ func (hub *hub) spWrite(arg string) {
 	case "send", "sendnobuf", "sendraw":
 		// valid buffering mode, go ahead
 	default:
-		hub.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
+		h.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
 		return
 	}
 
 	// send it to the write channel
 	port.Write(data, bufferingMode)
 }
-
-func (hub *hub) spErr(err string) {
-	//log.Println("Sending err back: ", err)
-	hub.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
-}
diff --git a/serialportlist.go b/serial.go
similarity index 58%
rename from serialportlist.go
rename to serial.go
index 9b1fdfac6..845d4fa94 100644
--- a/serialportlist.go
+++ b/serial.go
@@ -1,8 +1,26 @@
+// Copyright 2022 Arduino SA
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+// Supports Windows, Linux, Mac, BeagleBone Black, and Raspberry Pi
+
 package main
 
 import (
 	"encoding/json"
 	"slices"
+	"strings"
 	"sync"
 	"time"
 
@@ -11,7 +29,26 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-type serialPortList struct {
+type serialhub struct {
+	// Opened serial ports.
+	ports map[*serport]bool
+
+	mu sync.Mutex
+
+	onRegister   func(port *serport)
+	onUnregister func(port *serport)
+}
+
+func newSerialHub(onRegister func(port *serport), onUnregister func(port *serport)) *serialhub {
+	return &serialhub{
+		ports:        make(map[*serport]bool),
+		onRegister:   onRegister,
+		onUnregister: onUnregister,
+	}
+}
+
+// SerialPortList is the serial port list
+type SerialPortList struct {
 	Ports     []*SpPortItem
 	portsLock sync.Mutex
 
@@ -34,8 +71,50 @@ type SpPortItem struct {
 	ProductID       string
 }
 
-func newSerialPortList(tools *tools.Tools, onList func(data []byte), onErr func(err string)) *serialPortList {
-	return &serialPortList{
+// serialPorts contains the ports attached to the machine
+var serialPorts SerialPortList
+
+var sh = serialhub{
+	ports: make(map[*serport]bool),
+}
+
+// Register serial ports from the connections.
+func (sh *serialhub) Register(port *serport) {
+	sh.mu.Lock()
+	sh.onRegister(port)
+	// sh.h.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}")
+	sh.ports[port] = true
+	sh.mu.Unlock()
+}
+
+// Unregister requests from connections.
+func (sh *serialhub) Unregister(port *serport) {
+	sh.mu.Lock()
+	//log.Print("Unregistering a port: ", p.portConf.Name)
+	// h.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
+	sh.onUnregister(port)
+	delete(sh.ports, port)
+	close(port.sendBuffered)
+	close(port.sendNoBuf)
+	sh.mu.Unlock()
+}
+
+func (sh *serialhub) FindPortByName(portname string) (*serport, bool) {
+	sh.mu.Lock()
+	defer sh.mu.Unlock()
+
+	for port := range sh.ports {
+		if strings.EqualFold(port.portConf.Name, portname) {
+			// we found our port
+			//spHandlerClose(port)
+			return port, true
+		}
+	}
+	return nil, false
+}
+
+func newSerialPortList(tools *tools.Tools, onList func(data []byte), onErr func(err string)) *SerialPortList {
+	return &SerialPortList{
 		tools:  tools,
 		OnList: onList,
 		OnErr:  onErr,
@@ -43,7 +122,7 @@ func newSerialPortList(tools *tools.Tools, onList func(data []byte), onErr func(
 }
 
 // List broadcasts a Json representation of the ports found
-func (sp *serialPortList) List() {
+func (sp *SerialPortList) List() {
 	sp.portsLock.Lock()
 	ls, err := json.MarshalIndent(sp, "", "\t")
 	sp.portsLock.Unlock()
@@ -55,28 +134,8 @@ func (sp *serialPortList) List() {
 	}
 }
 
-// MarkPortAsOpened marks a port as opened by the user
-func (sp *serialPortList) MarkPortAsOpened(portname string) {
-	sp.portsLock.Lock()
-	defer sp.portsLock.Unlock()
-	port := sp.getPortByName(portname)
-	if port != nil {
-		port.IsOpen = true
-	}
-}
-
-// MarkPortAsClosed marks a port as no more opened by the user
-func (sp *serialPortList) MarkPortAsClosed(portname string) {
-	sp.portsLock.Lock()
-	defer sp.portsLock.Unlock()
-	port := sp.getPortByName(portname)
-	if port != nil {
-		port.IsOpen = false
-	}
-}
-
 // Run is the main loop for port discovery and management
-func (sp *serialPortList) Run() {
+func (sp *SerialPortList) Run() {
 	for retries := 0; retries < 10; retries++ {
 		sp.runSerialDiscovery()
 
@@ -86,7 +145,7 @@ func (sp *serialPortList) Run() {
 	logrus.Errorf("Failed restarting serial discovery. Giving up...")
 }
 
-func (sp *serialPortList) runSerialDiscovery() {
+func (sp *SerialPortList) runSerialDiscovery() {
 	// First ensure that all the discoveries are available
 	noOpProgress := func(msg string) {}
 	if err := sp.tools.Download("builtin", "serial-discovery", "latest", "keep", noOpProgress); err != nil {
@@ -115,7 +174,6 @@ func (sp *serialPortList) runSerialDiscovery() {
 		logrus.Errorf("Error starting event watcher on serial-discovery: %s", err)
 		panic(err)
 	}
-	d.List()
 
 	logrus.Infof("Serial discovery started, watching for events")
 	for ev := range events {
@@ -132,13 +190,13 @@ func (sp *serialPortList) runSerialDiscovery() {
 	logrus.Errorf("Serial discovery stopped.")
 }
 
-func (sp *serialPortList) reset() {
+func (sp *SerialPortList) reset() {
 	sp.portsLock.Lock()
 	defer sp.portsLock.Unlock()
 	sp.Ports = []*SpPortItem{}
 }
 
-func (sp *serialPortList) add(addedPort *discovery.Port) {
+func (sp *SerialPortList) add(addedPort *discovery.Port) {
 	if addedPort.Protocol != "serial" {
 		return
 	}
@@ -181,7 +239,7 @@ func (sp *serialPortList) add(addedPort *discovery.Port) {
 	})
 }
 
-func (sp *serialPortList) remove(removedPort *discovery.Port) {
+func (sp *SerialPortList) remove(removedPort *discovery.Port) {
 	sp.portsLock.Lock()
 	defer sp.portsLock.Unlock()
 
@@ -191,7 +249,27 @@ func (sp *serialPortList) remove(removedPort *discovery.Port) {
 	})
 }
 
-func (sp *serialPortList) getPortByName(portname string) *SpPortItem {
+// MarkPortAsOpened marks a port as opened by the user
+func (sp *SerialPortList) MarkPortAsOpened(portname string) {
+	sp.portsLock.Lock()
+	defer sp.portsLock.Unlock()
+	port := sp.getPortByName(portname)
+	if port != nil {
+		port.IsOpen = true
+	}
+}
+
+// MarkPortAsClosed marks a port as no more opened by the user
+func (sp *SerialPortList) MarkPortAsClosed(portname string) {
+	sp.portsLock.Lock()
+	defer sp.portsLock.Unlock()
+	port := sp.getPortByName(portname)
+	if port != nil {
+		port.IsOpen = false
+	}
+}
+
+func (sp *SerialPortList) getPortByName(portname string) *SpPortItem {
 	for _, port := range sp.Ports {
 		if port.Name == portname {
 			return port
diff --git a/serialhub.go b/serialhub.go
deleted file mode 100755
index 06a29003c..000000000
--- a/serialhub.go
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2022 Arduino SA
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published
-// by the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-// Supports Windows, Linux, Mac, BeagleBone Black, and Raspberry Pi
-
-package main
-
-import (
-	"strings"
-	"sync"
-)
-
-type serialhub struct {
-	// Opened serial ports.
-	ports map[*serport]bool
-	mu    sync.Mutex
-
-	onRegister   func(port *serport)
-	onUnregister func(port *serport)
-}
-
-func newSerialHub(onRegister func(port *serport), onUnregister func(port *serport)) *serialhub {
-	return &serialhub{
-		ports:        make(map[*serport]bool),
-		onRegister:   onRegister,
-		onUnregister: onUnregister,
-	}
-}
-
-// Register serial ports from the connections.
-func (sh *serialhub) Register(port *serport) {
-	sh.mu.Lock()
-	//log.Print("Registering a port: ", p.portConf.Name)
-	sh.onRegister(port)
-	sh.ports[port] = true
-	sh.mu.Unlock()
-}
-
-// Unregister requests from connections.
-func (sh *serialhub) Unregister(port *serport) {
-	sh.mu.Lock()
-	//log.Print("Unregistering a port: ", p.portConf.Name)
-	sh.onUnregister(port)
-	delete(sh.ports, port)
-	close(port.sendBuffered)
-	close(port.sendNoBuf)
-	sh.mu.Unlock()
-}
-
-func (sh *serialhub) FindPortByName(portname string) (*serport, bool) {
-	sh.mu.Lock()
-	defer sh.mu.Unlock()
-
-	for port := range sh.ports {
-		if strings.EqualFold(port.portConf.Name, portname) {
-			// we found our port
-			//spHandlerClose(port)
-			return port, true
-		}
-	}
-	return nil, false
-}

From 0699e93365a8908a51bf226e0db2a9f23ceabd73 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:31:19 +0200
Subject: [PATCH 40/50] refactor: rename hub parameter in send and wsHandler
 functions for clarity

---
 conn.go | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/conn.go b/conn.go
index 42b573bb5..75486bf9f 100644
--- a/conn.go
+++ b/conn.go
@@ -211,12 +211,12 @@ func (l PLogger) Info(args ...interface{}) {
 	send(l.hub, map[string]string{uploadStatusStr: "Busy", "Msg": output})
 }
 
-func send(hub *hub, args map[string]string) {
+func send(h *hub, args map[string]string) {
 	mapB, _ := json.Marshal(args)
-	hub.broadcastSys <- mapB
+	h.broadcastSys <- mapB
 }
 
-func wsHandler(hub *hub) *WsServer {
+func wsHandler(h *hub) *WsServer {
 	server, err := socketio.NewServer(nil)
 	if err != nil {
 		log.Fatal(err)
@@ -224,13 +224,13 @@ func wsHandler(hub *hub) *WsServer {
 
 	server.On("connection", func(so socketio.Socket) {
 		c := &connection{send: make(chan []byte, 256*10), ws: so}
-		hub.register <- c
+		h.register <- c
 		so.On("command", func(message string) {
-			hub.broadcast <- []byte(message)
+			h.broadcast <- []byte(message)
 		})
 
 		so.On("disconnection", func() {
-			hub.unregister <- c
+			h.unregister <- c
 		})
 		go c.writer()
 	})

From 062bca0217549009be1ef63972f7e1be688a62f9 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:33:29 +0200
Subject: [PATCH 41/50] refactor: move spHandlerOpen function from
 serialport.go to hub.go for better organization

---
 hub.go        | 86 ---------------------------------------------------
 serialport.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 86 deletions(-)

diff --git a/hub.go b/hub.go
index 0e9ade88d..32c5f867c 100755
--- a/hub.go
+++ b/hub.go
@@ -16,7 +16,6 @@
 package main
 
 import (
-	"bytes"
 	"encoding/json"
 	"fmt"
 	"html"
@@ -31,7 +30,6 @@ import (
 	"github.com/arduino/arduino-create-agent/tools"
 	"github.com/arduino/arduino-create-agent/upload"
 	log "github.com/sirupsen/logrus"
-	"go.bug.st/serial"
 )
 
 type hub struct {
@@ -341,90 +339,6 @@ func (h *hub) garbageCollection() {
 	h.memoryStats()
 }
 
-func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
-
-	log.Print("Inside spHandler")
-
-	var out bytes.Buffer
-
-	out.WriteString("Opening serial port ")
-	out.WriteString(portname)
-	out.WriteString(" at ")
-	out.WriteString(strconv.Itoa(baud))
-	out.WriteString(" baud")
-	log.Print(out.String())
-
-	conf := &SerialConfig{Name: portname, Baud: baud, RtsOn: true}
-
-	mode := &serial.Mode{
-		BaudRate: baud,
-	}
-
-	sp, err := serial.Open(portname, mode)
-	log.Print("Just tried to open port")
-	if err != nil {
-		//log.Fatal(err)
-		log.Print("Error opening port " + err.Error())
-		//h.broadcastSys <- []byte("Error opening port. " + err.Error())
-		h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}")
-
-		return
-	}
-	log.Print("Opened port successfully")
-	//p := &serport{send: make(chan []byte, 256), portConf: conf, portIo: sp}
-	// we can go up to 256,000 lines of gcode in the buffer
-	p := &serport{
-		sendBuffered: make(chan string, 256000),
-		sendNoBuf:    make(chan []byte),
-		sendRaw:      make(chan string),
-		portConf:     conf,
-		portIo:       sp,
-		portName:     portname,
-		BufferType:   buftype,
-	}
-
-	p.OnMessage = func(msg []byte) {
-		h.broadcastSys <- msg
-	}
-	p.OnClose = func(port *serport) {
-		h.serialPortList.MarkPortAsClosed(p.portName)
-		h.serialPortList.List()
-	}
-
-	var bw Bufferflow
-
-	switch buftype {
-	case "timed":
-		bw = NewBufferflowTimed(portname, h.broadcastSys)
-	case "timedraw":
-		bw = NewBufferflowTimedRaw(portname, h.broadcastSys)
-	case "default":
-		bw = NewBufferflowDefault(portname, h.broadcastSys)
-	default:
-		log.Panicf("unknown buffer type: %s", buftype)
-	}
-
-	bw.Init()
-	p.bufferwatcher = bw
-
-	h.serialHub.Register(p)
-	defer h.serialHub.Unregister(p)
-
-	h.serialPortList.MarkPortAsOpened(portname)
-	h.serialPortList.List()
-
-	// this is internally buffered thread to not send to serial port if blocked
-	go p.writerBuffered()
-	// this is thread to send to serial port regardless of block
-	go p.writerNoBuf()
-	// this is thread to send to serial port but with base64 decoding
-	go p.writerRaw()
-
-	p.reader(buftype)
-
-	h.serialPortList.List()
-}
-
 func (h *hub) spErr(err string) {
 	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
 }
diff --git a/serialport.go b/serialport.go
index 4385a3e86..5c8ed5c55 100755
--- a/serialport.go
+++ b/serialport.go
@@ -25,6 +25,7 @@ import (
 	"unicode/utf8"
 
 	log "github.com/sirupsen/logrus"
+	"go.bug.st/serial"
 )
 
 // SerialConfig is the serial port configuration
@@ -281,6 +282,91 @@ func (p *serport) writerRaw() {
 	p.OnMessage([]byte(msgstr))
 }
 
+// FIXME: move this into the `hub.go` file
+func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
+
+	log.Print("Inside spHandler")
+
+	var out bytes.Buffer
+
+	out.WriteString("Opening serial port ")
+	out.WriteString(portname)
+	out.WriteString(" at ")
+	out.WriteString(strconv.Itoa(baud))
+	out.WriteString(" baud")
+	log.Print(out.String())
+
+	conf := &SerialConfig{Name: portname, Baud: baud, RtsOn: true}
+
+	mode := &serial.Mode{
+		BaudRate: baud,
+	}
+
+	sp, err := serial.Open(portname, mode)
+	log.Print("Just tried to open port")
+	if err != nil {
+		//log.Fatal(err)
+		log.Print("Error opening port " + err.Error())
+		//h.broadcastSys <- []byte("Error opening port. " + err.Error())
+		h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}")
+
+		return
+	}
+	log.Print("Opened port successfully")
+	//p := &serport{send: make(chan []byte, 256), portConf: conf, portIo: sp}
+	// we can go up to 256,000 lines of gcode in the buffer
+	p := &serport{
+		sendBuffered: make(chan string, 256000),
+		sendNoBuf:    make(chan []byte),
+		sendRaw:      make(chan string),
+		portConf:     conf,
+		portIo:       sp,
+		portName:     portname,
+		BufferType:   buftype,
+	}
+
+	p.OnMessage = func(msg []byte) {
+		h.broadcastSys <- msg
+	}
+	p.OnClose = func(port *serport) {
+		h.serialPortList.MarkPortAsClosed(p.portName)
+		h.serialPortList.List()
+	}
+
+	var bw Bufferflow
+
+	switch buftype {
+	case "timed":
+		bw = NewBufferflowTimed(portname, h.broadcastSys)
+	case "timedraw":
+		bw = NewBufferflowTimedRaw(portname, h.broadcastSys)
+	case "default":
+		bw = NewBufferflowDefault(portname, h.broadcastSys)
+	default:
+		log.Panicf("unknown buffer type: %s", buftype)
+	}
+
+	bw.Init()
+	p.bufferwatcher = bw
+
+	h.serialHub.Register(p)
+	defer h.serialHub.Unregister(p)
+
+	h.serialPortList.MarkPortAsOpened(portname)
+	h.serialPortList.List()
+
+	// this is internally buffered thread to not send to serial port if blocked
+	go p.writerBuffered()
+	// this is thread to send to serial port regardless of block
+	go p.writerNoBuf()
+	// this is thread to send to serial port but with base64 decoding
+	go p.writerRaw()
+
+	p.reader(buftype)
+
+	h.serialPortList.List()
+}
+
 func (p *serport) Close() {
 	p.isClosing.Store(true)
 

From 2bcc06d4695f5da5e237b9e995c8102aab92fb77 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:39:25 +0200
Subject: [PATCH 42/50] remove comment

---
 serial.go | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/serial.go b/serial.go
index 845d4fa94..07e0451c7 100644
--- a/serial.go
+++ b/serial.go
@@ -82,7 +82,6 @@ var sh = serialhub{
 func (sh *serialhub) Register(port *serport) {
 	sh.mu.Lock()
 	sh.onRegister(port)
-	// sh.h.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}")
 	sh.ports[port] = true
 	sh.mu.Unlock()
 }
@@ -90,8 +89,6 @@ func (sh *serialhub) Register(port *serport) {
 // Unregister requests from connections.
 func (sh *serialhub) Unregister(port *serport) {
 	sh.mu.Lock()
-	//log.Print("Unregistering a port: ", p.portConf.Name)
-	// h.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}")
 	sh.onUnregister(port)
 	delete(sh.ports, port)
 	close(port.sendBuffered)

From 4da1624864d86d0bab985fc8ec5a276777d2eab9 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:42:02 +0200
Subject: [PATCH 43/50] refactor: remove unused serialPorts variable and
 instance of serialhub for cleaner code

---
 serial.go | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/serial.go b/serial.go
index 07e0451c7..d198fd5f7 100644
--- a/serial.go
+++ b/serial.go
@@ -71,13 +71,6 @@ type SpPortItem struct {
 	ProductID       string
 }
 
-// serialPorts contains the ports attached to the machine
-var serialPorts SerialPortList
-
-var sh = serialhub{
-	ports: make(map[*serport]bool),
-}
-
 // Register serial ports from the connections.
 func (sh *serialhub) Register(port *serport) {
 	sh.mu.Lock()

From effe50d1bfc22064218c62126348b7de718f420a Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:43:51 +0200
Subject: [PATCH 44/50] remove commented-out spHandlerClose call in
 FindPortByName for cleaner code

---
 serial.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/serial.go b/serial.go
index d198fd5f7..f9930f9e6 100644
--- a/serial.go
+++ b/serial.go
@@ -96,7 +96,6 @@ func (sh *serialhub) FindPortByName(portname string) (*serport, bool) {
 	for port := range sh.ports {
 		if strings.EqualFold(port.portConf.Name, portname) {
 			// we found our port
-			//spHandlerClose(port)
 			return port, true
 		}
 	}

From d68fc7a9e61296a484d71448156de0bb66503e69 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:45:18 +0200
Subject: [PATCH 45/50] fix: correct GitHub link in comment for clarity

---
 hub.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hub.go b/hub.go
index 32c5f867c..66266046b 100755
--- a/hub.go
+++ b/hub.go
@@ -264,7 +264,7 @@ func (h *hub) checkCmd(m []byte) {
 	} else if strings.HasPrefix(sl, "log") {
 		go h.logAction(sl)
 	} else if strings.HasPrefix(sl, "restart") {
-		// potentially, the sysStray dependencies can be removed  https://gith.com/arduino/arduino-create-agent/issues/1013
+		// potentially, the sysStray dependencies can be removed  https://github.com/arduino/arduino-create-agent/issues/1013
 		log.Println("Received restart from the daemon. Why? Boh")
 		h.systray.Restart()
 	} else if strings.HasPrefix(sl, "exit") {

From fd3b2d72af38ae5fe3651c2191250814bf4f665a Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:50:41 +0200
Subject: [PATCH 46/50] refactor: replace logWriter with ChanWriter for
 improved logging mechanism

---
 hub.go | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/hub.go b/hub.go
index 66266046b..0a8f1091f 100755
--- a/hub.go
+++ b/hub.go
@@ -282,20 +282,21 @@ func (h *hub) checkCmd(m []byte) {
 	}
 }
 
-type logWriter struct {
-	onWrite func([]byte)
+// ChanWriter is a simple io.Writer that sends data to a channel.
+type ChanWriter struct {
+	Ch chan<- []byte
+}
+
+func (u *ChanWriter) Write(p []byte) (n int, err error) {
+	u.Ch <- p
+	return len(p), nil
 }
 
 func (h *hub) logAction(sl string) {
 	if strings.HasPrefix(sl, "log on") {
 		*logDump = "on"
 
-		logWriter := logWriter{}
-		logWriter.onWrite = func(p []byte) {
-			h.broadcastSys <- p
-		}
-
-		multiWriter := io.MultiWriter(&logWriter, os.Stderr)
+		multiWriter := io.MultiWriter(&ChanWriter{Ch: h.broadcastSys}, os.Stderr)
 		log.SetOutput(multiWriter)
 	} else if strings.HasPrefix(sl, "log off") {
 		*logDump = "off"

From 03705914766579754c28bddf5cc18c1f52819d03 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 15:54:38 +0200
Subject: [PATCH 47/50] refactor: move spErr, spWrite, and spClose functions
 from serial.go to hub.go for better organization

---
 hub.go    | 58 -------------------------------------------------------
 serial.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 54 insertions(+), 58 deletions(-)

diff --git a/hub.go b/hub.go
index 0a8f1091f..48c1cb1af 100755
--- a/hub.go
+++ b/hub.go
@@ -307,11 +307,6 @@ func (h *hub) logAction(sl string) {
 	}
 }
 
-func (u *logWriter) Write(p []byte) (n int, err error) {
-	u.onWrite(p)
-	return len(p), nil
-}
-
 func (h *hub) memoryStats() {
 	var memStats runtime.MemStats
 	runtime.ReadMemStats(&memStats)
@@ -339,56 +334,3 @@ func (h *hub) garbageCollection() {
 	h.broadcastSys <- []byte("{\"gc\":\"done\"}")
 	h.memoryStats()
 }
-
-func (h *hub) spErr(err string) {
-	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
-}
-
-func (h *hub) spClose(portname string) {
-	if myport, ok := h.serialHub.FindPortByName(portname); ok {
-		h.broadcastSys <- []byte("Closing serial port " + portname)
-		myport.Close()
-	} else {
-		h.spErr("We could not find the serial port " + portname + " that you were trying to close.")
-	}
-}
-
-func (h *hub) spWrite(arg string) {
-	// we will get a string of comXX asdf asdf asdf
-	//log.Println("Inside spWrite arg: " + arg)
-	arg = strings.TrimPrefix(arg, " ")
-	//log.Println("arg after trim: " + arg)
-	args := strings.SplitN(arg, " ", 3)
-	if len(args) != 3 {
-		errstr := "Could not parse send command: " + arg
-		//log.Println(errstr)
-		h.spErr(errstr)
-		return
-	}
-	bufferingMode := args[0]
-	portname := strings.Trim(args[1], " ")
-	data := args[2]
-
-	//log.Println("The port to write to is:" + portname + "---")
-	//log.Println("The data is:" + data + "---")
-
-	// See if we have this port open
-	port, ok := h.serialHub.FindPortByName(portname)
-	if !ok {
-		// we couldn't find the port, so send err
-		h.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
-		return
-	}
-
-	// see if bufferingMode is valid
-	switch bufferingMode {
-	case "send", "sendnobuf", "sendraw":
-		// valid buffering mode, go ahead
-	default:
-		h.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
-		return
-	}
-
-	// send it to the write channel
-	port.Write(data, bufferingMode)
-}
diff --git a/serial.go b/serial.go
index f9930f9e6..8fd74dd72 100644
--- a/serial.go
+++ b/serial.go
@@ -266,3 +266,57 @@ func (sp *SerialPortList) getPortByName(portname string) *SpPortItem {
 	}
 	return nil
 }
+
+// FIXME: the  spErr, spWrite, spClose should be moved to the 'hub.go' file
+func (h *hub) spErr(err string) {
+	h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}")
+}
+
+func (h *hub) spClose(portname string) {
+	if myport, ok := h.serialHub.FindPortByName(portname); ok {
+		h.broadcastSys <- []byte("Closing serial port " + portname)
+		myport.Close()
+	} else {
+		h.spErr("We could not find the serial port " + portname + " that you were trying to close.")
+	}
+}
+
+func (h *hub) spWrite(arg string) {
+	// we will get a string of comXX asdf asdf asdf
+	//log.Println("Inside spWrite arg: " + arg)
+	arg = strings.TrimPrefix(arg, " ")
+	//log.Println("arg after trim: " + arg)
+	args := strings.SplitN(arg, " ", 3)
+	if len(args) != 3 {
+		errstr := "Could not parse send command: " + arg
+		//log.Println(errstr)
+		h.spErr(errstr)
+		return
+	}
+	bufferingMode := args[0]
+	portname := strings.Trim(args[1], " ")
+	data := args[2]
+
+	//log.Println("The port to write to is:" + portname + "---")
+	//log.Println("The data is:" + data + "---")
+
+	// See if we have this port open
+	port, ok := h.serialHub.FindPortByName(portname)
+	if !ok {
+		// we couldn't find the port, so send err
+		h.spErr("We could not find the serial port " + portname + " that you were trying to write to.")
+		return
+	}
+
+	// see if bufferingMode is valid
+	switch bufferingMode {
+	case "send", "sendnobuf", "sendraw":
+		// valid buffering mode, go ahead
+	default:
+		h.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
+		return
+	}
+
+	// send it to the write channel
+	port.Write(data, bufferingMode)
+}

From a50aa86a1c29077743c59737de3ededb951c3486 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 16:00:09 +0200
Subject: [PATCH 48/50] refactor: remove logger from comment

---
 tools/tools.go | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/tools/tools.go b/tools/tools.go
index 0ad95763a..96265c2d2 100644
--- a/tools/tools.go
+++ b/tools/tools.go
@@ -33,14 +33,13 @@ import (
 //
 // - *directory* contains the location where the tools are downloaded.
 // - *indexURL* contains the url where the tools description is contained.
-// - *logger* is a StdLogger used for reporting debug and info messages
 // - *installed* contains a map[string]string of the tools installed and their exact location
 //
 // Usage:
 // You have to call the New() function passing it the required parameters:
 //
 // 	index = index.Init("https://downloads.arduino.cc/packages/package_index.json", dataDir)
-// 	tools := tools.New(dataDir, index, logger)
+// 	tools := tools.New(dataDir, index)
 
 // Tools will represent the installed tools
 type Tools struct {
@@ -54,7 +53,6 @@ type Tools struct {
 // New will return a Tool object, allowing the caller to execute operations on it.
 // The New functions accept the directory to use to host the tools,
 // an index (used to download the tools),
-// and a logger to log the operations
 func New(directory *paths.Path, index *index.Resource, signPubKey *rsa.PublicKey) *Tools {
 	t := &Tools{
 		directory: directory,

From 65218a666abf117e3a35bd4b0a5e473f0afdee1c Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 16:13:17 +0200
Subject: [PATCH 49/50] refactor: replace OnMessage with ChanWriter for
 improved message handling

---
 serialport.go | 24 +++++++++++-------------
 1 file changed, 11 insertions(+), 13 deletions(-)

diff --git a/serialport.go b/serialport.go
index 5c8ed5c55..9c46ac823 100755
--- a/serialport.go
+++ b/serialport.go
@@ -62,8 +62,8 @@ type serport struct {
 	//bufferwatcher *BufferflowDummypause
 	bufferwatcher Bufferflow
 
-	OnMessage func([]byte)
-	OnClose   func(*serport)
+	ChanWriter ChanWriter
+	OnClose    func(*serport)
 }
 
 // SpPortMessage is the serial port message
@@ -92,7 +92,7 @@ func (p *serport) reader(buftype string) {
 		if p.isClosing.Load() {
 			strmsg := "Shutting down reader on " + p.portConf.Name
 			log.Println(strmsg)
-			p.OnMessage([]byte(strmsg))
+			p.ChanWriter.Write.Write([]byte(strmsg))
 			break
 		}
 
@@ -146,14 +146,14 @@ func (p *serport) reader(buftype string) {
 			if err == io.EOF || err == io.ErrUnexpectedEOF {
 				// hit end of file
 				log.Println("Hit end of file on serial port")
-				p.OnMessage([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}"))
+				p.ChanWriter.Write([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}"))
 
 			}
 
 			if err != nil {
 				log.Println(err)
-				p.OnMessage([]byte("Error reading on " + p.portConf.Name + " " + err.Error() + " Closing port."))
-				p.OnMessage([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}"))
+				p.ChanWriter.Write([]byte("Error reading on " + p.portConf.Name + " " + err.Error() + " Closing port."))
+				p.ChanWriter.Write([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}"))
 				p.isClosingDueToError = true
 				break
 			}
@@ -211,7 +211,7 @@ func (p *serport) writerBuffered() {
 	}
 	msgstr := "writerBuffered just got closed. make sure you make a new one. port:" + p.portConf.Name
 	log.Println(msgstr)
-	p.OnMessage([]byte(msgstr))
+	p.ChanWriter.Write([]byte(msgstr))
 }
 
 // this method runs as its own thread because it's instantiated
@@ -232,13 +232,13 @@ func (p *serport) writerNoBuf() {
 		if err != nil {
 			errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port."
 			log.Print(errstr)
-			p.OnMessage([]byte(errstr))
+			p.ChanWriter.Write([]byte(errstr))
 			break
 		}
 	}
 	msgstr := "Shutting down writer on " + p.portConf.Name
 	log.Println(msgstr)
-	p.OnMessage([]byte(msgstr))
+	p.ChanWriter.Write([]byte(msgstr))
 
 	p.portIo.Close()
 
@@ -279,7 +279,7 @@ func (p *serport) writerRaw() {
 	}
 	msgstr := "writerRaw just got closed. make sure you make a new one. port:" + p.portConf.Name
 	log.Println(msgstr)
-	p.OnMessage([]byte(msgstr))
+	p.ChanWriter.Write([]byte(msgstr))
 }
 
 // FIXME: move this into the `hub.go` file
@@ -323,11 +323,9 @@ func (h *hub) spHandlerOpen(portname string, baud int, buftype string) {
 		portIo:       sp,
 		portName:     portname,
 		BufferType:   buftype,
+		ChanWriter:   ChanWriter{h.broadcastSys},
 	}
 
-	p.OnMessage = func(msg []byte) {
-		h.broadcastSys <- msg
-	}
 	p.OnClose = func(port *serport) {
 		h.serialPortList.MarkPortAsClosed(p.portName)
 		h.serialPortList.List()

From 9cbe49e00fed6e17bed4e67ff18fcb08df1a5971 Mon Sep 17 00:00:00 2001
From: Davide N <davideneri18@gmail.com>
Date: Wed, 16 Apr 2025 17:00:42 +0200
Subject: [PATCH 50/50] fix tests

---
 serialport.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/serialport.go b/serialport.go
index 9c46ac823..614111c71 100755
--- a/serialport.go
+++ b/serialport.go
@@ -92,7 +92,7 @@ func (p *serport) reader(buftype string) {
 		if p.isClosing.Load() {
 			strmsg := "Shutting down reader on " + p.portConf.Name
 			log.Println(strmsg)
-			p.ChanWriter.Write.Write([]byte(strmsg))
+			p.ChanWriter.Write([]byte(strmsg))
 			break
 		}