Skip to content

Commit

Permalink
feat(plc4go): plc4xbrowser should now remember last host and support …
Browse files Browse the repository at this point in the history
…more drivers
  • Loading branch information
sruehl committed Jul 30, 2022
1 parent 64c52c7 commit 54d0cf9
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 21 deletions.
111 changes: 111 additions & 0 deletions plc4go/tools/plc4xbrowser/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package main

import (
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"os"
"path"
"time"
)

var plc4xBrowserConfigDir string
var configFile string
var config Config

type Config struct {
History struct {
Last10Hosts []string `yaml:"last_hosts"`
}
lastUpdated time.Time `yaml:"last_updated"`
}

func init() {
userConfigDir, err := os.UserConfigDir()
if err != nil {
panic(err)
}
plc4xBrowserConfigDir = path.Join(userConfigDir, "plc4xbrowser")
if _, err := os.Stat(plc4xBrowserConfigDir); os.IsNotExist(err) {
err := os.Mkdir(plc4xBrowserConfigDir, os.ModeDir|os.ModePerm)
if err != nil {
panic(err)
}
}
configFile = path.Join(plc4xBrowserConfigDir, "config.yml")
}

func loadConfig() {
f, err := os.Open(configFile)
if err != nil {
log.Info().Err(err).Msg("No config file found")
return
}
defer func(f *os.File) {
err := f.Close()
if err != nil {
log.Error().Err(err).Msg("Error closing config file")
}
}(f)

decoder := yaml.NewDecoder(f)
if err = decoder.Decode(&config); err != nil {
log.Warn().Err(err).Msg("Can't decode config file")
return
}
}

func saveConfig() {
config.lastUpdated = time.Now()
f, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
log.Warn().Err(err).Msg("Can't save config file")
return
}
encoder := yaml.NewEncoder(f)
defer func(encoder *yaml.Encoder) {
err := encoder.Close()
if err != nil {
log.Error().Err(err).Msg("Error closing config file")
}
}(encoder)
if err := encoder.Encode(config); err != nil {
log.Warn().Err(err).Msg("Can't encode config file")
panic(err)
}
}

func addHost(host string) {
existingIndex := -1
for i, lastHost := range config.History.Last10Hosts {
if lastHost == host {
existingIndex = i
break
}
}
if existingIndex >= 0 {
config.History.Last10Hosts = append(config.History.Last10Hosts[:existingIndex], config.History.Last10Hosts[existingIndex+1:]...)
}
if len(config.History.Last10Hosts) >= 10 {
config.History.Last10Hosts = config.History.Last10Hosts[1:]
}
config.History.Last10Hosts = append(config.History.Last10Hosts, host)
}
141 changes: 120 additions & 21 deletions plc4go/tools/plc4xbrowser/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ package main

import (
"fmt"
"github.com/apache/plc4x/plc4go/internal/ads"
"github.com/apache/plc4x/plc4go/internal/bacnetip"
"github.com/apache/plc4x/plc4go/internal/cbus"
"github.com/apache/plc4x/plc4go/internal/s7"
"io"
"net/url"
"strconv"
"strings"
"sync"
"time"

"github.com/gdamore/tcell/v2"
Expand All @@ -32,15 +38,17 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"

"github.com/apache/plc4x/plc4go/internal/cbus"
plc4go "github.com/apache/plc4x/plc4go/pkg/api"
"github.com/apache/plc4x/plc4go/pkg/api/model"
"github.com/apache/plc4x/plc4go/pkg/api/transports"
)

// TODO: replace with real commands
const plc4xCommands = "connect,disconnect,read,write,register,subscribe,quit"
const protocols = "ads,bacnetip,c-bus,s7"

var driverManager plc4go.PlcDriverManager
var driverAdded func(string)
var connections map[string]plc4go.PlcConnection
var connectionsChanged func()

Expand All @@ -50,9 +58,30 @@ var messagesReceived int
var messageOutput io.Writer

