From ca7f799eaeaa52fa5d887da5bca4975cfb0abd1c Mon Sep 17 00:00:00 2001 From: Roy Gollub Date: Fri, 30 Jun 2023 12:08:42 +0200 Subject: [PATCH] # Version 2.0.0 ## Repository moved to GitHub organization --- CHANGELOG.md | 42 +- .../pages/assets/legacy/resources.json | 1 + .../pages/assets/legacy/settings.json | 3 + .../pages/global.css | 1 + .../pages/i18n/de.json | 3 + .../pages/i18n/en.json | 3 + .../pages/package.json | 11 + .../CSK_Module_DeviceNetworkConfig.css | 10 + .../CSK_Module_DeviceNetworkConfig.html | 226 ++ .../pages/pages/navigation.json | 4 + .../pages/src/converter.ts | 0 .../pages/src/index.ts | 0 CSK_Module_DeviceNetworkConfig/project.mf.xml | 158 + .../CSK_Module_DeviceNetworkConfig.lua | 59 + .../DeviceNetworkConfig_Controller.lua | 316 ++ .../DeviceNetworkConfig_Model.lua | 79 + .../DeviceNetworkConfig/helper/Json.lua | 388 +++ .../DeviceNetworkConfig/helper/funcs.lua | 175 ++ README.md | 24 +- docu/CSK_Module_DeviceNetworkConfig.html | 2623 +++++++++++++++++ docu/media/UI_Screenshot.png | Bin 0 -> 44832 bytes 21 files changed, 4104 insertions(+), 22 deletions(-) create mode 100644 CSK_Module_DeviceNetworkConfig/pages/assets/legacy/resources.json create mode 100644 CSK_Module_DeviceNetworkConfig/pages/assets/legacy/settings.json create mode 100644 CSK_Module_DeviceNetworkConfig/pages/global.css create mode 100644 CSK_Module_DeviceNetworkConfig/pages/i18n/de.json create mode 100644 CSK_Module_DeviceNetworkConfig/pages/i18n/en.json create mode 100644 CSK_Module_DeviceNetworkConfig/pages/package.json create mode 100644 CSK_Module_DeviceNetworkConfig/pages/pages/CSK_Module_DeviceNetworkConfig/CSK_Module_DeviceNetworkConfig.css create mode 100644 CSK_Module_DeviceNetworkConfig/pages/pages/CSK_Module_DeviceNetworkConfig/CSK_Module_DeviceNetworkConfig.html create mode 100644 CSK_Module_DeviceNetworkConfig/pages/pages/navigation.json create mode 100644 CSK_Module_DeviceNetworkConfig/pages/src/converter.ts create mode 100644 CSK_Module_DeviceNetworkConfig/pages/src/index.ts create mode 100644 CSK_Module_DeviceNetworkConfig/project.mf.xml create mode 100644 CSK_Module_DeviceNetworkConfig/scripts/CSK_Module_DeviceNetworkConfig.lua create mode 100644 CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Controller.lua create mode 100644 CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Model.lua create mode 100644 CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/helper/Json.lua create mode 100644 CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/helper/funcs.lua create mode 100644 docu/CSK_Module_DeviceNetworkConfig.html create mode 100644 docu/media/UI_Screenshot.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 449f8ea..a9d2e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,44 @@ # Changelog All notable changes to this project will be documented in this file. -## Release 0.1.0 -- Initial commit +## Release 2.0.0 -### New features -- ... +### Improvements +- Renamed function 'setPingIpAddress' to 'setPingIPAddress' +- Using recursive helper functions to convert Container <-> Lua table + +## Release 1.4.0 ### Improvements -- ... +- Update to EmmyLua annotations +- Usage of lua diagnostics +- Documentation updates + +## Release 1.3.0 + +### Improvements +- Prepared for CSK_UserManagement user levels: Operator, Maintenance, Service, Admin to optionally hide content related to UserManagement module (using bool parameter) +- Module name added to log messages +- Renamed page folder accordingly to module name +- Hiding SOPAS Login +- Documentation updates (manifest, code internal, UI elements) +- camelCase renamed functions +- Minor code edits +- Using prefix for events ### Bugfix -- ... \ No newline at end of file +- UI events notified after pageLoad after 300ms instead of 100ms to not miss + +## Release 1.2.0 +- Initial commit + +### Improvements +- Hide IPs in the list if DHCP is enabled + +## Release 1.1.0 + +### New features +- Added IP utils + +## Release 1.0.0 +- Initial commit diff --git a/CSK_Module_DeviceNetworkConfig/pages/assets/legacy/resources.json b/CSK_Module_DeviceNetworkConfig/pages/assets/legacy/resources.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/assets/legacy/resources.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/CSK_Module_DeviceNetworkConfig/pages/assets/legacy/settings.json b/CSK_Module_DeviceNetworkConfig/pages/assets/legacy/settings.json new file mode 100644 index 0000000..939ec2a --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/assets/legacy/settings.json @@ -0,0 +1,3 @@ +{ +"showLoginButton": false +} \ No newline at end of file diff --git a/CSK_Module_DeviceNetworkConfig/pages/global.css b/CSK_Module_DeviceNetworkConfig/pages/global.css new file mode 100644 index 0000000..e43ba5b --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/global.css @@ -0,0 +1 @@ +/* Add project wide CSS settings here */ \ No newline at end of file diff --git a/CSK_Module_DeviceNetworkConfig/pages/i18n/de.json b/CSK_Module_DeviceNetworkConfig/pages/i18n/de.json new file mode 100644 index 0000000..0db3279 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/i18n/de.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/CSK_Module_DeviceNetworkConfig/pages/i18n/en.json b/CSK_Module_DeviceNetworkConfig/pages/i18n/en.json new file mode 100644 index 0000000..0db3279 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/i18n/en.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/CSK_Module_DeviceNetworkConfig/pages/package.json b/CSK_Module_DeviceNetworkConfig/pages/package.json new file mode 100644 index 0000000..74b18af --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/package.json @@ -0,0 +1,11 @@ +{ + "name": "CSK_Module_DeviceNetworkConfig", + "version": "1.0.0", + "description": "Generated", + "components": [ + "@sick-davinci/basic-elements" + ], + "dependencies": { + "@sick-davinci/basic-elements": "^5.0.5" + } +} \ No newline at end of file diff --git a/CSK_Module_DeviceNetworkConfig/pages/pages/CSK_Module_DeviceNetworkConfig/CSK_Module_DeviceNetworkConfig.css b/CSK_Module_DeviceNetworkConfig/pages/pages/CSK_Module_DeviceNetworkConfig/CSK_Module_DeviceNetworkConfig.css new file mode 100644 index 0000000..eadc5e2 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/pages/CSK_Module_DeviceNetworkConfig/CSK_Module_DeviceNetworkConfig.css @@ -0,0 +1,10 @@ +.myCustomFrame_CSK_Module_DeviceNetworkConfig { + border-style: solid; + border-width: 1px; + border-color: grey; + margin: 6px; +} + +.myCustomMinWidth_CSK_Module_DeviceNetworkConfig { + min-width: 60px; +} diff --git a/CSK_Module_DeviceNetworkConfig/pages/pages/CSK_Module_DeviceNetworkConfig/CSK_Module_DeviceNetworkConfig.html b/CSK_Module_DeviceNetworkConfig/pages/pages/CSK_Module_DeviceNetworkConfig/CSK_Module_DeviceNetworkConfig.html new file mode 100644 index 0000000..e8948ec --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/pages/CSK_Module_DeviceNetworkConfig/CSK_Module_DeviceNetworkConfig.html @@ -0,0 +1,226 @@ + + + + + + + + + + Applied network configuration will be saved persistently. + + + + + + + ... Currently processing ... + + + + + + + New config accepted + + + + + New config NOT accepted + + + + + + Refresh + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Apply new network configuration + + + + + +

