From a00e13568745faf72387d8aa0c1c2e6aa4a093a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=B1=D0=B5=D0=BB=D0=B5=D0=B2=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B4=D1=80=D0=B5=D0=B9=20=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=B8=D1=87?= Date: Sat, 25 Jan 2025 23:32:33 +0300 Subject: [PATCH] init commit --- README.md | 14 ++++ cmd/plotter/main.go | 197 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 29 +++++++ go.sum | 45 ++++++++++ 4 files changed, 285 insertions(+) create mode 100644 README.md create mode 100644 cmd/plotter/main.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1ef009 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +Плоттер серийного порта + +https://chat.deepseek.com/a/chat/s/5091a6a6-862f-406c-8c8b-fbd9bec4e303 + + +TODO: + +1. Добавить окно выбора серийного порта и скорости подключения +2. Обновлять информацию из серийного порта -> горутина с каналом на обновление и получения из update +3. Парсер значений и их отрисовка различными цветами +4. Запуск аргументами командной строки или конфигом +5. Отслеживание времени +6. Добавить функцию записи данных в файл + diff --git a/cmd/plotter/main.go b/cmd/plotter/main.go new file mode 100644 index 0000000..af50cdc --- /dev/null +++ b/cmd/plotter/main.go @@ -0,0 +1,197 @@ +package main + +import ( + "fmt" + "log" + "math" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "go.bug.st/serial/enumerator" +) + +type model struct { + values []float64 // Data points to plot + width int // Width of the plot + height int // Height of the plot + time float64 // Time variable for animation +} +type TickMsg time.Time + +func doTick() tea.Cmd { + return tea.Tick(time.Second/30, func(t time.Time) tea.Msg { + return TickMsg(t) + }) +} + +// Initialize the model with some data +func initialModel() model { + return model{ + values: make([]float64, 100), // More points for higher resolution + width: 70, + height: 25, + time: 0.0, + } +} + +func (m model) Init() tea.Cmd { + return doTick() +} + +// Update function (required by bubbletea) +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + } + case TickMsg: + + // Update the time variable to animate the sine wave + m.time += 0.1 + // Generate new sine wave values + for i := range m.values { + m.values[i] = math.Sin(float64(i)/10.0 + m.time) + } + return m, doTick() + } + return m, nil +} + +// Interpolate between two colors based on a factor (0 to 1) +func interpolateColor(color1, color2 lipgloss.Color, factor float64) lipgloss.Color { + r1, g1, b1 := hexToRGB(string(color1)) + r2, g2, b2 := hexToRGB(string(color2)) + r := uint8(float64(r1) + (float64(r2)-float64(r1))*factor) + g := uint8(float64(g1) + (float64(g2)-float64(g1))*factor) + b := uint8(float64(b1) + (float64(b2)-float64(b1))*factor) + return lipgloss.Color(fmt.Sprintf("#%02X%02X%02X", r, g, b)) +} + +// Convert hex color to RGB values +func hexToRGB(hex string) (uint8, uint8, uint8) { + var r, g, b uint8 + fmt.Sscanf(hex, "#%02X%02X%02X", &r, &g, &b) + return r, g, b +} + +// View function (required by bubbletea) +func (m model) View() string { + // Create a 2D grid to represent the plot + grid := make([][]string, m.height) + for i := range grid { + grid[i] = make([]string, m.width) + for j := range grid[i] { + grid[i][j] = " " // Initialize with spaces + } + } + + // Normalize values to fit within the plot height + maxValue := 1.0 // Sine wave ranges from -1 to 1 + minValue := -1.0 + scale := float64(m.height-1) / (maxValue - minValue) + + // Define the gradient colors + colorGreen := lipgloss.Color("#00FF00") // Green + colorOrange := lipgloss.Color("#FF7F00") // Orange + colorRed := lipgloss.Color("#FF0000") // Red + + // Plot the values with higher resolution and color + for x, value := range m.values { + y := int((value - minValue) * scale) + if y >= 0 && y < m.height && x < m.width { + // Calculate the interpolation factor based on the y position + factor := float64(y) / float64(m.height-1) + var color lipgloss.Color + if factor < 0.5 { + // Interpolate between Green and Orange + color = interpolateColor(colorGreen, colorOrange, factor*2) + } else { + // Interpolate between Orange and Red + color = interpolateColor(colorOrange, colorRed, (factor-0.5)*2) + } + style := lipgloss.NewStyle().Foreground(color) + // Use a thinner character for the line + grid[m.height-1-y][x] = style.Render("│") // Vertical bar for a thin line + } + } + + // Draw a horizontal line at y = 0.0 + yZero := int((0.0 - minValue) * scale) // Calculate the y position for 0.0 + if yZero >= 0 && yZero < m.height { + for x := 0; x < m.width; x++ { + // Use a horizontal line character + grid[m.height-1-yZero][x] = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Render("─") + } + } + + // Convert the grid to a string with axis labels + var plot string + + // Add y-axis labels + for i := 0; i < m.height; i++ { + // Calculate the corresponding value for the y-axis + value := minValue + (maxValue-minValue)*float64(m.height-1-i)/float64(m.height-1) + // Format the value to 2 decimal places + label := fmt.Sprintf("%.2f", value) + // Add padding to align the labels + label = fmt.Sprintf("%6s", label) + plot += label + " " + // Add the grid row + for j := 0; j < m.width; j++ { + plot += grid[i][j] + } + plot += "\n" + } + + // Add x-axis labels (time) + plot += " " // Align with y-axis labels + for i := 0; i < m.width; i += 10 { + // Calculate the time value for the x-axis + timeValue := m.time + float64(i)/10.0 + // Format the time value to 1 decimal place + label := fmt.Sprintf("%.1f", timeValue) + label = fmt.Sprintf("%-10s", label) + plot += label + } + plot += "\n" + + // Style the plot with a border + style := lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FFFFFF")). // White border + Padding(1, 2) + return style.Render(plot) +} + +func main() { + + // Initialize the model + model := initialModel() + + // Start the bubbletea program + p := tea.NewProgram(model, tea.WithAltScreen()) + // _, err = p.Run() + // if err != nil { + // fmt.Printf("Error running program: %v\n", err) + // os.Exit(1) + // } + p.Kill() + ports, err := enumerator.GetDetailedPortsList() + if err != nil { + log.Fatal(err) + } + if len(ports) == 0 { + fmt.Println("No serial ports found!") + return + } + for _, port := range ports { + fmt.Printf("Found port: %s\n", port.Name) + if port.IsUSB { + fmt.Printf(" USB ID %s:%s\n", port.VID, port.PID) + fmt.Printf(" USB serial %s\n", port.SerialNumber) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e630c31 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module git.belvedersky.ru/plotter + +go 1.23.5 + +require github.com/charmbracelet/lipgloss v1.0.0 + +require ( + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/creack/goselect v0.1.2 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/text v0.3.8 // indirect +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.2.4 + github.com/charmbracelet/x/ansi v0.4.5 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + go.bug.st/serial v1.6.2 + golang.org/x/sys v0.27.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..94ea69e --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= +github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= +go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=