func init() {
hasShutdown = false
connections = make(map[string]plc4go.PlcConnection)
}

func initSubsystem() {
driverManager = plc4go.NewPlcDriverManager()
}

var shutdownMutex sync.Mutex
var hasShutdown bool

func shutdown() {
shutdownMutex.Lock()
defer shutdownMutex.Unlock()
if hasShutdown {
return
}
for _, connection := range connections {
connection.Close()
}
saveConfig()
hasShutdown = true
}

func main() {
application := tview.NewApplication()

Expand All @@ -66,7 +95,7 @@ func main() {
commandArea := buildCommandArea(newPrimitive, application)

grid := tview.NewGrid().
SetRows(3, 0, 3).
SetRows(3, 0, 1).
SetColumns(30, 0, 30).
SetBorders(true).
AddItem(newPrimitive("PLC4X Browser"), 0, 0, 1, 3, 0, 0, false).
Expand All @@ -82,18 +111,30 @@ func main() {
AddItem(outputArea, 1, 1, 1, 1, 0, 100, false).
AddItem(commandArea, 1, 2, 1, 1, 0, 100, false)

if err := application.SetRoot(grid, true).EnableMouse(true).Run(); err != nil {
application.SetRoot(grid, true).EnableMouse(true)

loadConfig()

initSubsystem()

application.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyCtrlC:
shutdown()
}
return event
})

if err := application.Run(); err != nil {
panic(err)
}
for _, connection := range connections {
connection.Close()
}
shutdown()
}

func buildConnectionArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) tview.Primitive {
connectionAreaHeader := newPrimitive("Connections")
connectionArea := tview.NewGrid().
SetRows(3, 0).
SetRows(3, 0, 10).
SetColumns(0).
AddItem(connectionAreaHeader, 0, 0, 1, 1, 0, 0, false)
{
Expand All @@ -109,7 +150,27 @@ func buildConnectionArea(newPrimitive func(text string) tview.Primitive, applica
}
})
}
connectionArea.AddItem(connectionList, 1, 0, 1, 1, 0, 0, true)
connectionArea.AddItem(connectionList, 1, 0, 1, 1, 0, 0, false)
{
registeredDriverAreaHeader := newPrimitive("Registered drivers")
registeredDriverArea := tview.NewGrid().
SetRows(3, 0).
SetColumns(0).
AddItem(registeredDriverAreaHeader, 0, 0, 1, 1, 0, 0, false)
{
driverList := tview.NewList()
driverAdded = func(driver string) {
application.QueueUpdateDraw(func() {
driverList.AddItem(driver, "", 0x0, func() {
//TODO: disconnect popup
})
})
}
registeredDriverArea.AddItem(driverList, 1, 0, 1, 1, 0, 0, false)
}
connectionArea.AddItem(registeredDriverArea, 2, 0, 1, 1, 0, 0, false)
}

}
return connectionArea
}
Expand All @@ -130,7 +191,8 @@ func buildCommandArea(newPrimitive func(text string) tview.Primitive, applicatio
})
commandArea.AddItem(enteredCommands, 1, 0, 1, 1, 0, 0, false)