Tools

+ + + + + + + PING + + + + + + + + + + + + + +
+
+
+ + + + + Please login with at least user level 'Maintenance' or higher to see this page. + + + + + + +
+ + + + +
+ +
diff --git a/CSK_Module_DeviceNetworkConfig/pages/pages/navigation.json b/CSK_Module_DeviceNetworkConfig/pages/pages/navigation.json new file mode 100644 index 0000000..3f3a226 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/pages/pages/navigation.json @@ -0,0 +1,4 @@ +{ + "version": "1.0", + "pages": [] +} \ No newline at end of file diff --git a/CSK_Module_DeviceNetworkConfig/pages/src/converter.ts b/CSK_Module_DeviceNetworkConfig/pages/src/converter.ts new file mode 100644 index 0000000..e69de29 diff --git a/CSK_Module_DeviceNetworkConfig/pages/src/index.ts b/CSK_Module_DeviceNetworkConfig/pages/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/CSK_Module_DeviceNetworkConfig/project.mf.xml b/CSK_Module_DeviceNetworkConfig/project.mf.xml new file mode 100644 index 0000000..68ac025 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/project.mf.xml @@ -0,0 +1,158 @@ + + + + + This is an automatically generated CROWN (description not necessary). + + + + released + This module provides the possibility to setup the ethernet interfaces of the device. + +See following descriptions of events/functions regarding further information. + + + + Notify current subnet mask. + + + + Notify current IP. + + + + Notify interface table as JSON (e.g. for table in UI). + + + + Notify current DHCP status. + + + + Notify current 'Default Gateway' + + + + Notified to disable / enable 'IP' text field in UI. + + + + Notified to disable / enable 'Subnet mask' text field in UI. + + + + Notified to disable / enable 'Gateway' text field in UI. + + + + Highlights the 'IP' in UI if format of IP is not correct. + + + + Highlights the 'Subnet' field in UI if format is not correct. + + + + Highlights the 'Gateway field' in UI if format of gateway is not correct. + + + + Notified to disable / enable 'Apply new config' button in UI. + + + + Notify current configuration process status. + + + + Notified to disable / enable 'DHCP' checkbox in UI. + + + + Notify currently selected interface. + + + + Status of Operator userlevel. Used internally in combination with the CSK_UserManagement module if available. + + + + Status of Maintenance userlevel. Used internally in combination with the CSK_UserManagement module if available. + + + + Status of Service userlevel. Used internally in combination with the CSK_UserManagement module if available. + + + + Status of Admin userlevel. Used internally in combination with the CSK_UserManagement module if available. + + + + Notify result of executed ping command. + + + + Notify details of executed ping command. + + + + Function to register "OnResume" of the module UI (only as helper function). + + + + Preset subnet mask to be configured via 'applyConfig'. + + + + Preset IP to be configured via 'applyConfig'. + + + + Preset DHCP status to be configured via 'applyConfig'. + + + + Preset default gateway to be configured via 'applyConfig'. + + + + Select ethernet interface via table in UI. + + + + Get current configuration of Ethernet ports. + + + Apply preset network configuration to device when button in UI is pressed. + + + Applies new configuration of Ethernet interface. + + + + + + + + Get current network description of device in JSON format. + + + + Try to ping preset IP (see 'setPingIpAddress'). + + + Preset IP to ping (see 'ping' function). + + + + + SICK AG + 2.0.0 + low + false + false + false + true + + + diff --git a/CSK_Module_DeviceNetworkConfig/scripts/CSK_Module_DeviceNetworkConfig.lua b/CSK_Module_DeviceNetworkConfig/scripts/CSK_Module_DeviceNetworkConfig.lua new file mode 100644 index 0000000..4fff678 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/scripts/CSK_Module_DeviceNetworkConfig.lua @@ -0,0 +1,59 @@ +--MIT License +-- +--Copyright (c) 2023 SICK AG +-- +--Permission is hereby granted, free of charge, to any person obtaining a copy +--of this software and associated documentation files (the "Software"), to deal +--in the Software without restriction, including without limitation the rights +--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--copies of the Software, and to permit persons to whom the Software is +--furnished to do so, subject to the following conditions: +-- +--The above copyright notice and this permission notice shall be included in all +--copies or substantial portions of the Software. +-- +--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--SOFTWARE. + +---@diagnostic disable: undefined-global, redundant-parameter, missing-parameter + +--************************************************************************** +--**********************Start Global Scope ********************************* +--************************************************************************** +----------------------------------------------------------- +-- Logger +_G.logger = Log.SharedLogger.create('ModuleLogger') +_G.logHandle = Log.Handler.create() +_G.logHandle:attachToSharedLogger('ModuleLogger') +_G.logHandle:setConsoleSinkEnabled(false) --> Set to TRUE if CSK_Logger module is not used +_G.logHandle:setLevel("ALL") +_G.logHandle:applyConfig() +----------------------------------------------------------- + +-- Loading script regarding DeviceNetworkConfig_Model +-- Check this script regarding DeviceNetworkConfig_Model parameters and functions +_G.deviceNetworkConfig_Model = require('Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Model') + +--************************************************************************** +--**********************End Global Scope *********************************** +--************************************************************************** +--**********************Start Function Scope ******************************* +--************************************************************************** +_G.deviceNetworkConfig_Model.refreshInterfaces() + +--- Function to react on startup event of the app +local function main() + + CSK_DeviceNetworkConfig.pageCalled() + +end +Script.register("Engine.OnStarted", main) + +--************************************************************************** +--**********************End Function Scope ********************************* +--************************************************************************** diff --git a/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Controller.lua b/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Controller.lua new file mode 100644 index 0000000..f2ddbb1 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Controller.lua @@ -0,0 +1,316 @@ +---@diagnostic disable: undefined-global, redundant-parameter, missing-parameter + +--*************************************************************** +-- Inside of this script, you will find the necessary functions, +-- variables and events to communicate with the DeviceNetworkConfig_Model +--*************************************************************** + +--************************************************************************** +--************************ Start Global Scope ****************************** +--************************************************************************** +local nameOfModule = 'CSK_DeviceNetworkConfig' + +-- Timer to update UI via events after page was loaded +local tmrDeviceNetworkConfig = Timer.create() +tmrDeviceNetworkConfig:setExpirationTime(300) +tmrDeviceNetworkConfig:setPeriodic(false) + +-- Currently selected / predefined values for network config +local currentInterfaceName = '-' +local currentIP = '-' +local currentSubnet = '-' +local currentGateway = '-' +local currentDHCP = false + +local interfacesTable = {} -- table to hold available interfaces +local jsonInterfaceListContent -- available interfaces as JSON + +-- Reference to global handle +local deviceNetworkConfig_Model + +-- ************************ UI Events Start ******************************** + +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewEthernetConfigStatus", "DeviceNetworkConfig_OnNewEthernetConfigStatus") +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewInterfaceTable", "DeviceNetworkConfig_OnNewInterfaceTable") +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewIP", "DeviceNetworkConfig_OnNewIP") +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewSubnetMask", "DeviceNetworkConfig_OnNewSubnetMask") +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewDefaultGateway", "DeviceNetworkConfig_OnNewDefaultGateway") +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewDHCPStatus", "DeviceNetworkConfig_OnNewDHCPStatus") +Script.serveEvent("CSK_DeviceNetworkConfig.OnIPDisabled", "DeviceNetworkConfig_OnIPDisabled") +Script.serveEvent("CSK_DeviceNetworkConfig.OnSubnetDisabled", "DeviceNetworkConfig_OnSubnetDisabled") +Script.serveEvent("CSK_DeviceNetworkConfig.OnGatewayDisabled", "DeviceNetworkConfig_OnGatewayDisabled") +Script.serveEvent("CSK_DeviceNetworkConfig.OnDHCPDisabled", "DeviceNetworkConfig_OnDHCPDisabled") +Script.serveEvent("CSK_DeviceNetworkConfig.OnIPError", "DeviceNetworkConfig_OnIPError") +Script.serveEvent("CSK_DeviceNetworkConfig.OnSubnetError", "DeviceNetworkConfig_OnSubnetError") +Script.serveEvent("CSK_DeviceNetworkConfig.OnGatewayError", "DeviceNetworkConfig_OnGatewayError") +Script.serveEvent("CSK_DeviceNetworkConfig.OnApplyButtonDisabled", "DeviceNetworkConfig_OnApplyButtonDisabled") +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewInterfaceChoice", "DeviceNetworkConfig_OnNewInterfaceChoice") + +Script.serveEvent("CSK_DeviceNetworkConfig.OnUserLevelOperatorActive", "DeviceNetworkConfig_OnUserLevelOperatorActive") +Script.serveEvent("CSK_DeviceNetworkConfig.OnUserLevelMaintenanceActive", "DeviceNetworkConfig_OnUserLevelMaintenanceActive") +Script.serveEvent("CSK_DeviceNetworkConfig.OnUserLevelServiceActive", "DeviceNetworkConfig_OnUserLevelServiceActive") +Script.serveEvent("CSK_DeviceNetworkConfig.OnUserLevelAdminActive", "DeviceNetworkConfig_OnUserLevelAdminActive") + +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewPingResult", "DeviceNetworkConfig_OnNewPingResult") +Script.serveEvent("CSK_DeviceNetworkConfig.OnNewPingDetails", "DeviceNetworkConfig_OnNewPingDetails") + +-- ************************ UI Events End ********************************** + +--************************************************************************** +--********************** End Global Scope ********************************** +--************************************************************************** +--**********************Start Function Scope ******************************* +--************************************************************************** + +-- Functions to forward logged in user roles via CSK_UserManagement module (if available) +-- *********************************************** +--- Function to react on status change of Operator user level +---@param status boolean Status if Operator level is active +local function handleOnUserLevelOperatorActive(status) + Script.notifyEvent("DeviceNetworkConfig_OnUserLevelOperatorActive", status) +end + +--- Function to react on status change of Maintenance user level +---@param status boolean Status if Maintenance level is active +local function handleOnUserLevelMaintenanceActive(status) + Script.notifyEvent("DeviceNetworkConfig_OnUserLevelMaintenanceActive", status) +end + +--- Function to react on status change of Service user level +---@param status boolean Status if Service level is active +local function handleOnUserLevelServiceActive(status) + Script.notifyEvent("DeviceNetworkConfig_OnUserLevelServiceActive", status) +end + +--- Function to react on status change of Admin user level +---@param status boolean Status if Admin level is active +local function handleOnUserLevelAdminActive(status) + Script.notifyEvent("DeviceNetworkConfig_OnUserLevelAdminActive", status) +end + +--- Function to check what options should be adjustable in UI +local function checkWhatToDisable() + if currentDHCP == true or (deviceNetworkConfig_Model.helperFuncs.checkIP(currentIP) and deviceNetworkConfig_Model.helperFuncs.checkIP(currentSubnet) and (deviceNetworkConfig_Model.helperFuncs.checkIP(currentGateway) or currentGateway == '')) then + Script.notifyEvent("DeviceNetworkConfig_OnApplyButtonDisabled", false) + else + Script.notifyEvent("DeviceNetworkConfig_OnApplyButtonDisabled", true) + end + if currentInterfaceName == '-' or nil then + Script.notifyEvent("DeviceNetworkConfig_OnIPDisabled", true) + Script.notifyEvent("DeviceNetworkConfig_OnSubnetDisabled", true) + Script.notifyEvent("DeviceNetworkConfig_OnGatewayDisabled", true) + Script.notifyEvent("DeviceNetworkConfig_OnDHCPDisabled", true) + else + Script.notifyEvent("DeviceNetworkConfig_OnDHCPDisabled", false) + if currentDHCP == true then -- when DHCP is ON, the rest of the fields are empty and can't be edited + Script.notifyEvent("DeviceNetworkConfig_OnIPError", false) + Script.notifyEvent("DeviceNetworkConfig_OnSubnetError", false) + Script.notifyEvent("DeviceNetworkConfig_OnGatewayError", false) + Script.notifyEvent("DeviceNetworkConfig_OnNewIP", '-') + Script.notifyEvent("DeviceNetworkConfig_OnNewSubnetMask", '-') + Script.notifyEvent("DeviceNetworkConfig_OnNewDefaultGateway", '-') + Script.notifyEvent("DeviceNetworkConfig_OnIPDisabled", true) + Script.notifyEvent("DeviceNetworkConfig_OnSubnetDisabled", true) + Script.notifyEvent("DeviceNetworkConfig_OnGatewayDisabled", true) + else + Script.notifyEvent("DeviceNetworkConfig_OnIPDisabled", false) + Script.notifyEvent("DeviceNetworkConfig_OnSubnetDisabled", false) + Script.notifyEvent("DeviceNetworkConfig_OnGatewayDisabled", false) + end + end +end + +--- Function to get access to the deviceNetworkConfig_Model object +---@param handle handle Handle of deviceNetworkConfig_Model object +local function setDeviceNetworkConfig_Model_Handle(handle) + deviceNetworkConfig_Model = handle + if deviceNetworkConfig_Model.userManagementModuleAvailable then + -- Register on events of CSK_UserManagement module if available + Script.register('CSK_UserManagement.OnUserLevelOperatorActive', handleOnUserLevelOperatorActive) + Script.register('CSK_UserManagement.OnUserLevelMaintenanceActive', handleOnUserLevelMaintenanceActive) + Script.register('CSK_UserManagement.OnUserLevelServiceActive', handleOnUserLevelServiceActive) + Script.register('CSK_UserManagement.OnUserLevelAdminActive', handleOnUserLevelAdminActive) + end + Script.releaseObject(handle) +end + +-- ********************* UI Setting / Submit Functions Start ******************** + +local function refresh() + interfacesTable = deviceNetworkConfig_Model.refreshInterfaces() + jsonInterfaceListContent = deviceNetworkConfig_Model.helperFuncs.createJsonList(interfacesTable) + Script.notifyEvent("DeviceNetworkConfig_OnNewInterfaceTable", jsonInterfaceListContent) + checkWhatToDisable() +end +Script.serveFunction("CSK_DeviceNetworkConfig.refresh", refresh) + +--- Function to update user levels +local function updateUserLevel() + if deviceNetworkConfig_Model.userManagementModuleAvailable then + -- Trigger CSK_UserManagement module to provide events regarding user role + CSK_UserManagement.pageCalled() + else + -- If CSK_UserManagement is not active, show everything + Script.notifyEvent("DeviceNetworkConfig_OnUserLevelOperatorActive", true) + Script.notifyEvent("DeviceNetworkConfig_OnUserLevelMaintenanceActive", true) + Script.notifyEvent("DeviceNetworkConfig_OnUserLevelServiceActive", true) + Script.notifyEvent("DeviceNetworkConfig_OnUserLevelAdminActive", true) + end +end + +--- Function to send all relevant values to UI on resume +local function handleOnExpiredTmrDeviceNetworkConfig() + + updateUserLevel() + + refresh() + currentInterfaceName = '-' + currentIP = '-' + currentSubnet = '-' + currentGateway = '-' + currentDHCP = false + Script.notifyEvent("DeviceNetworkConfig_OnNewDHCPStatus", false) + Script.notifyEvent("DeviceNetworkConfig_OnNewIP", '-') + Script.notifyEvent("DeviceNetworkConfig_OnNewSubnetMask", '-') + Script.notifyEvent("DeviceNetworkConfig_OnNewDefaultGateway", '-') + Script.notifyEvent("DeviceNetworkConfig_OnNewInterfaceChoice",'-') + Script.notifyEvent("DeviceNetworkConfig_OnNewEthernetConfigStatus", 'empty') + checkWhatToDisable() +end +Timer.register(tmrDeviceNetworkConfig, "OnExpired", handleOnExpiredTmrDeviceNetworkConfig) + +-- ********************* UI Setting / Submit Functions Start ******************** + +local function pageCalled() + updateUserLevel() -- try to hide user specific content asap + tmrDeviceNetworkConfig:start() + return '' +end +Script.serveFunction("CSK_DeviceNetworkConfig.pageCalled", pageCalled) + +local function selectInterface(row_selected) + Script.notifyEvent("DeviceNetworkConfig_OnNewEthernetConfigStatus", 'empty') + Script.notifyEvent("DeviceNetworkConfig_OnIPError", false) + Script.notifyEvent("DeviceNetworkConfig_OnSubnetError", false) + Script.notifyEvent("DeviceNetworkConfig_OnGatewayError", false) + local _, pos1 = string.find(row_selected, '"Interface":"') + local pos2, _ = string.find(row_selected, '"', pos1+1) + local selectedInterfaceName = string.sub(row_selected, pos1+1, pos2-1) + if selectedInterfaceName ~= '-' and selectedInterfaceName ~= '' then + currentIP = interfacesTable[selectedInterfaceName].ipAddress + currentSubnet = interfacesTable[selectedInterfaceName].subnetMask + currentGateway = interfacesTable[selectedInterfaceName].defaultGateway + currentDHCP = interfacesTable[selectedInterfaceName].dhcp + currentInterfaceName = selectedInterfaceName + Script.notifyEvent("DeviceNetworkConfig_OnNewIP", currentIP) + Script.notifyEvent("DeviceNetworkConfig_OnNewSubnetMask", currentSubnet) + Script.notifyEvent("DeviceNetworkConfig_OnNewDefaultGateway", currentGateway) + Script.notifyEvent("DeviceNetworkConfig_OnNewDHCPStatus", currentDHCP) + Script.notifyEvent("DeviceNetworkConfig_OnNewInterfaceChoice",currentInterfaceName) + end + if currentDHCP == true then + Script.notifyEvent("DeviceNetworkConfig_OnIPDisabled", true) + Script.notifyEvent("DeviceNetworkConfig_OnSubnetDisabled", true) + Script.notifyEvent("DeviceNetworkConfig_OnGatewayDisabled", true) + end + Script.sleep(100) + Script.notifyEvent("DeviceNetworkConfig_OnNewInterfaceTable", jsonInterfaceListContent) + checkWhatToDisable() +end +Script.serveFunction("CSK_DeviceNetworkConfig.selectInterface", selectInterface) + +local function setInterfaceIP(newIP) + currentIP = newIP + if deviceNetworkConfig_Model.helperFuncs.checkIP(newIP) then + Script.notifyEvent("DeviceNetworkConfig_OnIPError", false) + else + Script.notifyEvent("DeviceNetworkConfig_OnIPError", true) + end + checkWhatToDisable() +end +Script.serveFunction("CSK_DeviceNetworkConfig.setInterfaceIP", setInterfaceIP) + +local function setSubnetMask(newSubnetMask) + currentSubnet = newSubnetMask + if deviceNetworkConfig_Model.helperFuncs.checkIP(newSubnetMask) then + Script.notifyEvent("DeviceNetworkConfig_OnSubnetError", false) + else + Script.notifyEvent("DeviceNetworkConfig_OnSubnetError", true) + end + checkWhatToDisable() +end +Script.serveFunction("CSK_DeviceNetworkConfig.setSubnetMask", setSubnetMask) + +local function setDefaultGateway(newDefaultGateway) + currentGateway = newDefaultGateway + if newDefaultGateway == '' or deviceNetworkConfig_Model.helperFuncs.checkIP(newDefaultGateway) then + Script.notifyEvent("DeviceNetworkConfig_OnGatewayError", false) + else + Script.notifyEvent("DeviceNetworkConfig_OnGatewayError", true) + end + checkWhatToDisable() +end +Script.serveFunction("CSK_DeviceNetworkConfig.setDefaultGateway", setDefaultGateway) + +local function setDHCPState(newDHCPState) + currentDHCP = newDHCPState + if newDHCPState == false then + if currentIP == '-' then currentIP = '192.168.0.1' end + if currentSubnet == '-' then currentSubnet = '255.255.255.0' end + if currentGateway == '-' then currentGateway = '0.0.0.0' end + Script.notifyEvent("DeviceNetworkConfig_OnNewIP", currentIP) + Script.notifyEvent("DeviceNetworkConfig_OnNewSubnetMask", currentSubnet) + Script.notifyEvent("DeviceNetworkConfig_OnNewDefaultGateway", currentGateway) + end + checkWhatToDisable() +end +Script.serveFunction("CSK_DeviceNetworkConfig.setDHCPState", setDHCPState) + +local function setPingIPAddress(ping_ip) + deviceNetworkConfig_Model.ping_ip_adress = ping_ip +end +Script.serveFunction("CSK_DeviceNetworkConfig.setPingIPAddress", setPingIPAddress) + +local function ping() + local succes, time = Ethernet.ping(deviceNetworkConfig_Model.ping_ip_adress) + Script.notifyEvent("DeviceNetworkConfig_OnNewPingResult", succes) + if (time) then + Script.notifyEvent("DeviceNetworkConfig_OnNewPingDetails", tostring(time).." ms") + else + Script.notifyEvent("DeviceNetworkConfig_OnNewPingDetails", "No Connection") + end +end +Script.serveFunction("CSK_DeviceNetworkConfig.ping", ping) + +local function applyConfig() + if deviceNetworkConfig_Model.helperFuncs.checkIP(currentIP) and deviceNetworkConfig_Model.helperFuncs.checkIP(currentSubnet) and deviceNetworkConfig_Model.helperFuncs.checkIP(currentGateway) or currentGateway == '' then + Script.notifyEvent("DeviceNetworkConfig_OnNewEthernetConfigStatus", 'processing') + if currentDHCP == true then + _G.logger:info(nameOfModule .. ": Applying device's Ethernet config: \n Interface " .. currentInterfaceName .. " \n DHCP: " .. tostring(currentDHCP)) + deviceNetworkConfig_Model.applyEthernetConfig(currentInterfaceName, currentDHCP, nil, nil, nil) + else + _G.logger:info(nameOfModule .. ": Applying device's Ethernet config: \n Interface " .. currentInterfaceName .. " \n DHCP: " .. tostring(currentDHCP) .. " \n IP: " .. currentIP.. " \n Subnet: " .. currentSubnet .. " \n Gateway: " .. currentGateway) + deviceNetworkConfig_Model.applyEthernetConfig(currentInterfaceName, currentDHCP, currentIP, currentSubnet, currentGateway) + end + refresh() + Script.notifyEvent("DeviceNetworkConfig_OnNewEthernetConfigStatus", 'success') + else + Script.notifyEvent("DeviceNetworkConfig_OnNewEthernetConfigStatus", 'error') + end + _G.logger:info(nameOfModule .. ": Applying device's Ethernet config finished") +end +Script.serveFunction("CSK_DeviceNetworkConfig.applyConfig", applyConfig) + +--- Function to react 'Ethernet.Interface.OnLinkActiveChanged' event +local function handleOnLinkActiveChanged(ifName, linkActive) + refresh() + _G.logger:info(nameOfModule .. ': New link status = ' .. tostring(linkActive) .. ' on interface ' .. ifName) +end +Script.register("Ethernet.Interface.OnLinkActiveChanged", handleOnLinkActiveChanged) + +return setDeviceNetworkConfig_Model_Handle + +--************************************************************************** +--**********************End Function Scope ********************************* +--************************************************************************** + diff --git a/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Model.lua b/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Model.lua new file mode 100644 index 0000000..c176873 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Model.lua @@ -0,0 +1,79 @@ +---@diagnostic disable: undefined-global, redundant-parameter, missing-parameter +--***************************************************************** +-- Inside of this script, you will find the module definition +-- including its parameters and functions +--***************************************************************** + +--************************************************************************** +--**********************Start Global Scope ********************************* +--************************************************************************** +local nameOfModule = 'CSK_DeviceNetworkConfig' + +local deviceNetworkConfig_Model = {} + +-- Check if CSK_UserManagement features can be used if wanted +deviceNetworkConfig_Model.userManagementModuleAvailable = CSK_UserManagement ~= nil or false + +-- Load script to communicate with the DeviceNetworkConfig_Model interface and give access +-- to the DeviceNetworkConfig_Model object. +-- Check / edit this script to see/edit functions which communicate with the UI +local setDeviceNetworkConfig_ModelHandle = require('Configuration/DeviceNetworkConfig/DeviceNetworkConfig_Controller') +setDeviceNetworkConfig_ModelHandle(deviceNetworkConfig_Model) + +--Loading helper functions if needed +deviceNetworkConfig_Model.helperFuncs = require('Configuration/DeviceNetworkConfig/helper/funcs') + +deviceNetworkConfig_Model.interfacesTable = {} -- table to hold setup of available ethernet interfaces +deviceNetworkConfig_Model.ping_ip_adress = "" -- IP address to check for ping + +--************************************************************************** +--********************** End Global Scope ********************************** +--************************************************************************** +--**********************Start Function Scope ******************************* +--************************************************************************** + +---Function to get current setting of ethernet interfaces +local function refreshInterfaces() + deviceNetworkConfig_Model.interfacesTable = {} + for _, enum in pairs(Ethernet.Interface.getInterfaces()) do + local dhcpEnabled, ipAddress, subnetMask, gateway = Ethernet.Interface.getAddressConfig(enum) + local isLinkActive = Ethernet.Interface.isLinkActive(enum) + local macAddress = Ethernet.Interface.getMACAddress(enum) + local interfaceConfig = {} + interfaceConfig.interfaceName = enum + interfaceConfig.dhcp = dhcpEnabled + interfaceConfig.macAddress = macAddress + interfaceConfig.isLinkActive = isLinkActive + interfaceConfig.ipAddress = ipAddress + interfaceConfig.subnetMask = subnetMask + interfaceConfig.defaultGateway = gateway + deviceNetworkConfig_Model.interfacesTable[enum] = interfaceConfig + end + return deviceNetworkConfig_Model.interfacesTable +end +deviceNetworkConfig_Model.refreshInterfaces = refreshInterfaces + +local function getNetworkDescription() + if deviceNetworkConfig_Model.interfacesTable ~= {} then + local jsonInterfacesTable = deviceNetworkConfig_Model.helperFuncs.json.encode(deviceNetworkConfig_Model.interfacesTable) + return jsonInterfacesTable + else + return nil + end +end +Script.serveFunction("CSK_DeviceNetworkConfig.getNetworkDescription", getNetworkDescription) + +local function applyEthernetConfig(interfaceName, dhcpEnabled, ipAddress, subnetMask, gateway) + if gateway == '' then gateway = nil end + Ethernet.Interface.setAddressConfig(interfaceName, dhcpEnabled, ipAddress, subnetMask, gateway) + Ethernet.Interface.applyAddressConfig(interfaceName) + Parameters.savePermanent() +end +Script.serveFunction("CSK_DeviceNetworkConfig.applyEthernetConfig", applyEthernetConfig) +deviceNetworkConfig_Model.applyEthernetConfig = applyEthernetConfig + +--************************************************************************* +--********************** End Function Scope ******************************* +--************************************************************************* + +return deviceNetworkConfig_Model diff --git a/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/helper/Json.lua b/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/helper/Json.lua new file mode 100644 index 0000000..711ef78 --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/helper/Json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/helper/funcs.lua b/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/helper/funcs.lua new file mode 100644 index 0000000..a29d25c --- /dev/null +++ b/CSK_Module_DeviceNetworkConfig/scripts/Configuration/DeviceNetworkConfig/helper/funcs.lua @@ -0,0 +1,175 @@ +---@diagnostic disable: undefined-global, redundant-parameter, missing-parameter +--***************************************************************** +-- Inside of this script, you will find helper functions +--***************************************************************** + +--************************************************************************** +--**********************Start Global Scope ********************************* +--************************************************************************** + +local funcs = {} +-- Providing standard JSON functions +funcs.json = require('Configuration/DeviceNetworkConfig/helper/Json') + +--************************************************************************** +--********************** End Global Scope ********************************** +--************************************************************************** +--**********************Start Function Scope ******************************* +--************************************************************************** + +--- Function to check if inserted string is a valid IP +---@param ip string String to check for IP +---@return boolean status Result if IP is valid +local function checkIP(ip) + if not ip then return false end + local a,b,c,d=ip:match("^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)$") + a=tonumber(a) + b=tonumber(b) + c=tonumber(c) + d=tonumber(d) + if not a or not b or not c or not d then return false end + if a<0 or 255= 1 then + list = list .. '"' .. tostring(1) .. '"' + end + if size >= 2 then + for i=2, size do + list = list .. ', ' .. '"' .. tostring(i) .. '"' + end + end + list = list .. "]" + return list +end +funcs.createStringListBySize = createStringListBySize + +--- Function to convert a table into a Container object +---@param content auto[] Lua Table to convert to Container +---@return Container cont Created Container +local function convertTable2Container(content) + local cont = Container.create() + for key, value in pairs(content) do + if type(value) == 'table' then + cont:add(key, convertTable2Container(value), nil) + else + cont:add(key, value, nil) + end + end + return cont +end +funcs.convertTable2Container = convertTable2Container + +--- Function to convert a Container into a table +---@param cont Container Container to convert to Lua table +---@return auto[] data Created Lua table +local function convertContainer2Table(cont) + local data = {} + local containerList = Container.list(cont) + local containerCheck = false + if tonumber(containerList[1]) then + containerCheck = true + end + for i=1, #containerList do + + local subContainer + + if containerCheck then + subContainer = Container.get(cont, tostring(i) .. '.00') + else + subContainer = Container.get(cont, containerList[i]) + end + if type(subContainer) == 'userdata' then + if Object.getType(subContainer) == "Container" then + + if containerCheck then + table.insert(data, convertContainer2Table(subContainer)) + else + data[containerList[i]] = convertContainer2Table(subContainer) + end + + else + if containerCheck then + table.insert(data, subContainer) + else + data[containerList[i]] = subContainer + end + end + else + if containerCheck then + table.insert(data, subContainer) + else + data[containerList[i]] = subContainer + end + end + end + return data +end +funcs.convertContainer2Table = convertContainer2Table + +return funcs + +--************************************************************************** +--**********************End Function Scope ********************************* +--************************************************************************** \ No newline at end of file diff --git a/README.md b/README.md index 168a273..abd4f68 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,17 @@ -# CSK_Module_[ModuleName] +# CSK_Module_DeviceNetworkConfig -Module / Application to provide [...] functionality. +Module to provide ethernet setup functionality. -*If available, please also add a screenshot/gif of the UI of the module here placed within /docu/media/ (see code)* -![](https://github.com/SICKAppSpaceCodingStarterKit/[REPO_OF_MODULE]/blob/main/docu/media/UI_Screenshot.png) +![](https://github.com/SICKAppSpaceCodingStarterKit/CSK_Module_DeviceNetworkConfig/blob/main/docu/media/UI_Screenshot.png) ## How to Run - -[***...please fill with informations...***] -For further information check out the [documentation](https://raw.githack.com/SICKAppSpaceCodingStarterKit/[REPO_OF_MODULE]/main/docu/CSK_Module_[MODULENAME].html) [update link] in the folder "docu". +The app includes an intuitive GUI to setup the ethernet setup of the device. +For further information check out the [documentation](https://raw.githack.com/SICKAppSpaceCodingStarterKit/CSK_Module_DeviceNetworkConfig/main/docu/CSK_Module_DeviceNetworkConfig.html) in the folder "docu". ## Information -Tested on: -[Device] - [firmware] -... - -[***optionally***] -Following CSK modules are used for this application via Git subtrees and should NOT be further developed within this repository (see [contribution guideline](https://github.com/SICKAppSpaceCodingStarterKit/.github/blob/main/Contribution_Guideline.md) of this GitHub organization): - - * CSK_Module_XYZ (release/tag v1.2.3) +Tested on: +1. SIM1012 - Firmware 2.2.0 This application / module is part of the SICK AppSpace Coding Starter Kit developing approach. It is programmed in an object oriented way. Some of the modules use kind of "classes" in Lua to make it possible to reuse code / classes in other projects. @@ -28,4 +20,4 @@ Please check the [documentation](https://github.com/SICKAppSpaceCodingStarterKit ## Topics -Coding Starter Kit, CSK, Module, SICK-AppSpace, [key_words] +Coding Starter Kit, CSK, Module, SICK-AppSpace, Ethernet, Interface, Config, Device diff --git a/docu/CSK_Module_DeviceNetworkConfig.html b/docu/CSK_Module_DeviceNetworkConfig.html new file mode 100644 index 0000000..011bc86 --- /dev/null +++ b/docu/CSK_Module_DeviceNetworkConfig.html @@ -0,0 +1,2623 @@ + + + + + + + + +Documentation - CSK_Module_DeviceNetworkConfig 2.0.0 + + + + + + +
+
+

Document metadata

+
+ ++++ + + + + + + + + + + + + + + + + + + +

Application name

CSK_Module_DeviceNetworkConfig

Version

2.0.0

Date

2023-06-14

Author

SICK AG

+
+
+
+

Crowns

+
+ +
+

CSK_DeviceNetworkConfig

+
+

Short description

+
+

This module provides the possibility to setup the ethernet interfaces of the device.
+See following descriptions of events/functions regarding further information.

+
+
+ +
+

Functions

+
+
CSK_DeviceNetworkConfig.applyConfig()
+
+
Short description
+
+

Apply preset network configuration to device when button in UI is pressed.

+
+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.applyConfig()
+
+
+
+
+
+
CSK_DeviceNetworkConfig.applyEthernetConfig()
+
+
Short description
+
+

Applies new configuration of Ethernet interface.

+
+
+
+
Parameters
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

interfaceName

STRING

1

Name of Ethernet interface to upload new configuration to, e.g. ETH1, ETH2 etc.

dhcpEnabled

BOOL

1

New DHCP state. If true, then IP, subnet mask and gateway must be nil.

ipAddress

STRING

1

New IP addres. Must be nil if DHCP is on.

subnetMask

STRING

1

New subnet mask. Must be nil if DHCP is on.

gateway

STRING