words := strings.Split(plc4xCommands, ",")
plc4xCommandSuggestions := strings.Split(plc4xCommands, ",")
protocolsSuggestions := strings.Split(protocols, ",")
commandInputField := tview.NewInputField().
SetLabel("PLC4X Command").
SetFieldWidth(30)
Expand Down Expand Up @@ -158,21 +220,37 @@ func buildCommandArea(newPrimitive func(text string) tview.Primitive, applicatio
if len(currentText) == 0 {
return
}
for _, word := range words {
for _, word := range plc4xCommandSuggestions {
if strings.HasPrefix(strings.ToLower(word), strings.ToLower(currentText)) {
entries = append(entries, word)
}
}
switch {
case strings.HasPrefix(currentText, "connect"):
for _, protocol := range protocolsSuggestions {
if strings.HasPrefix(currentText, "connect "+protocol) {
for _, host := range config.History.Last10Hosts {
entries = append(entries, "connect "+protocol+"://"+host)
}
entries = append(entries, currentText)
} else {
entries = append(entries, "connect "+protocol)
}
}
case strings.HasPrefix(currentText, "disconnect"):
for connectionsString, _ := range connections {
entries = append(entries, "disconnect "+connectionsString)
}
case strings.HasPrefix(currentText, "register"):
for _, protocol := range protocolsSuggestions {
entries = append(entries, "register "+protocol)
}
case strings.HasPrefix(currentText, "subscribe"):
for connectionsString, _ := range connections {
entries = append(entries, "subscribe "+connectionsString)
}
}
log.Info().Msgf("%v %v", entries, config.History.Last10Hosts)
return
})
commandArea.AddItem(commandInputField, 2, 0, 1, 1, 0, 0, true)
Expand All @@ -183,22 +261,42 @@ func buildCommandArea(newPrimitive func(text string) tview.Primitive, applicatio
func handleCommand(commandText string) error {
switch {
case strings.HasPrefix(commandText, "register "):
protocol := strings.TrimPrefix(commandText, "register ")
switch protocol {
case "ads":
driverManager.RegisterDriver(ads.NewDriver())
transports.RegisterTcpTransport(driverManager)
case "bacnetip":
driverManager.RegisterDriver(bacnetip.NewDriver())
transports.RegisterUdpTransport(driverManager)
case "c-bus":
driverManager.RegisterDriver(cbus.NewDriver())
transports.RegisterTcpTransport(driverManager)
case "s7":
driverManager.RegisterDriver(s7.NewDriver())
transports.RegisterTcpTransport(driverManager)
default:
return errors.Errorf("Unknown protocol %s", protocol)
}
driverAdded(protocol)
case strings.HasPrefix(commandText, "connect "):
host := strings.TrimPrefix(commandText, "connect ")
if _, ok := connections[host]; ok {
return errors.Errorf("%s already connected", host)
connectionString := strings.TrimPrefix(commandText, "connect ")
log.Info().Msgf("commandText [%s] connectionString [%s]", commandText, connectionString)
connectionUrl, err := url.Parse(connectionString)
if err != nil {
return errors.Wrapf(err, "can't parse connection url %s", connectionString)
}
addHost(connectionUrl.Host)
connectionId := fmt.Sprintf("%s://%s", connectionUrl.Scheme, connectionUrl.Host)
if _, ok := connections[connectionId]; ok {
return errors.Errorf("%s already connected", connectionId)
}
//TODO: we hardcode that to cbus for now
connectionString := fmt.Sprintf("c-bus://%s?srchk=true", host)
driverManager := plc4go.NewPlcDriverManager()
driverManager.RegisterDriver(cbus.NewDriver())
transports.RegisterTcpTransport(driverManager)
connectionResult := <-driverManager.GetConnection(connectionString)
if err := connectionResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s can't connect to", host)
return errors.Wrapf(err, "%s can't connect to", connectionUrl.Host)
}
log.Info().Msgf("%s connected", host)
connections[host] = connectionResult.GetConnection()
log.Info().Msgf("%s connected", connectionId)
connections[connectionId] = connectionResult.GetConnection()
connectionsChanged()
case strings.HasPrefix(commandText, "disconnect "):
host := strings.TrimPrefix(commandText, "disconnect ")
Expand All @@ -218,6 +316,7 @@ func handleCommand(commandText string) error {
if connection, ok := connections[host]; !ok {
return errors.Errorf("%s not connected", host)
} else {
// TODO: hardcoded to c-bus at the moment
subscriptionRequest, err := connection.SubscriptionRequestBuilder().
AddEventQuery("something", "monitor/*/*").
AddItemHandler(func(event model.PlcSubscriptionEvent) {
Expand Down

0 comments on commit 54d0cf9

Please sign in to comment.