1

New default gateway. Must be nil if DHCP is on.

+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.applyEthernetConfig(interfaceName, dhcpEnabled, ipAddress, subnetMask, gateway)
+
+
+
+
+
+
CSK_DeviceNetworkConfig.getNetworkDescription()
+
+
Short description
+
+

Get current network description of device in JSON format.

+
+
+
+
Return values
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

jsonInterfacesTable

STRING

?

Network description as JSON.

+
+
+
Sample (auto-generated)
+
+
+
jsonInterfacesTable = CSK_DeviceNetworkConfig.getNetworkDescription()
+
+
+
+
+
+
CSK_DeviceNetworkConfig.pageCalled()
+
+
Short description
+
+

Function to register "OnResume" of the module UI (only as helper function).

+
+
+
+
Return values
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

empty

STRING

1

Empty string (only needed to simplify binding).

+
+
+
Sample (auto-generated)
+
+
+
empty = CSK_DeviceNetworkConfig.pageCalled()
+
+
+
+
+
+
CSK_DeviceNetworkConfig.ping()
+
+
Short description
+
+

Try to ping preset IP (see 'setPingIpAddress').

+
+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.ping()
+
+
+
+
+
+
CSK_DeviceNetworkConfig.refresh()
+
+
Short description
+
+

Get current configuration of Ethernet ports.

+
+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.refresh()
+
+
+
+
+
+
CSK_DeviceNetworkConfig.selectInterface()
+
+
Short description
+
+

Select ethernet interface via table in UI.

+
+
+
+
Parameters
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

row_selected

STRING

1

The string with chosen row’s content in JSON format.

+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.selectInterface(row_selected)
+
+
+
+
+
+
CSK_DeviceNetworkConfig.setDefaultGateway()
+
+
Short description
+
+

Preset default gateway to be configured via 'applyConfig'.

+
+
+
+
Parameters
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

newDefaultGateway

STRING

1

Gateway

+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.setDefaultGateway(newDefaultGateway)
+
+
+
+
+
+
CSK_DeviceNetworkConfig.setDHCPState()
+
+
Short description
+
+

Preset DHCP status to be configured via 'applyConfig'.

+
+
+
+
Parameters
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

newDHCPstate

BOOL

1

DHCP state (true to enable DHCP).

+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.setDHCPState(newDHCPstate)
+
+
+
+
+
+
CSK_DeviceNetworkConfig.setInterfaceIP()
+
+
Short description
+
+

Preset IP to be configured via 'applyConfig'.

+
+
+
+
Parameters
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

newIP

STRING

1

IP

+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.setInterfaceIP(newIP)
+
+
+
+
+
+
CSK_DeviceNetworkConfig.setPingIPAddress()
+
+
Short description
+
+

Preset IP to ping (see 'ping' function).

+
+
+
+
Parameters
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

ping_ip

STRING

1

IP

+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.setPingIPAddress(ping_ip)
+
+
+
+
+
+
CSK_DeviceNetworkConfig.setSubnetMask()
+
+
Short description
+
+

Preset subnet mask to be configured via 'applyConfig'.

+
+
+
+
Parameters
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

newSubnetMask

STRING

1

Subnet mask

+
+
+
Sample (auto-generated)
+
+
+
CSK_DeviceNetworkConfig.setSubnetMask(newSubnetMask)
+
+
+
+
+
+
+

Events

+
+
CSK_DeviceNetworkConfig.OnApplyButtonDisabled
+
+
Short description
+
+

Notified to disable / enable 'Apply new config' button in UI.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

isDisabled

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnApplyButtonDisabled(isDisabled)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnApplyButtonDisabled", "handleOnApplyButtonDisabled")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnDHCPDisabled
+
+
Short description
+
+

Notified to disable / enable 'DHCP' checkbox in UI.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

isDisabled

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnDHCPDisabled(isDisabled)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnDHCPDisabled", "handleOnDHCPDisabled")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnGatewayDisabled
+
+
Short description
+
+

Notified to disable / enable 'Gateway' text field in UI.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

isDisabled

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnGatewayDisabled(isDisabled)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnGatewayDisabled", "handleOnGatewayDisabled")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnGatewayError
+
+
Short description
+
+

Highlights the 'Gateway field' in UI if format of gateway is not correct.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

isError

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnGatewayError(isError)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnGatewayError", "handleOnGatewayError")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnIPDisabled
+
+
Short description
+
+

Notified to disable / enable 'IP' text field in UI.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

isDisabled

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnIPDisabled(isDisabled)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnIPDisabled", "handleOnIPDisabled")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnIPError
+
+
Short description
+
+

Highlights the 'IP' in UI if format of IP is not correct.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

isError

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnIPError(isError)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnIPError", "handleOnIPError")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewDefaultGateway
+
+
Short description
+
+

Notify current 'Default Gateway'

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

DefaultGateway

STRING

1

Default Gateway.

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewDefaultGateway(DefaultGateway)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewDefaultGateway", "handleOnNewDefaultGateway")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewDHCPStatus
+
+
Short description
+
+

Notify current DHCP status.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

DHCPStatus

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewDHCPStatus(DHCPStatus)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewDHCPStatus", "handleOnNewDHCPStatus")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewEthernetConfigStatus
+
+
Short description
+
+

Notify current configuration process status.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

newEthernetConfigStatus

STRING

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewEthernetConfigStatus(newEthernetConfigStatus)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewEthernetConfigStatus", "handleOnNewEthernetConfigStatus")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewInterfaceChoice
+
+
Short description
+
+

Notify currently selected interface.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

NewInterface

STRING

1

The selected interface.

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewInterfaceChoice(NewInterface)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewInterfaceChoice", "handleOnNewInterfaceChoice")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewInterfaceTable
+
+
Short description
+
+

Notify interface table as JSON (e.g. for table in UI).

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

InterfaceTable

STRING

1

Table of interfaces as a JSON string.

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewInterfaceTable(InterfaceTable)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewInterfaceTable", "handleOnNewInterfaceTable")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewIP
+
+
Short description
+
+

Notify current IP.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

ip

STRING

1

IP

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewIP(ip)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewIP", "handleOnNewIP")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewPingDetails
+
+
Short description
+
+

Notify details of executed ping command.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

details

STRING

1

Ping details.

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewPingDetails(details)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewPingDetails", "handleOnNewPingDetails")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewPingResult
+
+
Short description
+
+

Notify result of executed ping command.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

result

BOOL

1

Result

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewPingResult(result)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewPingResult", "handleOnNewPingResult")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnNewSubnetMask
+
+
Short description
+
+

Notify current subnet mask.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

SubnetMask

STRING

1

Subnet mask.

+
+
+
Sample (auto-generated)
+
+
+
function handleOnNewSubnetMask(SubnetMask)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnNewSubnetMask", "handleOnNewSubnetMask")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnSubnetDisabled
+
+
Short description
+
+

Notified to disable / enable 'Subnet mask' text field in UI.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

isDisabled

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnSubnetDisabled(isDisabled)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnSubnetDisabled", "handleOnSubnetDisabled")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnSubnetError
+
+
Short description
+
+

Highlights the 'Subnet' field in UI if format is not correct.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

isError

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnSubnetError(isError)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnSubnetError", "handleOnSubnetError")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnUserLevelAdminActive
+
+
Short description
+
+

Status of Admin userlevel. Used internally in combination with the CSK_UserManagement module if available.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

status

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnUserLevelAdminActive(status)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnUserLevelAdminActive", "handleOnUserLevelAdminActive")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnUserLevelMaintenanceActive
+
+
Short description
+
+

Status of Maintenance userlevel. Used internally in combination with the CSK_UserManagement module if available.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

status

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnUserLevelMaintenanceActive(status)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnUserLevelMaintenanceActive", "handleOnUserLevelMaintenanceActive")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnUserLevelOperatorActive
+
+
Short description
+
+

Status of Operator userlevel. Used internally in combination with the CSK_UserManagement module if available.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

status

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnUserLevelOperatorActive(status)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnUserLevelOperatorActive", "handleOnUserLevelOperatorActive")
+
+
+
+
+
+
CSK_DeviceNetworkConfig.OnUserLevelServiceActive
+
+
Short description
+
+

Status of Service userlevel. Used internally in combination with the CSK_UserManagement module if available.

+
+
+
+
Callback arguments
+ ++++++ + + + + + + + + + + + + + + + + +
NameTypeMultiplicityDescription

status

BOOL

1

Status

+
+
+
Sample (auto-generated)
+
+
+
function handleOnUserLevelServiceActive(status)
+  -- Do something
+end
+
+Script.register("CSK_DeviceNetworkConfig.OnUserLevelServiceActive", "handleOnUserLevelServiceActive")
+
+
+
+
+
+
+
+

CSK_Module_DeviceNetworkConfig

+
+

Short description

+
+

This is an automatically generated CROWN (description not necessary).

+
+
+
+

Overview

+ +
+
+
+
+
+ + + + diff --git a/docu/media/UI_Screenshot.png b/docu/media/UI_Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c6af2d931e11d3311b394b349d87a270c0b289ac GIT binary patch literal 44832 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 UhKSSc0E^C_HPp*F^J~!G0atgUga7~l literal 0 HcmV?d00001