├── .gitignore
├── Apps
└── GrafanaDataProvider
│ ├── GrafanaDataProvider.sln
│ ├── GrafanaDataProvider
│ ├── AppSettings.cs
│ ├── App_Start
│ │ └── WebApiConfig.cs
│ ├── Controllers
│ │ ├── PasswordsController.cs
│ │ └── TrendsController.cs
│ ├── Global.asax
│ ├── Global.asax.cs
│ ├── GrafanaDataProvider.csproj
│ ├── Models
│ │ ├── GrafanaArg.cs
│ │ ├── Password.cs
│ │ ├── Range.cs
│ │ ├── Targets.cs
│ │ └── TrendData.cs
│ ├── Properties
│ │ └── AssemblyInfo.cs
│ ├── Web.Debug.config
│ ├── Web.Release.config
│ ├── Web.config
│ ├── index.html
│ ├── packages.config
│ └── password.html
│ └── README.md
├── Drivers
├── KpMqtt
│ ├── KpMqtt.sln
│ ├── KpMqtt
│ │ ├── Config
│ │ │ ├── KpMqtt_001.xml
│ │ │ └── KpMqtt_Job.js
│ │ ├── KpMqtt.csproj
│ │ ├── KpMqttLogic.cs
│ │ ├── KpMqttView.cs
│ │ ├── Mqtt
│ │ │ └── Config
│ │ │ │ ├── CmdType.cs
│ │ │ │ ├── DeviceConfig.cs
│ │ │ │ ├── MqttPubCmd.cs
│ │ │ │ ├── MqttPubParam.cs
│ │ │ │ ├── MqttPubTopic.cs
│ │ │ │ ├── MqttSubCmd.cs
│ │ │ │ ├── MqttSubJS.cs
│ │ │ │ ├── MqttSubTopic.cs
│ │ │ │ ├── MqttTopic.cs
│ │ │ │ └── PubBehavior.cs
│ │ ├── Properties
│ │ │ └── AssemblyInfo.cs
│ │ ├── StriderMqtt
│ │ │ ├── ConnectionPackets.cs
│ │ │ ├── Constants.cs
│ │ │ ├── EventArgs.cs
│ │ │ ├── Exceptions.cs
│ │ │ ├── MqttConnection.cs
│ │ │ ├── PacketBase.cs
│ │ │ ├── PacketFactory.cs
│ │ │ ├── PacketReader.cs
│ │ │ ├── PacketWriter.cs
│ │ │ ├── Persistence.cs
│ │ │ ├── PublishPackets.cs
│ │ │ ├── SubscriptionPackets.cs
│ │ │ └── Transport.cs
│ │ └── packages.config
│ ├── README-RU.md
│ └── README.md
└── drivers.txt
├── Formulas
├── BasicFormulas
│ ├── Avg.cs
│ ├── BasicFormulas.csproj
│ ├── Prev.cs
│ └── Properties
│ │ └── AssemblyInfo.cs
├── BasicFormulasTest
│ ├── AvgTest.cs
│ ├── BasicFormulasTest.csproj
│ ├── Properties
│ │ └── AssemblyInfo.cs
│ └── packages.config
└── Formulas.sln
├── LICENSE
├── Modules
└── modules.txt
├── Plugins
├── CustomPageExample
│ ├── custom
│ │ ├── MyPage.html
│ │ ├── mypage.css
│ │ └── mypage.js
│ └── readme.txt
└── plugins.txt
├── README.md
└── Samples
└── WebApiClientSample
├── WebApiClientSample.sln
└── WebApiClientSample
├── Program.cs
└── WebApiClientSample.csproj
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.sln.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
331 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.28307.421
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrafanaDataProvider", "GrafanaDataProvider\GrafanaDataProvider.csproj", "{0C9F1214-4B42-4515-AB2F-D1637E57D911}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {0C9F1214-4B42-4515-AB2F-D1637E57D911}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {0C9F1214-4B42-4515-AB2F-D1637E57D911}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {0C9F1214-4B42-4515-AB2F-D1637E57D911}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {0C9F1214-4B42-4515-AB2F-D1637E57D911}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {7560569E-35FB-4FD5-9B14-6DA53D56AD08}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/AppSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Specialized;
3 | using System.Configuration;
4 | using Scada;
5 | using Scada.Client;
6 |
7 | namespace GrafanaDataProvider
8 | {
9 | ///
10 | /// Represents application settings.
11 | ///
12 | public class AppSettings : CommSettings
13 | {
14 | ///
15 | /// Initializes connection parameters.
16 | ///
17 | public AppSettings()
18 | : base()
19 | {
20 |
21 | }
22 |
23 |
24 | ///
25 | /// Gets the specified parameter from the collection.
26 | ///
27 | private string GetParameter(NameValueCollection settings, string paramName)
28 | {
29 | try
30 | {
31 | return settings[paramName];
32 | }
33 | catch (Exception ex)
34 | {
35 | throw new Exception(string.Format("Error retrieving parameter \"{0}\": {1}",
36 | paramName, ex.Message));
37 | }
38 | }
39 |
40 | ///
41 | /// Loads settings from Web.config.
42 | ///
43 | public bool Load(out string errMsg)
44 | {
45 | try
46 | {
47 | SetToDefault();
48 | NameValueCollection settings = ConfigurationManager.AppSettings;
49 | ServerHost = GetParameter(settings, "serverHost");
50 | ServerPort = Convert.ToInt32(GetParameter(settings, "serverPort"));
51 | ServerUser = GetParameter(settings, "serverUser");
52 | ServerPwd = ScadaUtils.Decrypt(GetParameter(settings, "Password"));
53 | ServerTimeout = Convert.ToInt32(GetParameter(settings, "serverTimeout"));
54 |
55 | errMsg = "";
56 | return true;
57 | }
58 | catch (Exception ex)
59 | {
60 | errMsg = "Error loading application settings: " + ex.Message;
61 | return false;
62 | }
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/App_Start/WebApiConfig.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Web.Http;
3 |
4 | namespace GrafanaDataProvider
5 | {
6 | public static class WebApiConfig
7 | {
8 | public static void Register(HttpConfiguration config)
9 | {
10 | // Web API configuration and services
11 |
12 | // Web API routes
13 | config.MapHttpAttributeRoutes();
14 |
15 | config.Routes.MapHttpRoute(
16 | name: "DefaultApi",
17 | routeTemplate: "api/{controller}/{id}",
18 | defaults: new { id = RouteParameter.Optional }
19 | );
20 |
21 | var xmlFormatter = config.Formatters.XmlFormatter;
22 | var appXmlType = xmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
23 | xmlFormatter.SupportedMediaTypes.Remove(appXmlType);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Controllers/PasswordsController.cs:
--------------------------------------------------------------------------------
1 | using System.Web.Http;
2 | using Scada;
3 | using GrafanaDataProvider.Models;
4 |
5 | namespace GrafanaDataProvider.Controllers
6 | {
7 | ///
8 | /// Represents a controller for password encryption.
9 | ///
10 | public class PasswordsController : ApiController
11 | {
12 | [HttpGet]
13 | [Route("api/passwords/{password}")]
14 | public Password GetEncryptor(string password)
15 | {
16 | return new Password
17 | {
18 | password = password,
19 | encryptedPassword = ScadaUtils.Encrypt(password)
20 | };
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Controllers/TrendsController.cs:
--------------------------------------------------------------------------------
1 | using GrafanaDataProvider.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Web.Http;
5 | using Scada.Client;
6 | using Scada.Data.Tables;
7 | using Utils;
8 | using System.Text;
9 | using System.IO;
10 | using Scada.Data.Models;
11 |
12 | namespace GrafanaDataProvider.Controllers
13 | {
14 | ///
15 | /// Represents a controller for plotting a graph.
16 | ///
17 | public class TrendsController : ApiController
18 | {
19 | ///
20 | /// The application log file name.
21 | ///
22 | private const string LogFileName = "GrafanaGrafic.log";
23 |
24 | ///
25 | /// The application log.
26 | ///
27 | private static readonly Log Log;
28 |
29 | ///
30 | /// Communicates with the Server application.
31 | ///
32 | protected static readonly ServerComm serverComm;
33 |
34 | ///
35 | /// Cache of the data received from SCADA-Server for clients usage.
36 | ///
37 | protected static readonly DataCache dataCache;
38 |
39 | ///
40 | /// The object for thread-safe access to client cache data.
41 | ///
42 | protected static readonly DataAccess dataAccess;
43 |
44 |
45 | ///
46 | /// Initializes the class.
47 | ///
48 | static TrendsController()
49 | {
50 | string path = AppDomain.CurrentDomain.BaseDirectory;
51 | string LogDir = path + "log" + Path.DirectorySeparatorChar;
52 | Log = new Log(Log.Formats.Simple) { Encoding = Encoding.UTF8 };
53 | Log.FileName = LogDir + LogFileName;
54 |
55 | AppSettings appSettings = new AppSettings();
56 | Log = new Log(Log.Formats.Simple) { Encoding = Encoding.UTF8 };
57 | Log.FileName = LogDir + LogFileName;
58 |
59 | if (!appSettings.Load(out string errMsg))
60 | Log.WriteAction(errMsg, Log.ActTypes.Exception);
61 | else
62 | {
63 | CommSettings settings = new CommSettings(appSettings.ServerHost, appSettings.ServerPort, appSettings.ServerUser,
64 | appSettings.ServerPwd, appSettings.ServerTimeout);
65 | serverComm = new ServerComm(settings, Log);
66 | dataCache = new DataCache(serverComm, Log);
67 | dataAccess = new DataAccess(dataCache, Log);
68 | }
69 | }
70 |
71 | #if DEBUG
72 | ///
73 | /// A simple example of building graphics.
74 | ///
75 | private static List PointCalc(GrafanaArg grafArg)
76 | {
77 | long t1;
78 | long t0;
79 |
80 | if (grafArg.range == null)
81 | {
82 | t1 = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
83 | t0 = t1 - 900000; // 15 min
84 | }
85 | else
86 | {
87 | t1 = GetUnixTimeMs(grafArg.range.to);
88 | t0 = GetUnixTimeMs(grafArg.range.from);
89 | }
90 |
91 | List points = new List();
92 |
93 | int step = 1000;
94 | if (grafArg.intervalMs > 0)
95 | step = grafArg.intervalMs;
96 |
97 | while (t0 < t1)
98 | {
99 | DateTimeOffset offset = DateTimeOffset.FromUnixTimeMilliseconds(t0);
100 | int day = offset.Day;
101 | int hour = offset.Hour;
102 |
103 | double time = t0;
104 | //double y = 100 * Math.Sin(time * 0.0001);
105 | double y = day * 100 + hour + 0.1 * Math.Sin(time * 0.0001);
106 |
107 | points.Add(new double?[] { y, time });
108 | t0 += step;
109 | }
110 |
111 | return points;
112 | }
113 | #endif
114 |
115 | ///
116 | /// Requests input channel data from Server.
117 | ///
118 | private static Trend GetTrend(DateTime date, int cnlNum, bool chekHours)
119 | {
120 | string tableName = chekHours ?
121 | SrezAdapter.BuildHourTableName(date) :
122 | SrezAdapter.BuildMinTableName(date);
123 |
124 | Trend trend = new Trend(cnlNum);
125 | bool dataReceived = serverComm.ReceiveTrend(tableName, date, trend);
126 |
127 | //serverComm.Close();
128 |
129 | if (dataReceived)
130 | trend.LastFillTime = DateTime.UtcNow;
131 | else
132 | Log.WriteError("Unable to receive trend.");
133 |
134 | return trend;
135 | }
136 |
137 | ///
138 | /// Returns an empty list.
139 | ///
140 | private IEnumerable GetEmptyTrend()
141 | {
142 | TrendData[] trends = new TrendData[]
143 | {
144 | new TrendData { target = "-1", datapoints = null }
145 | };
146 | return trends;
147 | }
148 |
149 | ///
150 | /// Converts the specified date and time to the unix milliseconds.
151 | ///
152 | private static long GetUnixTimeMs(DateTime dateTime)
153 | {
154 | return new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
155 | }
156 |
157 | ///
158 | /// Converts the specified date and time to the local time.
159 | ///
160 | private static DateTime UtcToLocalTime(DateTime dateTime)
161 | {
162 | return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc).ToLocalTime();
163 | }
164 |
165 | ///
166 | /// Converts local date and time to the univeral time.
167 | ///
168 | private static DateTime LocalToUtcTime(DateTime dateTime)
169 | {
170 | return DateTime.SpecifyKind(dateTime, DateTimeKind.Local).ToUniversalTime();
171 | }
172 |
173 | ///
174 | /// Determine the type of archive, hourly or minute.
175 | ///
176 | private static void SelectArcType(GrafanaArg grafArg, out bool isHour, out int coef)
177 | {
178 | long diff = GetUnixTimeMs(grafArg.range.to) - GetUnixTimeMs(grafArg.range.from);
179 |
180 | // more than 24 h
181 | if (diff / 60000 > 1440)
182 | {
183 | coef = 60;
184 | isHour = true;
185 | }
186 | else
187 | {
188 | coef = 1;
189 | isHour = false;
190 | }
191 | }
192 |
193 | ///
194 | /// Iterates through the dates.
195 | ///
196 | private IEnumerable EachDay(DateTime from, DateTime to)
197 | {
198 | for (DateTime day = from; day.Date <= to; day = day.AddDays(1))
199 | {
200 | yield return day;
201 | }
202 | }
203 |
204 | ///
205 | /// Gets points of data on specific channels number.
206 | ///
207 | [HttpGet, HttpPost]
208 | public IEnumerable GetDataForGrafana([FromBody]GrafanaArg grafanaArg)
209 | {
210 | if (grafanaArg == null)
211 | {
212 | return GetEmptyTrend();
213 | }
214 | else
215 | {
216 | if (grafanaArg.targets == null)
217 | {
218 | Log.WriteError("It is not possible to receive data");
219 | return GetEmptyTrend();
220 | }
221 | else
222 | {
223 | SelectArcType(grafanaArg, out bool isHour, out int timeCoef);
224 | TrendData[] trends = new TrendData[grafanaArg.targets.Length];
225 | long fromMs = GetUnixTimeMs(grafanaArg.range.from);
226 | long toMs = GetUnixTimeMs(grafanaArg.range.to);
227 |
228 | for (int i = 0; i < grafanaArg.targets.Length; i++)
229 | {
230 | List points = new List();
231 | if (!int.TryParse(grafanaArg.targets[i].target.Trim(), out int cnlNum))
232 | {
233 | Log.WriteError("It is not possible to read the dates for the channel " + cnlNum);
234 | trends[i] = new TrendData { target = "-1", datapoints = null };
235 | }
236 | else
237 | {
238 | foreach (DateTime date in EachDay (grafanaArg.range.from, grafanaArg.range.to))
239 | {
240 | Trend trend = GetTrend(UtcToLocalTime(date), cnlNum, isHour);
241 |
242 | for (int i1 = 0; i1 < trend.Points.Count; i1++)
243 | {
244 | long pointMs = GetUnixTimeMs(LocalToUtcTime(trend.Points[i1].DateTime));
245 |
246 | if (pointMs >= fromMs && pointMs <= toMs)
247 | {
248 | if (i1 > 0)
249 | {
250 | long prevMs = GetUnixTimeMs(LocalToUtcTime(trend.Points[i1 - 1].DateTime));
251 |
252 | if (pointMs - prevMs > timeCoef * 60000)
253 | {
254 | points.Add(new double?[] { null, prevMs + timeCoef * 60000 });
255 | }
256 | else
257 | {
258 | if (trend.Points[i1].Stat > 0)
259 | points.Add(new double?[] { trend.Points[i1].Val, pointMs });
260 | else
261 | points.Add(new double?[] { null, pointMs });
262 | }
263 | }
264 | else
265 | {
266 | if (trend.Points[i1].Stat > 0)
267 | points.Add(new double?[] { trend.Points[i1].Val, pointMs });
268 | else
269 | points.Add(new double?[] { null, pointMs });
270 | }
271 | }
272 | }
273 | }
274 |
275 | InCnlProps inCnlProps = dataAccess.GetCnlProps(cnlNum);
276 | string cnlName = inCnlProps == null ? "" : inCnlProps.CnlName;
277 |
278 | trends[i] = new TrendData { target = "[" + cnlNum + "] " + cnlName, datapoints = points };
279 | Log.WriteAction("Channel data received " + cnlNum);
280 | }
281 | }
282 |
283 | return trends;
284 | }
285 | }
286 | }
287 | }
288 | }
289 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Global.asax:
--------------------------------------------------------------------------------
1 | <%@ Application Codebehind="Global.asax.cs" Inherits="GrafanaDataProvider.WebApiApplication" Language="C#" %>
2 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Global.asax.cs:
--------------------------------------------------------------------------------
1 | using System.Web.Http;
2 |
3 | namespace GrafanaDataProvider
4 | {
5 | public class WebApiApplication : System.Web.HttpApplication
6 | {
7 | protected void Application_Start()
8 | {
9 | GlobalConfiguration.Configure(WebApiConfig.Register);
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/GrafanaDataProvider.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 |
7 |
8 | 2.0
9 | {0C9F1214-4B42-4515-AB2F-D1637E57D911}
10 | {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
11 | Library
12 | Properties
13 | GrafanaDataProvider
14 | GrafanaDataProvider
15 | v4.7.2
16 | true
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | true
28 | full
29 | false
30 | bin\
31 | DEBUG;TRACE
32 | prompt
33 | 4
34 |
35 |
36 | true
37 | pdbonly
38 | true
39 | bin\
40 | TRACE
41 | prompt
42 | 4
43 |
44 |
45 |
46 | ..\..\..\..\scada\Log\Log\bin\Release\Log.dll
47 |
48 |
49 | ..\..\..\..\scada\ScadaData\ScadaData\bin\Release\ScadaData.dll
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | ..\packages\Newtonsoft.Json.11.0.1\lib\net45\Newtonsoft.Json.dll
69 |
70 |
71 | ..\packages\Microsoft.AspNet.WebApi.Client.5.2.4\lib\net45\System.Net.Http.Formatting.dll
72 |
73 |
74 | ..\packages\Microsoft.AspNet.WebApi.Core.5.2.4\lib\net45\System.Web.Http.dll
75 |
76 |
77 | ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.4\lib\net45\System.Web.Http.WebHost.dll
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Global.asax
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | Web.config
106 |
107 |
108 | Web.config
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | 10.0
117 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | True
127 | True
128 | 61517
129 | /
130 | http://localhost:61517/
131 | False
132 | False
133 |
134 |
135 | False
136 |
137 |
138 |
139 |
140 |
147 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Models/GrafanaArg.cs:
--------------------------------------------------------------------------------
1 | namespace GrafanaDataProvider.Models
2 | {
3 | ///
4 | /// Represents parameters passed by Grafana.
5 | ///
6 | public class GrafanaArg
7 | {
8 | public string app { get; set; }
9 | public int intervalMs { get; set; }
10 | public Range range { get; set; }
11 | public Targets[] targets { get; set; }
12 | public long startTime { get; set; }
13 | public long endTime { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Models/Password.cs:
--------------------------------------------------------------------------------
1 | namespace GrafanaDataProvider.Models
2 | {
3 | ///
4 | /// Represents password to connect to the server.
5 | ///
6 | public class Password
7 | {
8 | public string password { get; set; }
9 | public string encryptedPassword { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Models/Range.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace GrafanaDataProvider.Models
4 | {
5 | ///
6 | /// Represents time period for graph.
7 | ///
8 | public class Range
9 | {
10 | public /*string*/DateTime from { get; set; }
11 | public /*string*/DateTime to { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Models/Targets.cs:
--------------------------------------------------------------------------------
1 | namespace GrafanaDataProvider.Models
2 | {
3 | ///
4 | /// Represents target options.
5 | ///
6 | public class Targets
7 | {
8 | public string target { get; set; }
9 | public string refId { get; set; }
10 | public string hide { get; set; }
11 | public string type { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Models/TrendData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace GrafanaDataProvider.Models
4 | {
5 | ///
6 | /// Represents parameters transmitted to Grafana for build graph.
7 | ///
8 | public class TrendData
9 | {
10 | public string target { get; set; }
11 | public List datapoints { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | // General Information about an assembly is controlled through the following
5 | // set of attributes. Change these attribute values to modify the information
6 | // associated with an assembly.
7 | [assembly: AssemblyTitle("GrafanaDataProvider")]
8 | [assembly: AssemblyDescription("")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("")]
11 | [assembly: AssemblyProduct("Rapid SCADA")]
12 | [assembly: AssemblyCopyright("Copyright © 2020")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // Setting ComVisible to false makes the types in this assembly not visible
17 | // to COM components. If you need to access a type in this assembly from
18 | // COM, set the ComVisible attribute to true on that type.
19 | [assembly: ComVisible(false)]
20 |
21 | // The following GUID is for the ID of the typelib if this project is exposed to COM
22 | [assembly: Guid("0c9f1214-4b42-4515-ab2f-d1637e57d911")]
23 |
24 | // Version information for an assembly consists of the following four values:
25 | //
26 | // Major Version
27 | // Minor Version
28 | // Build Number
29 | // Revision
30 | //
31 | // You can specify all the values or you can default the Revision and Build Numbers
32 | // by using the '*' as shown below:
33 | [assembly: AssemblyVersion("5.0.1.1")]
34 | [assembly: AssemblyFileVersion("5.0.1.1")]
35 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Web.Debug.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
18 |
29 |
30 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Web.Release.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
18 |
19 |
30 |
31 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/Web.config:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Grafana Data Provider
6 |
7 |
8 |
11 |
12 |
16 |
17 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/GrafanaDataProvider/password.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Password Encryption
6 |
7 |
27 |
28 |
29 | Password Encryption
30 | Given string:
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Apps/GrafanaDataProvider/README.md:
--------------------------------------------------------------------------------
1 | A Simple Example of Integrating Grafana and Rapid SCADA
2 | =============================
3 | Grafana is the open source analytics and monitoring solution for every database.
4 |
5 | Sequencing
6 | ------------------
7 | - **Installation Grafana**
8 |
9 | Download Grafana from the link https://grafana.com/grafana/download Here you will find detailed installation and configuration instructions.
10 |
11 | Set parameter **allow_embedding = true** in file defaults.ini wich you can find C:\Program Files\GrafanaLabs\grafana\conf
12 |
13 | - **Install the Simple Jason plugin**
14 |
15 | Simple JSON Datasource - a generic backend datasource
16 |
17 | You can download the plugin at https://grafana.com/grafana/plugins/grafana-simple-json-datasource. A detailed description, as well as installation and configuration instructions, are also available here.
18 |
19 | - **Description of work with GrafanaDataProvider**
20 |
21 | 1. Configure access to the server SCADA file Web.config in GrafanaDataProvider project.
22 | ```xml
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ```
31 |
32 | 2. Password must be encrypted.
33 | To encrypt the password, you can use the index.html service page GrafanaDataProvider.
34 |
35 | **3. The manual installation sequence:**
36 | - Install Microsoft Internet Information Services (IIS) by selecting the appropriate Windows features.
37 | - Install Microsoft .NET Framework (install IIS before the framework) of the version specified in System Requirements.
38 | - Unzip files from the GrafanaDataProvider folder of the installation pachage to the SCADA installation directory. Recommended destination is C:\SCADA
39 | - Open IIS management console: Control Panel > System and Security > Administrative Tools > Internet Information Services (IIS) Manager.
40 | - Add a web application to the site tree. Right click the appropriate site, Default Web Site in most cases, then choose the Add Application menu item.
41 | - Enter the application alias: Scada. Check that the selected application pool uses .NET 4.0 runtime version and integrated pipeline mode. Specify the physical path to the web application files: C:\SCADA\GrafanaDataProvider. Then click OK.
42 |
43 | 4. Configure DataSource SimpleJason in Grafana Server.
44 | Create a data source - select the previously installed SimpleJason plugin.
45 | Set HTPP URL: http: http://localhost/GrafanaDataProvider/api/trends
46 |
47 | 5. Build the graph. Select a database source (Query) SimpleJason. Timeseries: 101 (here enter the channel number on which you want to plot the graph). It is important to enter the channel digital values. Choose the time range for which you want to get data. It is also possible to plot along multiple channels.
48 |
49 | 6. It is important to set the visualization parameter during settings:
50 | Stacking & Null value - set Null value = null. This makes it possible to see a graph with a line break, in that time period where no data were received.
51 |
52 | 7. In Grafana get Api Key, when setting up Configuration the and run the script as:
53 |
54 | ```script
55 | curl -H "Authorization: Bearer eyJrIjoiVjkyRjd2a2dSQW81ZU51QW5pbDR5WmxESUNDWUY0Z0UiLCJuIjoiTXktV2ViU2l0ZS1Nb25pdG9yaW5nIiwiaWQiOjFash//{path for grafana graph}
56 | ```
57 |
58 | This is just an example.
59 |
60 | 7. Get a link to your graph in grafana. Panel Title -> Share->Copy.
61 |
62 | 8. In the application Administrator Application, add a link to the graph.
63 |
64 | If during operation you find any problems in the operation of this service, please inform at https://forum.rapidscada.org/
65 |
66 | https://www.youtube.com/watch?v=fC1lKPcui4A&t=15s
67 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.28307.572
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KpMqtt", "KpMqtt\KpMqtt.csproj", "{DCE0A39F-172B-4EBC-965A-8B858F80B165}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {DCE0A39F-172B-4EBC-965A-8B858F80B165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {DCE0A39F-172B-4EBC-965A-8B858F80B165}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {DCE0A39F-172B-4EBC-965A-8B858F80B165}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {DCE0A39F-172B-4EBC-965A-8B858F80B165}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {DE8DF865-E1CC-48B7-BD01-213AC203CC0B}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Config/KpMqtt_001.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Config/KpMqtt_Job.js:
--------------------------------------------------------------------------------
1 | // Data example
2 | // { "Val": 12.3, "Stat": 1 }
3 | var data = JSON.parse(InMsg);
4 |
5 | jsvals[0].TagName = "My tag";
6 | jsvals[0].Stat = data.Stat;
7 | jsvals[0].Val = data.Val;
8 |
9 | mylog("Script completed successfully");
10 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/KpMqtt.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {DCE0A39F-172B-4EBC-965A-8B858F80B165}
8 | Library
9 | Properties
10 | Scada.Comm.Devices
11 | KpMqtt
12 | v4.7.2
13 | 512
14 | true
15 |
16 |
17 |
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 | ..\packages\Jint.2.11.58\lib\net451\Jint.dll
37 |
38 |
39 | ..\..\..\..\scada\Log\Log\bin\Release\Log.dll
40 |
41 |
42 |
43 | ..\..\..\..\scada\ScadaComm\ScadaComm\ScadaCommCommon\bin\Release\ScadaCommCommon.dll
44 |
45 |
46 | ..\..\..\..\scada\ScadaData\ScadaData\bin\Release\ScadaData.dll
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | PreserveNewest
87 |
88 |
89 | PreserveNewest
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/KpMqttView.cs:
--------------------------------------------------------------------------------
1 | using Scada.Comm.Devices.Mqtt.Config;
2 | using Scada.Data.Configuration;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Reflection;
6 | using System.Text;
7 |
8 | namespace Scada.Comm.Devices
9 | {
10 | public class KpMqttView : KPView
11 | {
12 | internal const string KpVersion = "5.0.4.1";
13 |
14 |
15 | public KpMqttView() :
16 | this(0)
17 | {
18 | }
19 |
20 | public KpMqttView(int number)
21 | : base(number)
22 | {
23 | CanShowProps = number > 0;
24 | }
25 |
26 |
27 | public override string KPDescr
28 | {
29 | get
30 | {
31 | return Localization.UseRussian ?
32 | "Подписка и публикация данных по протоколу MQTT." :
33 | "Subscribes and publishes data using the MQTT protocol.";
34 | }
35 | }
36 |
37 | public override string Version
38 | {
39 | get
40 | {
41 | return KpVersion;
42 | }
43 | }
44 |
45 | public override KPReqParams DefaultReqParams
46 | {
47 | get
48 | {
49 | return new KPReqParams(10000, 500);
50 | }
51 | }
52 |
53 | public override KPCnlPrototypes DefaultCnls
54 | {
55 | get
56 | {
57 | // load configuration
58 | DeviceConfig deviceConfig = new DeviceConfig();
59 | string configFileName = DeviceConfig.GetFileName(AppDirs.ConfigDir, Number, KPProps.CmdLine);
60 |
61 | if (!File.Exists(configFileName))
62 | return null;
63 | else if (!deviceConfig.Load(configFileName, out string errMsg))
64 | throw new ScadaException(errMsg);
65 |
66 | // create channel prototypes
67 | KPCnlPrototypes prototypes = new KPCnlPrototypes();
68 | int signal = 1;
69 |
70 | void AddCnl(string name)
71 | {
72 | prototypes.InCnls.Add(new InCnlPrototype(name, BaseValues.CnlTypes.TI)
73 | {
74 | Signal = signal++
75 | });
76 | }
77 |
78 | deviceConfig.SubTopics.ForEach(t => AddCnl(t.TopicName));
79 | deviceConfig.SubJSs.ForEach(t =>
80 | {
81 | for (int i = 0; i < t.CnlCnt; i++)
82 | {
83 | AddCnl(t.TopicName + " [" + i + "]");
84 | }
85 | });
86 |
87 | return prototypes;
88 | }
89 | }
90 |
91 |
92 | public override void ShowProps()
93 | {
94 | // create configuration file if it doesn't exist
95 | string configFileName = DeviceConfig.GetFileName(AppDirs.ConfigDir, Number, KPProps.CmdLine);
96 |
97 | if (!File.Exists(configFileName))
98 | {
99 | string resourceName = "Scada.Comm.Devices.Config.KpMqtt_001.xml";
100 | string fileContents;
101 |
102 | using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
103 | {
104 | using (StreamReader reader = new StreamReader(stream))
105 | {
106 | fileContents = reader.ReadToEnd();
107 | }
108 | }
109 |
110 | File.WriteAllText(configFileName, fileContents, Encoding.UTF8);
111 | }
112 |
113 | // open configuration directory
114 | Process.Start(AppDirs.ConfigDir);
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/CmdType.cs:
--------------------------------------------------------------------------------
1 | namespace Scada.Comm.Devices.Mqtt.Config
2 | {
3 | internal enum CmdType
4 | {
5 | St,
6 | BinTxt,
7 | BinHex,
8 | Req
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/DeviceConfig.cs:
--------------------------------------------------------------------------------
1 | using StriderMqtt;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Xml;
6 |
7 | namespace Scada.Comm.Devices.Mqtt.Config
8 | {
9 | ///
10 | /// Represents a driver configuration for a device.
11 | /// Представляет конфигурацию драйвера для устройства.
12 | ///
13 | internal class DeviceConfig
14 | {
15 | ///
16 | /// Initializes a new instance of the class.
17 | ///
18 | public DeviceConfig()
19 | {
20 | SetToDefault();
21 | }
22 |
23 |
24 | ///
25 | /// Gets the MQTT connection options.
26 | ///
27 | public MqttConnectionArgs ConnectionArgs { get; private set; }
28 |
29 | ///
30 | /// Gets the subscriptions.
31 | ///
32 | public List SubTopics { get; private set; }
33 |
34 | ///
35 | /// Gets the subscriptions processed by JavaScript.
36 | ///
37 | public List SubJSs { get; private set; }
38 |
39 | ///
40 | /// Gets the topics to publish.
41 | ///
42 | public List PubTopics { get; private set; }
43 |
44 | ///
45 | /// Gets the commands to publish data when a telecommand is sent.
46 | ///
47 | public List PubCmds { get; private set; }
48 |
49 | ///
50 | /// Gets the commands sent to Server when new data is received.
51 | ///
52 | public List SubCmds { get; private set; }
53 |
54 |
55 | ///
56 | /// Sets the default values.
57 | ///
58 | private void SetToDefault()
59 | {
60 | ConnectionArgs = new MqttConnectionArgs();
61 | SubTopics = new List();
62 | PubTopics = new List();
63 | PubCmds = new List();
64 | SubCmds = new List();
65 | SubJSs = new List();
66 | }
67 |
68 | ///
69 | /// Loads the configuration from the specified file.
70 | ///
71 | public bool Load(string fileName, out string errMsg)
72 | {
73 | try
74 | {
75 | SetToDefault();
76 |
77 | if (!File.Exists(fileName))
78 | throw new FileNotFoundException(string.Format(CommonPhrases.NamedFileNotFound, fileName));
79 |
80 | XmlDocument xmlDoc = new XmlDocument();
81 | xmlDoc.Load(fileName);
82 | XmlElement rootElem = xmlDoc.DocumentElement;
83 |
84 | if (rootElem.SelectSingleNode("MqttParams") is XmlElement mqttParamsElem)
85 | {
86 | ConnectionArgs.ClientId = mqttParamsElem.GetAttribute("ClientID");
87 | ConnectionArgs.Hostname = mqttParamsElem.GetAttribute("Hostname");
88 | ConnectionArgs.Port = mqttParamsElem.GetAttrAsInt("Port");
89 | ConnectionArgs.Username = mqttParamsElem.GetAttribute("UserName");
90 | ConnectionArgs.Password = mqttParamsElem.GetAttribute("Password");
91 | }
92 |
93 | if (xmlDoc.DocumentElement.SelectSingleNode("MqttSubTopics") is XmlNode mqttSubTopicsNode)
94 | {
95 | foreach (XmlElement topicElem in mqttSubTopicsNode.SelectNodes("Topic"))
96 | {
97 | SubTopics.Add(new MqttSubTopic
98 | {
99 | TopicName = topicElem.GetAttribute("TopicName"),
100 | QosLevel = (MqttQos)topicElem.GetAttrAsInt("QosLevel")
101 | });
102 | }
103 | }
104 |
105 | if (xmlDoc.DocumentElement.SelectSingleNode("MqttSubJSs") is XmlNode mqttSubJSsNode)
106 | {
107 | foreach (XmlElement topicElem in mqttSubJSsNode.SelectNodes("Topic"))
108 | {
109 | MqttSubJS subJS = new MqttSubJS
110 | {
111 | TopicName = topicElem.GetAttribute("TopicName"),
112 | QosLevel = (MqttQos)topicElem.GetAttrAsInt("QosLevel"),
113 | CnlCnt = topicElem.GetAttrAsInt("CnlCnt", 1),
114 | JSHandlerPath = topicElem.GetAttribute("JSHandlerPath")
115 | };
116 |
117 | if (subJS.LoadJSHandler())
118 | SubJSs.Add(subJS);
119 | }
120 | }
121 |
122 | if (xmlDoc.DocumentElement.SelectSingleNode("MqttPubTopics") is XmlNode mqttPubTopicsNode)
123 | {
124 | foreach (XmlElement topicElem in mqttPubTopicsNode.SelectNodes("Topic"))
125 | {
126 | PubTopics.Add(new MqttPubTopic
127 | {
128 | TopicName = topicElem.GetAttribute("TopicName"),
129 | QosLevel = (MqttQos)topicElem.GetAttrAsInt("QosLevel"),
130 | Retain = topicElem.GetAttrAsBool("Retain"),
131 | NumCnl = topicElem.GetAttrAsInt("NumCnl"),
132 | PubBehavior = topicElem.GetAttrAsEnum("PubBehavior"),
133 | DecimalSeparator = topicElem.GetAttribute("NDS"),
134 | Prefix = topicElem.GetAttribute("Prefix"),
135 | Suffix = topicElem.GetAttribute("Suffix")
136 | });
137 | }
138 | }
139 |
140 | if (xmlDoc.DocumentElement.SelectSingleNode("MqttPubCmds") is XmlNode mqttPubCmdsNode)
141 | {
142 | foreach (XmlElement topicElem in mqttPubCmdsNode.SelectNodes("Topic"))
143 | {
144 | PubCmds.Add(new MqttPubCmd
145 | {
146 | TopicName = topicElem.GetAttribute("TopicName"),
147 | QosLevel = (MqttQos)topicElem.GetAttrAsInt("QosLevel"),
148 | Retain = false,
149 | NumCmd = topicElem.GetAttrAsInt("NumCmd")
150 | });
151 | }
152 | }
153 |
154 | if (xmlDoc.DocumentElement.SelectSingleNode("MqttSubCmds") is XmlNode mqttSubCmdsNode)
155 | {
156 | foreach (XmlElement topicElem in mqttSubCmdsNode.SelectNodes("Topic"))
157 | {
158 | SubCmds.Add(new MqttSubCmd
159 | {
160 | TopicName = topicElem.GetAttribute("TopicName"),
161 | QosLevel = (MqttQos)topicElem.GetAttrAsInt("QosLevel"),
162 | CmdType = topicElem.GetAttrAsEnum("CmdType"),
163 | IDUser = topicElem.GetAttrAsInt("IDUser"),
164 | NumCnlCtrl = topicElem.GetAttrAsInt("NumCnlCtrl"),
165 | });
166 | }
167 | }
168 |
169 | errMsg = "";
170 | return true;
171 | }
172 | catch (Exception ex)
173 | {
174 | errMsg = CommPhrases.LoadKpSettingsError + ": " + ex.Message;
175 | return false;
176 | }
177 | }
178 |
179 | ///
180 | /// Gets the configuration file name.
181 | ///
182 | public static string GetFileName(string configDir, int kpNum, string defaultFileName)
183 | {
184 | return string.IsNullOrWhiteSpace(defaultFileName)
185 | ? Path.Combine(configDir, "KpMqtt_" + CommUtils.AddZeros(kpNum, 3) + ".xml")
186 | : Path.Combine(configDir, defaultFileName.Trim());
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/MqttPubCmd.cs:
--------------------------------------------------------------------------------
1 | namespace Scada.Comm.Devices.Mqtt.Config
2 | {
3 | ///
4 | /// Represents a command that publishes a topic when a telecommand is sent.
5 | ///
6 | internal class MqttPubCmd : MqttPubParam
7 | {
8 | public int NumCmd { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/MqttPubParam.cs:
--------------------------------------------------------------------------------
1 | namespace Scada.Comm.Devices.Mqtt.Config
2 | {
3 | ///
4 | /// Represents an item that can be published.
5 | ///
6 | internal abstract class MqttPubParam : MqttTopic
7 | {
8 | ///
9 | /// If a broker receives a message on a topic for which there are no current subscribers,
10 | /// the broker discards the message unless the publisher of the message designated the message as a retained message.
11 | /// The broker stores the last retained message and the corresponding QoS for the selected topic.
12 | ///
13 | public bool Retain { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/MqttPubTopic.cs:
--------------------------------------------------------------------------------
1 | namespace Scada.Comm.Devices.Mqtt.Config
2 | {
3 | ///
4 | /// Represents a topic to publish.
5 | ///
6 | internal class MqttPubTopic : MqttPubParam
7 | {
8 | public int NumCnl { get; set; }
9 | public PubBehavior PubBehavior { get; set; }
10 | public string DecimalSeparator { get; set; }
11 | public string Prefix { get; set; }
12 | public string Suffix { get; set; }
13 | public double Value { get; set; }
14 | public bool IsPub { get; set; }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/MqttSubCmd.cs:
--------------------------------------------------------------------------------
1 | using Scada.Data.Models;
2 |
3 | namespace Scada.Comm.Devices.Mqtt.Config
4 | {
5 | ///
6 | /// Represents a command that is sent to Server when new topic data is received.
7 | ///
8 | internal class MqttSubCmd : MqttTopic
9 | {
10 | public CmdType CmdType { get; set; }
11 | public int IDUser { get; set; }
12 | public int NumCnlCtrl { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/MqttSubJS.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Scada.Comm.Devices.Mqtt.Config
4 | {
5 | ///
6 | /// Represents a subscription to a topic that is processed by Java Script.
7 | ///
8 | internal class MqttSubJS : MqttSubTopic
9 | {
10 | public string JSHandlerPath { get; set; }
11 | public string JSHandler { get; private set; }
12 | public int CnlCnt { get; set; }
13 |
14 | public bool LoadJSHandler()
15 | {
16 | if (string.IsNullOrEmpty(JSHandlerPath))
17 | {
18 | return false;
19 | }
20 | else
21 | {
22 | using (StreamReader reader = new StreamReader(JSHandlerPath))
23 | {
24 | JSHandler = reader.ReadToEnd();
25 | }
26 |
27 | return true;
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/MqttSubTopic.cs:
--------------------------------------------------------------------------------
1 | namespace Scada.Comm.Devices.Mqtt.Config
2 | {
3 | ///
4 | /// Represents a subscription to a topic.
5 | ///
6 | internal class MqttSubTopic : MqttTopic
7 | {
8 | public int TagIndex { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/MqttTopic.cs:
--------------------------------------------------------------------------------
1 | using StriderMqtt;
2 |
3 | namespace Scada.Comm.Devices.Mqtt.Config
4 | {
5 | ///
6 | /// Represents an MQTT topic.
7 | ///
8 | internal abstract class MqttTopic
9 | {
10 | ///
11 | /// Gets or sets the topic name.
12 | ///
13 | public string TopicName { get; set; }
14 |
15 | ///
16 | /// The QOS levels are a way of guaranteeing message delivery and they refer to the connection between a broker and a client.
17 | /// QOS 0 – Once (not guaranteed)
18 | /// QOS 1 – At Least Once(guaranteed)
19 | /// QOS 2 – Only Once(guaranteed)
20 | ///
21 | public MqttQos QosLevel { get; set; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Mqtt/Config/PubBehavior.cs:
--------------------------------------------------------------------------------
1 | namespace Scada.Comm.Devices.Mqtt.Config
2 | {
3 | internal enum PubBehavior
4 | {
5 | OnChange,
6 | OnAlways
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using Scada.Comm.Devices;
2 | using System.Reflection;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("KpMqtt")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("Rapid SCADA")]
13 | [assembly: AssemblyCopyright("Copyright © 2019-2021")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("dce0a39f-172b-4ebc-965a-8b858f80b165")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion(KpMqttView.KpVersion)]
36 | [assembly: AssemblyFileVersion(KpMqttView.KpVersion)]
37 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/ConnectionPackets.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace StriderMqtt
5 | {
6 | public class ConnectPacket : PacketBase
7 | {
8 | internal const byte PacketTypeCode = 0x01;
9 |
10 | internal const string ProtocolNameV3_1 = "MQIsdp";
11 | internal const string ProtocolNameV3_1_1 = "MQTT";
12 |
13 | // max length for client id (removed in 3.1.1)
14 | internal const int ClientIdMaxLength = 23;
15 |
16 | internal const ushort KeepAlivePeriodDefault = 60; // seconds
17 |
18 | // connect flags
19 | internal const byte UsernameFlagOffset = 0x07;
20 | internal const byte PasswordFlagOffset = 0x06;
21 | internal const byte WillRetainFlagOffset = 0x05;
22 | internal const byte WillQosFlagOffset = 0x03;
23 | internal const byte WillFlagOffset = 0x02;
24 | internal const byte CleanSessionFlagOffset = 0x01;
25 |
26 |
27 | public MqttProtocolVersion ProtocolVersion
28 | {
29 | get;
30 | set;
31 | }
32 |
33 | public string ClientId
34 | {
35 | get;
36 | set;
37 | }
38 |
39 | public bool WillRetain
40 | {
41 | get;
42 | set;
43 | }
44 |
45 |
46 | public MqttQos WillQosLevel
47 | {
48 | get;
49 | set;
50 | }
51 |
52 | public bool WillFlag
53 | {
54 | get;
55 | set;
56 | }
57 |
58 | public string WillTopic
59 | {
60 | get;
61 | set;
62 | }
63 |
64 | public byte[] WillMessage
65 | {
66 | get;
67 | set;
68 | }
69 |
70 | public string Username
71 | {
72 | get;
73 | set;
74 | }
75 |
76 | public string Password
77 | {
78 | get;
79 | set;
80 | }
81 |
82 | public bool CleanSession
83 | {
84 | get;
85 | set;
86 | }
87 |
88 | internal ushort KeepAlivePeriod
89 | {
90 | get;
91 | set;
92 | }
93 |
94 |
95 | internal ConnectPacket()
96 | {
97 | this.PacketType = PacketTypeCode;
98 | }
99 |
100 |
101 | ///
102 | /// Reads a Connect packet from the given stream.
103 | /// (This method should not be used since clients don't receive Connect packets)
104 | ///
105 | /// Fixed header first byte previously read
106 | /// The stream to read from
107 | /// The protocol version to be used to read
108 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
109 | {
110 | throw new MqttProtocolException("Connect packet should not be received");
111 | }
112 |
113 | ///
114 | /// Writes the Connect packet to the given stream and using the given
115 | /// protocol version.
116 | ///
117 | /// The stream to write to
118 | /// Protocol to be used to write
119 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
120 | {
121 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
122 | {
123 | // will flag set, will topic and will message MUST be present
124 | if (this.WillFlag && (WillMessage == null || String.IsNullOrEmpty(WillTopic)))
125 | {
126 | throw new MqttProtocolException("Last will message is invalid");
127 | }
128 | // willflag not set, retain must be 0 and will topic and message MUST NOT be present
129 | else if (!this.WillFlag && (this.WillRetain || WillMessage != null || !String.IsNullOrEmpty(WillTopic)))
130 | {
131 | throw new MqttProtocolException("Last will message is invalid");
132 | }
133 | }
134 |
135 | if (this.WillFlag && ((this.WillTopic.Length < Packet.MinTopicLength) || (this.WillTopic.Length > Packet.MaxTopicLength)))
136 | {
137 | throw new MqttProtocolException("Invalid last will topic length");
138 | }
139 |
140 | writer.SetFixedHeader(PacketType);
141 |
142 | MakeVariableHeader(writer);
143 | MakePayload(writer);
144 | }
145 |
146 | void MakeVariableHeader(PacketWriter w)
147 | {
148 | if (this.ProtocolVersion == MqttProtocolVersion.V3_1)
149 | {
150 | w.AppendTextField(ProtocolNameV3_1);
151 | }
152 | else
153 | {
154 | w.AppendTextField(ProtocolNameV3_1_1);
155 | }
156 |
157 | w.Append((byte)this.ProtocolVersion);
158 |
159 | w.Append(MakeConnectFlags());
160 | w.AppendIntegerField(KeepAlivePeriod);
161 | }
162 |
163 | byte MakeConnectFlags()
164 | {
165 | byte connectFlags = 0x00;
166 | connectFlags |= (Username != null) ? (byte)(1 << UsernameFlagOffset) : (byte)0x00;
167 | connectFlags |= (Password != null) ? (byte)(1 << PasswordFlagOffset) : (byte)0x00;
168 | connectFlags |= (this.WillRetain) ? (byte)(1 << WillRetainFlagOffset) : (byte)0x00;
169 |
170 | if (this.WillFlag)
171 | connectFlags |= (byte)((byte)WillQosLevel << WillQosFlagOffset);
172 |
173 | connectFlags |= (this.WillFlag) ? (byte)(1 << WillFlagOffset) : (byte)0x00;
174 | connectFlags |= (this.CleanSession) ? (byte)(1 << CleanSessionFlagOffset) : (byte)0x00;
175 |
176 | return connectFlags;
177 | }
178 |
179 | void MakePayload(PacketWriter w)
180 | {
181 | w.AppendTextField(ClientId);
182 |
183 | if (!String.IsNullOrEmpty(WillTopic))
184 | {
185 | w.AppendTextField(WillTopic);
186 | }
187 |
188 | if (WillMessage != null)
189 | {
190 | w.AppendBytesField(WillMessage);
191 | }
192 |
193 | if (Username != null)
194 | {
195 | w.AppendTextField(Username);
196 | }
197 |
198 | if (Password != null)
199 | {
200 | w.AppendTextField(Password);
201 | }
202 | }
203 | }
204 |
205 |
206 | public class ConnackPacket : PacketBase
207 | {
208 | internal const byte PacketTypeCode = 0x02;
209 |
210 | private const byte SessionPresentFlag = 0x01;
211 |
212 | public bool SessionPresent {
213 | get;
214 | private set;
215 | }
216 |
217 | public ConnackReturnCode ReturnCode {
218 | get;
219 | set;
220 | }
221 |
222 | internal ConnackPacket()
223 | {
224 | this.PacketType = PacketTypeCode;
225 | }
226 |
227 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
228 | {
229 | throw new MqttProtocolException("Clients should not send connack packets");
230 | }
231 |
232 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
233 | {
234 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
235 | {
236 | if ((reader.FixedHeaderFirstByte & Packet.PacketFlagsBitMask) != Packet.ZeroedHeaderFlagBits)
237 | {
238 | throw new MqttProtocolException("Connack packet received with invalid header flags");
239 | }
240 | }
241 |
242 | if (reader.RemainingLength != 2)
243 | {
244 | throw new MqttProtocolException("Connack packet received with invalid remaining length");
245 | }
246 |
247 | this.SessionPresent = (reader.ReadByte() & SessionPresentFlag) > 0;
248 | this.ReturnCode = (ConnackReturnCode)reader.ReadByte();
249 | }
250 | }
251 |
252 |
253 | internal class PingreqPacket : PacketBase
254 | {
255 | internal const byte PacketTypeCode = 0x0C;
256 | internal const byte PingreqFlagBits = 0x00;
257 |
258 | internal PingreqPacket()
259 | {
260 | this.PacketType = PacketTypeCode;
261 | }
262 |
263 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
264 | {
265 | writer.SetFixedHeader(this.PacketType);
266 | }
267 |
268 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
269 | {
270 | throw new MqttProtocolException("Pingreq packet should not be received");
271 | }
272 | }
273 |
274 |
275 | internal class PingrespPacket : PacketBase
276 | {
277 | internal const byte PacketTypeCode = 0x0D;
278 |
279 | internal PingrespPacket()
280 | {
281 | this.PacketType = PacketTypeCode;
282 | }
283 |
284 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
285 | {
286 | throw new MqttProtocolException("Clients should not send pingresp packets");
287 | }
288 |
289 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
290 | {
291 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
292 | {
293 | if ((reader.FixedHeaderFirstByte & Packet.PacketFlagsBitMask) != Packet.ZeroedHeaderFlagBits)
294 | {
295 | throw new MqttProtocolException("Pingresp packet received with invalid header flags");
296 | }
297 | }
298 |
299 | if (reader.RemainingLength != 0)
300 | {
301 | throw new MqttProtocolException("Pingresp packet received with invalid remaining length");
302 | }
303 | }
304 | }
305 |
306 |
307 | internal class DisconnectPacket : PacketBase
308 | {
309 | internal const byte PacketTypeCode = 0x0E;
310 |
311 | internal DisconnectPacket()
312 | {
313 | this.PacketType = PacketTypeCode;
314 | }
315 |
316 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
317 | {
318 | throw new MqttProtocolException("Disconnect packet should not be received");
319 | }
320 |
321 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
322 | {
323 | writer.SetFixedHeader(PacketType);
324 | }
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/Constants.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace StriderMqtt
4 | {
5 | public enum MqttProtocolVersion : byte
6 | {
7 | V3_1 = 0x03,
8 | V3_1_1 = 0x04
9 | }
10 |
11 | public enum MqttQos : byte
12 | {
13 | AtMostOnce = 0x00,
14 | AtLeastOnce = 0x01,
15 | ExactlyOnce = 0x02
16 | }
17 |
18 | public class Packet
19 | {
20 | internal const byte PacketTypeOffset = 0x04;
21 | internal const byte PacketTypeMask = 0xF0;
22 |
23 | internal const byte DupFlagOffset = 0x03;
24 | internal const byte DupFlagMask = 0x08;
25 |
26 | internal const byte RetainFlagOffset = 0x00;
27 | internal const byte RetainFlagMask = 0x01;
28 |
29 | internal const byte QosLevelOffset = 0x01;
30 | internal const byte QosLevelMask = 0x06;
31 |
32 | internal const byte PacketFlagsBitMask = 0x0F;
33 | internal const byte ZeroedHeaderFlagBits = 0x00;
34 | internal const byte Qos1HeaderFlagBits = 0x02;
35 |
36 | public const uint MaxRemainingLength = 268435455;
37 |
38 | public const uint MaxPacketId = 65535;
39 |
40 | public const ushort MinTopicLength = 1;
41 | public const ushort MaxTopicLength = 65535;
42 | }
43 |
44 | public enum ConnackReturnCode : byte
45 | {
46 | Accepted = 0x00,
47 | UnacceptableProtocol = 0x01,
48 | IdentifierRejected = 0x02,
49 | BrokerUnavailable = 0x03,
50 | BadUsernameOrPassword = 0x04,
51 | NotAuthorized = 0x05
52 | }
53 |
54 | public enum SubackReturnCode : byte
55 | {
56 | AtMostOnceGranted = 0x00,
57 | AtLeastOnceGranted = 0x01,
58 | ExactlyOnceGranted = 0x02,
59 | SubscriptionFailed = 0x80
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/EventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace StriderMqtt
4 | {
5 | public class PublishReceivedEventArgs : EventArgs
6 | {
7 | public PublishPacket Packet {
8 | get;
9 | private set;
10 | }
11 |
12 | internal PublishReceivedEventArgs(PublishPacket packet) : base()
13 | {
14 | this.Packet = packet;
15 | }
16 | }
17 |
18 | public class IdentifiedPacketEventArgs : EventArgs
19 | {
20 | public ushort PacketId
21 | {
22 | get;
23 | private set;
24 | }
25 |
26 | internal IdentifiedPacketEventArgs(IdentifiedPacket packet)
27 | {
28 | this.PacketId = packet.PacketId;
29 | }
30 | }
31 |
32 | public class SubackReceivedEventArgs : EventArgs
33 | {
34 | public SubackReturnCode[] GrantedQosLevels
35 | {
36 | get;
37 | private set;
38 | }
39 |
40 | internal SubackReceivedEventArgs(SubackPacket packet)
41 | {
42 | this.GrantedQosLevels = packet.GrantedQosLevels;
43 | }
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/Exceptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace StriderMqtt
4 | {
5 | public class MqttClientException : Exception
6 | {
7 | public MqttClientException() : base()
8 | {
9 | }
10 |
11 | public MqttClientException(string message) : base(message)
12 | {
13 | }
14 | }
15 |
16 | ///
17 | /// Mqtt protocol exception, occurs when protocol specs are violated.
18 | ///
19 | public class MqttProtocolException : MqttClientException
20 | {
21 | public MqttProtocolException(string message) : base(message)
22 | {
23 | }
24 | }
25 |
26 | ///
27 | /// Mqtt connect exception, occurs when it is not possible to establish a connection.
28 | ///
29 | public class MqttConnectException : MqttClientException
30 | {
31 | public ConnackReturnCode ReturnCode
32 | {
33 | get;
34 | private set;
35 | }
36 |
37 | public MqttConnectException(string message, ConnackReturnCode code) : base(message)
38 | {
39 | this.ReturnCode = code;
40 | }
41 | }
42 |
43 | ///
44 | /// Mqtt timeout exception, occurs when nothing was read in 1.5*keepalive period.
45 | ///
46 | public class MqttTimeoutException : MqttClientException
47 | {
48 | public MqttTimeoutException() : base()
49 | {
50 | }
51 | }
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/MqttConnection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Sockets;
3 | using System.Net.Security;
4 | using System.Threading;
5 |
6 | namespace StriderMqtt
7 | {
8 | public class MqttConnection : IDisposable
9 | {
10 | private IMqttTransport Transport;
11 |
12 | private IMqttPersistence Persistence;
13 |
14 | // all in ms
15 | private int Keepalive;
16 | private int LastRead;
17 | private int LastWrite;
18 |
19 | ///
20 | /// Gets a value indicating whether a session in the broker is present.
21 | ///
22 | public bool IsSessionPresent { get; private set; }
23 |
24 | ///
25 | /// Gets a value indicating if there is an outgoing publish in the current connection.
26 | ///
27 | public bool IsPublishing { get; private set; }
28 |
29 | ///
30 | /// Set `true` to interrupt the `Loop` method.
31 | /// When Loop is called, InterruptLoop is automatically set to `false`.
32 | /// If you handle some event (specially Puback, Pubcomp, Suback and Unsuback)
33 | /// and you want to stop the Loop method to get the control back, set this field to `true`.
34 | ///
35 | public bool InterruptLoop { get; set; }
36 |
37 | private int inLoop = 0;
38 |
39 | #region events
40 |
41 | ///
42 | /// Occurs when a Publish packet is received from broker.
43 | ///
44 | public event EventHandler PublishReceived;
45 |
46 | ///
47 | /// Occurs when a Puback packet is received from broker.
48 | ///
49 | public event EventHandler PubackReceived;
50 |
51 | ///
52 | /// Occurs when Pubrec packet is received from broker.
53 | /// The Pubrel response is sent automatically if the event handlers
54 | /// succeed.
55 | ///
56 | public event EventHandler PubrecReceived;
57 |
58 | ///
59 | /// Occurs when a Pubrel packet received from broker.
60 | /// The Pubcomp response is sent automatically if the event handlers
61 | /// succeed.
62 | ///
63 | public event EventHandler PubrelReceived;
64 |
65 | ///
66 | /// Occurs when a Pubcomp packet is received from broker.
67 | ///
68 | public event EventHandler PubcompReceived;
69 |
70 | ///
71 | /// Occurs when a publish is sent to the broker.
72 | ///
73 | public event EventHandler PublishSent;
74 |
75 | ///
76 | /// Occurs when a Suback packet is received from broker.
77 | ///
78 | public event EventHandler SubackReceived;
79 |
80 | ///
81 | /// Occurs when a Unsuback packet is received from broker.
82 | ///
83 | public event EventHandler UnsubackReceived;
84 |
85 | #endregion
86 |
87 |
88 | bool ReadWaitExpired {
89 | get {
90 | if (Keepalive > 0) {
91 | return Environment.TickCount - LastRead > (Keepalive * 1.5);
92 | } else {
93 | return false;
94 | }
95 | }
96 | }
97 |
98 | bool WriteWaitExpired {
99 | get {
100 | if (Keepalive > 0) {
101 | return Environment.TickCount - LastWrite > Keepalive;
102 | } else {
103 | return false;
104 | }
105 | }
106 | }
107 |
108 |
109 | public MqttConnection (MqttConnectionArgs args, IMqttPersistence persistence = null)
110 | {
111 | if (args.Keepalive.TotalSeconds < 0 || args.Keepalive.TotalSeconds > ushort.MaxValue) {
112 | throw new ArgumentException ("Keepalive should be between 0 seconds and ushort.MaxValue (18 hours)");
113 | }
114 |
115 | this.Persistence = persistence ?? new InMemoryPersistence ();
116 |
117 | this.Keepalive = (int)args.Keepalive.TotalMilliseconds; // converts to milliseconds
118 | this.IsPublishing = false;
119 |
120 | InitTransport (args);
121 | Send (MakeConnectMessage (args));
122 | ReceiveConnack ();
123 |
124 | ResumeOutgoingFlows ();
125 | }
126 |
127 | private void InitTransport (MqttConnectionArgs args)
128 | {
129 | if (args.Secure) {
130 |
131 | Transport = new TlsTransport (args.Hostname, args.Port);
132 | } else {
133 | Transport = new TcpTransport (args.Hostname, args.Port);
134 | }
135 |
136 | Transport.Version = args.Version;
137 | Transport.SetTimeouts (args.ReadTimeout, args.WriteTimeout);
138 | }
139 |
140 | private ConnectPacket MakeConnectMessage (MqttConnectionArgs args)
141 | {
142 | var conn = new ConnectPacket ();
143 | conn.ProtocolVersion = args.Version;
144 |
145 | conn.ClientId = args.ClientId;
146 | conn.Username = args.Username;
147 | conn.Password = args.Password;
148 |
149 | if (args.WillMessage != null) {
150 | conn.WillFlag = true;
151 | conn.WillTopic = args.WillMessage.Topic;
152 | conn.WillMessage = args.WillMessage.Message;
153 | conn.WillQosLevel = args.WillMessage.Qos;
154 | conn.WillRetain = args.WillMessage.Retain;
155 | }
156 |
157 | conn.CleanSession = args.CleanSession;
158 | conn.KeepAlivePeriod = (ushort)args.Keepalive.TotalSeconds;
159 |
160 | return conn;
161 | }
162 |
163 | private void ReceiveConnack ()
164 | {
165 | PacketBase packet = Transport.Read ();
166 | this.LastRead = Environment.TickCount;
167 |
168 | var connack = packet as ConnackPacket;
169 |
170 | if (packet == null) {
171 | throw new MqttProtocolException (String.Format ("First received message should be Connack, but {0} received instead", packet.GetType ().Name));
172 | }
173 |
174 | if (connack.ReturnCode != ConnackReturnCode.Accepted) {
175 | throw new MqttConnectException ("The connection was not accepted", connack.ReturnCode);
176 | }
177 |
178 | this.IsSessionPresent = connack.SessionPresent;
179 | }
180 |
181 | private void ResumeOutgoingFlows ()
182 | {
183 | // tries to redeliver if that's the case
184 | foreach (var flow in Persistence.GetPendingOutgoingFlows()) {
185 | Resume (flow);
186 | }
187 | }
188 |
189 | // sends a publish with dup flag in the case of a publish redelivery
190 | // or a pubrel in the case of qos2 message that we know was received by the broker
191 | private void Resume (OutgoingFlow flow)
192 | {
193 | if (flow.Qos == MqttQos.AtLeastOnce ||
194 | (flow.Qos == MqttQos.ExactlyOnce && !flow.Received)) {
195 | var publish = new PublishPacket () {
196 | PacketId = flow.PacketId,
197 | QosLevel = flow.Qos,
198 | Topic = flow.Topic,
199 | Message = flow.Payload,
200 | DupFlag = true
201 | };
202 |
203 | Publish (publish);
204 | } else if (flow.Qos == MqttQos.ExactlyOnce && flow.Received) {
205 | Pubrel (flow.PacketId);
206 | }
207 |
208 | Persistence.LastOutgoingPacketId = flow.PacketId;
209 | }
210 |
211 |
212 | ///
213 | /// Publishes the given packet to the broker.
214 | ///
215 | /// Packet.
216 | public ushort Publish (PublishPacket packet)
217 | {
218 | if (packet.QosLevel != MqttQos.AtMostOnce) {
219 | if (packet.PacketId == 0) {
220 | packet.PacketId = this.GetNextPacketId ();
221 | }
222 |
223 | // persistence needed only on qos levels 1 and 2
224 | Persistence.RegisterOutgoingFlow (new OutgoingFlow () {
225 | PacketId = packet.PacketId,
226 | Topic = packet.Topic,
227 | Qos = packet.QosLevel,
228 | Payload = packet.Message
229 | });
230 | }
231 |
232 | try {
233 | IsPublishing = true;
234 | Send (packet);
235 |
236 | return packet.PacketId;
237 | } catch {
238 | IsPublishing = false;
239 | throw;
240 | }
241 | }
242 |
243 | ///
244 | /// Sends a Pubrel packet to the broker with the given packetId.
245 | /// This method is intended for resuming a QoS 2 flow (when a pubrel was sent but the pubcomp packet wasn't received).
246 | ///
247 | ///
248 | /// The client automatically sends the pubrel when a pubrec is received (and the `PubrecReceived` event completes without any error),
249 | /// so there is no need to explicitly call the `Pubrel` method in this case.
250 | ///
251 | /// Packet identifier.
252 | private void Pubrel (ushort packetId)
253 | {
254 | try {
255 | IsPublishing = true;
256 | Send (new PubrelPacket () { PacketId = packetId });
257 | } catch {
258 | IsPublishing = false;
259 | throw;
260 | }
261 | }
262 |
263 |
264 | public void Subscribe (SubscribePacket packet)
265 | {
266 | if (packet.PacketId == 0) {
267 | packet.PacketId = this.GetNextPacketId ();
268 | }
269 |
270 | Send (packet);
271 | }
272 |
273 | public void Unsubscribe (UnsubscribePacket packet)
274 | {
275 | if (packet.PacketId == 0) {
276 | packet.PacketId = this.GetNextPacketId ();
277 | }
278 |
279 | Send (packet);
280 | }
281 |
282 |
283 | private void Send (PacketBase packet)
284 | {
285 | if (Transport.IsClosed) {
286 | throw new MqttClientException ("Tried to send packet while closed");
287 | }
288 |
289 | Transport.Write (packet);
290 | LastWrite = Environment.TickCount;
291 | }
292 |
293 | public ushort GetNextPacketId ()
294 | {
295 | ushort x = Persistence.LastOutgoingPacketId;
296 | if (x == Packet.MaxPacketId) {
297 | Persistence.LastOutgoingPacketId = 1;
298 | return 1;
299 | } else {
300 | x += 1;
301 | Persistence.LastOutgoingPacketId = x;
302 | return x;
303 | }
304 | }
305 |
306 |
307 | ///
308 | /// Loop to receive packets. Use e
309 | /// The method exits when readLimit is reached or when an
310 | /// outbound flow completes. This
311 | ///
312 | /// Throws MqttTimeoutException if keepalive period expires.
313 | ///
314 | /// Read limit in milliseconds.
315 | /// Returns true if is connected, false otherwise.
316 | public bool Loop (int readLimit)
317 | {
318 | if (readLimit < 0) {
319 | throw new ArgumentException ("Poll limit should be positive");
320 | }
321 |
322 | // Loop shouldn't be called concurrently or recursivelly
323 | if (Interlocked.CompareExchange (ref inLoop, 1, 0) == 1) {
324 | throw new InvalidProgramException ("Loop is already running");
325 | }
326 |
327 | try {
328 | int readThreshold = Environment.TickCount + readLimit;
329 | int pollTime;
330 |
331 | InterruptLoop = false;
332 |
333 | while ((pollTime = readThreshold - Environment.TickCount) > 0 && !InterruptLoop) {
334 | if (Transport.IsClosed) {
335 | return false;
336 | } else if (Transport.Poll (pollTime)) {
337 | ReceivePacket ();
338 | } else if (WriteWaitExpired) {
339 | Send (new PingreqPacket ());
340 | } else if (ReadWaitExpired) {
341 | throw new MqttTimeoutException ();
342 | }
343 | }
344 |
345 | return !Transport.IsClosed;
346 | } finally {
347 | inLoop = 0;
348 | }
349 | }
350 |
351 | ///
352 | /// Loop that tries to receive packets.
353 | /// Returns true if is connected, false otherwise.
354 | /// Throws MqttTimeoutException if keepalive period expires.
355 | ///
356 | /// Poll limit TimeSpan.
357 | public bool Loop (TimeSpan readLimit)
358 | {
359 | if (readLimit.TotalMilliseconds > Int32.MaxValue) {
360 | throw new ArgumentException ("Read limit total milliseconds should be less than Int32 max value");
361 | }
362 |
363 | return Loop ((int)readLimit.TotalMilliseconds);
364 | }
365 |
366 | ///
367 | /// Tries to receive packets, reading for `Keepalive` duration
368 | ///
369 | public bool Loop ()
370 | {
371 | return Loop (Keepalive);
372 | }
373 |
374 |
375 | private void ReceivePacket ()
376 | {
377 | PacketBase packet = Transport.Read ();
378 |
379 | LastRead = Environment.TickCount;
380 |
381 | HandleReceivedPacket (packet);
382 | }
383 |
384 | void HandleReceivedPacket (PacketBase packet)
385 | {
386 | switch (packet.PacketType) {
387 | case PublishPacket.PacketTypeCode:
388 | OnPublishReceived (packet as PublishPacket);
389 | break;
390 | case PubackPacket.PacketTypeCode:
391 | OnPubackReceived (packet as PubackPacket);
392 | break;
393 | case PubrecPacket.PacketTypeCode:
394 | OnPubrecReceived (packet as PubrecPacket);
395 | break;
396 | case PubrelPacket.PacketTypeCode:
397 | OnPubrelReceived (packet as PubrelPacket);
398 | break;
399 | case PubcompPacket.PacketTypeCode:
400 | OnPubcompReceived (packet as PubcompPacket);
401 | break;
402 | case SubackPacket.PacketTypeCode:
403 | OnSubackReceived (packet as SubackPacket);
404 | break;
405 | case UnsubackPacket.PacketTypeCode:
406 | OnUnsubackReceived (packet as UnsubackPacket);
407 | break;
408 | case PingrespPacket.PacketTypeCode:
409 | break;
410 | default:
411 | throw new MqttProtocolException (String.Format ("Cannot receive message of type {0}", packet.GetType ().Name));
412 | }
413 | }
414 |
415 |
416 | // -- incoming publish events --
417 |
418 | void OnPublishReceived (PublishPacket packet)
419 | {
420 | if (packet.QosLevel == MqttQos.ExactlyOnce) {
421 | OnQos2PublishReceived (packet);
422 | } else {
423 | if (PublishReceived != null) {
424 | PublishReceived (this, new PublishReceivedEventArgs (packet));
425 | }
426 |
427 | if (packet.QosLevel == MqttQos.AtLeastOnce) {
428 | Send (new PubackPacket () { PacketId = packet.PacketId });
429 | }
430 | }
431 | }
432 |
433 | void OnQos2PublishReceived (PublishPacket packet)
434 | {
435 | if (!Persistence.IsIncomingFlowRegistered (packet.PacketId)) {
436 | if (PublishReceived != null) {
437 | PublishReceived (this, new PublishReceivedEventArgs (packet));
438 | }
439 |
440 | // Register the incoming packetId, so duplicate messages can be filtered.
441 | // This is done after "ProcessIncomingPublish" because we can't assume the
442 | // mesage was received in the case that method throws an exception.
443 | Persistence.RegisterIncomingFlow (packet.PacketId);
444 |
445 | // the ideal would be to run `PubishReceived` and `Persistence.RegisterIncomingFlow`
446 | // in a single transaction (either both or neither succeeds).
447 | }
448 |
449 | Send (new PubrecPacket () { PacketId = packet.PacketId });
450 | }
451 |
452 | void OnPubrelReceived (PubrelPacket packet)
453 | {
454 | if (PubrelReceived != null) {
455 | PubrelReceived (this, new IdentifiedPacketEventArgs (packet));
456 | }
457 |
458 | Persistence.ReleaseIncomingFlow (packet.PacketId);
459 |
460 | Send (new PubcompPacket () { PacketId = packet.PacketId });
461 | }
462 |
463 |
464 | // -- outgoing publish events --
465 |
466 | void OnPubackReceived (PubackPacket packet)
467 | {
468 | if (PubackReceived != null) {
469 | PubackReceived (this, new IdentifiedPacketEventArgs (packet));
470 | }
471 |
472 | if (PublishSent != null) {
473 | PublishSent (this, new IdentifiedPacketEventArgs (packet));
474 | }
475 |
476 | Persistence.SetOutgoingFlowCompleted (packet.PacketId);
477 |
478 | this.IsPublishing = false;
479 | }
480 |
481 | void OnPubrecReceived (PubrecPacket packet)
482 | {
483 | if (PubrecReceived != null) {
484 | PubrecReceived (this, new IdentifiedPacketEventArgs (packet));
485 | }
486 |
487 | Persistence.SetOutgoingFlowReceived (packet.PacketId);
488 |
489 | Send (new PubrelPacket () { PacketId = packet.PacketId });
490 | }
491 |
492 | void OnPubcompReceived (PubcompPacket packet)
493 | {
494 | if (PubcompReceived != null) {
495 | PubcompReceived (this, new IdentifiedPacketEventArgs (packet));
496 | }
497 |
498 | if (PublishSent != null) {
499 | PublishSent (this, new IdentifiedPacketEventArgs (packet));
500 | }
501 |
502 | Persistence.SetOutgoingFlowCompleted (packet.PacketId);
503 |
504 | this.IsPublishing = false;
505 | }
506 |
507 |
508 | // -- subscription events --
509 |
510 | void OnSubackReceived (SubackPacket packet)
511 | {
512 | if (SubackReceived != null) {
513 | SubackReceived (this, new SubackReceivedEventArgs (packet));
514 | }
515 | }
516 |
517 | void OnUnsubackReceived (UnsubackPacket packet)
518 | {
519 | if (UnsubackReceived != null) {
520 | UnsubackReceived (this, EventArgs.Empty);
521 | }
522 | }
523 |
524 | public void Disconnect ()
525 | {
526 | Send (new DisconnectPacket ());
527 | }
528 |
529 | public void Dispose ()
530 | {
531 | Transport.Close ();
532 | }
533 | }
534 |
535 |
536 | public class MqttConnectionArgs
537 | {
538 | public string Hostname { get; set; }
539 |
540 | public int Port { get; set; }
541 |
542 | public bool Secure { get; set; }
543 |
544 | public MqttProtocolVersion Version { get; set; }
545 |
546 | public string ClientId { get; set; }
547 |
548 | public string Username { get; set; }
549 |
550 | public string Password { get; set; }
551 |
552 | public bool CleanSession { get; set; }
553 |
554 | public TimeSpan Keepalive { get; set; }
555 |
556 | public WillMessage WillMessage { get; set; }
557 |
558 | public TimeSpan ReadTimeout { get; set; }
559 |
560 | public TimeSpan WriteTimeout { get; set; }
561 |
562 | public MqttConnectionArgs ()
563 | {
564 | this.Version = MqttProtocolVersion.V3_1_1;
565 | this.Keepalive = TimeSpan.FromSeconds (60);
566 |
567 | this.Port = 1883;
568 | this.CleanSession = true;
569 |
570 | this.ReadTimeout = TimeSpan.FromSeconds (10);
571 | this.WriteTimeout = TimeSpan.FromSeconds (10);
572 | }
573 | }
574 |
575 | public class WillMessage
576 | {
577 | public string Topic { get; set; }
578 |
579 | public byte[] Message { get; set; }
580 |
581 | public MqttQos Qos { get; set; }
582 |
583 | public bool Retain { get; set; }
584 | }
585 | }
586 |
587 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/PacketBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace StriderMqtt
5 | {
6 | ///
7 | /// Base class for all MQTT messages
8 | ///
9 | public abstract class PacketBase
10 | {
11 | ///
12 | /// Packet type
13 | ///
14 | public byte PacketType
15 | {
16 | get;
17 | protected set;
18 | }
19 |
20 |
21 | ///
22 | /// Writes the packet to the stream
23 | ///
24 | /// The stream to write to
25 | /// Protocol to be used while reading
26 | internal abstract void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion);
27 |
28 | ///
29 | /// Reads a packet from the stream
30 | ///
31 | /// Fixed header first byte previously read
32 | /// The stream to read from
33 | /// The protocol version to be used while reading
34 | internal abstract void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion);
35 |
36 | }
37 |
38 |
39 | public abstract class IdentifiedPacket : PacketBase
40 | {
41 | ///
42 | /// Packet identifier
43 | ///
44 | public ushort PacketId
45 | {
46 | get;
47 | set;
48 | }
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/PacketFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace StriderMqtt
4 | {
5 | internal class PacketFactory
6 | {
7 | internal static PacketBase GetInstance(byte packetTypeCode)
8 | {
9 | switch (packetTypeCode)
10 | {
11 | case ConnackPacket.PacketTypeCode:
12 | return new ConnackPacket();
13 |
14 | case PublishPacket.PacketTypeCode:
15 | return new PublishPacket();
16 |
17 | case PubackPacket.PacketTypeCode:
18 | return new PubackPacket();
19 | case PubrecPacket.PacketTypeCode:
20 | return new PubrecPacket();
21 | case PubrelPacket.PacketTypeCode:
22 | return new PubrelPacket();
23 | case PubcompPacket.PacketTypeCode:
24 | return new PubcompPacket();
25 |
26 | case SubackPacket.PacketTypeCode:
27 | return new SubackPacket();
28 | case UnsubackPacket.PacketTypeCode:
29 | return new UnsubackPacket();
30 |
31 | case PingrespPacket.PacketTypeCode:
32 | return new PingrespPacket();
33 |
34 | default:
35 | throw new MqttProtocolException(String.Format("Packet received with invalid code {0}", packetTypeCode.ToString("X")));
36 | }
37 | }
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/PacketReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 |
5 | namespace StriderMqtt
6 | {
7 | ///
8 | /// Mqtt packet reader with convenience methods to decode incoming data
9 | ///
10 | internal class PacketReader
11 | {
12 | Stream input;
13 |
14 | internal byte FixedHeaderFirstByte
15 | {
16 | get;
17 | private set;
18 | }
19 |
20 | internal byte PacketTypeCode
21 | {
22 | get
23 | {
24 | return (byte)(FixedHeaderFirstByte >> Packet.PacketTypeOffset);
25 | }
26 | }
27 |
28 | internal bool Dup
29 | {
30 | get
31 | {
32 | return (FixedHeaderFirstByte & Packet.DupFlagMask) == Packet.DupFlagMask;
33 | }
34 | }
35 |
36 | internal MqttQos QosLevel
37 | {
38 | get
39 | {
40 | return (MqttQos)((FixedHeaderFirstByte & Packet.QosLevelMask) >> Packet.QosLevelOffset);
41 | }
42 | }
43 |
44 | internal bool Retain
45 | {
46 | get
47 | {
48 | return (FixedHeaderFirstByte & Packet.RetainFlagMask) == Packet.RetainFlagMask;
49 | }
50 | }
51 |
52 | internal int RemainingLength
53 | {
54 | get;
55 | private set;
56 | }
57 |
58 | ///
59 | /// An index to follow remaining length reading
60 | ///
61 | /// The index.
62 | internal int Index
63 | {
64 | get;
65 | private set;
66 | }
67 |
68 | internal PacketReader(byte fixedHeaderFirstByte, Stream stream)
69 | {
70 | this.FixedHeaderFirstByte = fixedHeaderFirstByte;
71 | this.input = stream;
72 |
73 | this.RemainingLength = ReadRemainingLength(input);
74 | }
75 |
76 | internal PacketReader(Stream stream)
77 | {
78 | this.input = stream;
79 |
80 | this.FixedHeaderFirstByte = (byte)input.ReadByte();
81 | this.RemainingLength = ReadRemainingLength(input);
82 | }
83 |
84 | ///
85 | /// Decode remaining length reading bytes from socket
86 | ///
87 | /// Channel from reading bytes
88 | /// Decoded remaining length
89 | int ReadRemainingLength(Stream stream)
90 | {
91 | int multiplier = 1;
92 | int value = 0;
93 | int digit = 0;
94 |
95 | byte[] nextByte = new byte[1];
96 |
97 | do
98 | {
99 | // next digit from stream
100 | if (stream.Read(nextByte, 0, 1) == 1)
101 | {
102 | digit = nextByte[0];
103 | value += ((digit & 127) * multiplier);
104 | multiplier *= 128;
105 | }
106 | else
107 | {
108 | throw new MqttProtocolException("Could not read remaining length");
109 | }
110 | } while ((digit & 128) != 0);
111 |
112 | return value;
113 | }
114 |
115 | internal byte ReadByte()
116 | {
117 | Index++;
118 | return (byte)input.ReadByte();
119 | }
120 |
121 | internal byte[] ReadBytes(ushort n)
122 | {
123 | Index += n;
124 | byte[] buffer = new byte[n];
125 |
126 | this.input.Read(buffer, 0, n);
127 |
128 | return buffer;
129 | }
130 |
131 | internal ushort ReadIntegerField()
132 | {
133 | Index += 2;
134 |
135 | byte[] buffer = new byte[2];
136 |
137 | this.input.Read(buffer, 0, 2);
138 |
139 | ushort value = (ushort)((buffer[0] << 8) & 0xFF00);
140 | value |= buffer[1];
141 |
142 | return value;
143 | }
144 |
145 | internal string ReadTextField()
146 | {
147 | ushort length = this.ReadIntegerField();
148 | byte[] bytes = this.ReadBytes(length);
149 |
150 | return Encoding.UTF8.GetString(bytes);
151 | }
152 |
153 | internal byte[] ReadToEnd()
154 | {
155 | int remaining = RemainingLength - Index;
156 |
157 | if (remaining < 0)
158 | {
159 | throw new MqttProtocolException("More than the remaining length was read");
160 | }
161 | else if (remaining == 0)
162 | {
163 | return new byte[0];
164 | }
165 | else
166 | {
167 | byte[] buffer = new byte[remaining];
168 | this.input.Read(buffer, 0, remaining);
169 | return buffer;
170 | }
171 | }
172 |
173 | }
174 | }
175 |
176 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/PacketWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 |
5 | namespace StriderMqtt
6 | {
7 | ///
8 | /// Mqtt Packet writer, with common methods to write on mqtt data formats.
9 | ///
10 | internal class PacketWriter : IDisposable
11 | {
12 | byte fixedHeader;
13 | MemoryStream content = new MemoryStream();
14 |
15 |
16 | internal PacketWriter()
17 | {
18 | }
19 |
20 |
21 | ///
22 | /// Writes the fixed header with flags set to 0 and qos to 0.
23 | ///
24 | /// Packet type code.
25 | internal void SetFixedHeader(byte packetTypeCode)
26 | {
27 | this.fixedHeader = (byte)((packetTypeCode << Packet.PacketTypeOffset) & Packet.PacketTypeMask);
28 | }
29 |
30 | ///
31 | /// Writes the fixed header with the specified qos level and flags zeroed.
32 | ///
33 | /// Packet type.
34 | /// Qos level.
35 | internal void SetFixedHeader(byte packetType, MqttQos qosLevel)
36 | {
37 | fixedHeader = (byte)(packetType << Packet.PacketTypeOffset);
38 | fixedHeader |= (byte)((byte)qosLevel << Packet.QosLevelOffset);
39 | }
40 |
41 | ///
42 | /// Writes the fixed header using the provided values for qos and flags
43 | ///
44 | /// Packet type.
45 | /// If set to true duplicate.
46 | /// Qos level.
47 | /// If set to true retain.
48 | internal void SetFixedHeader(byte packetType, bool duplicate, MqttQos qosLevel, bool retain)
49 | {
50 | fixedHeader = (byte)(packetType << Packet.PacketTypeOffset);
51 | fixedHeader |= duplicate ? (byte)(1 << Packet.DupFlagOffset) : (byte)0x00;
52 | fixedHeader |= (byte)(((byte)qosLevel << Packet.QosLevelOffset) & Packet.QosLevelMask);
53 | fixedHeader |= retain ? (byte)(1 << Packet.RetainFlagOffset) : (byte)0x00;
54 | }
55 |
56 | internal void Append(byte value)
57 | {
58 | content.WriteByte(value);
59 | }
60 |
61 | internal void Append(byte[] bytes)
62 | {
63 | content.Write(bytes, 0, bytes.Length);
64 | }
65 |
66 | ///
67 | /// Writes an MQTT integer to the stream, with 16-bits in big-endian order.
68 | ///
69 | /// Stream.
70 | /// Value.
71 | internal void AppendIntegerField(ushort value)
72 | {
73 | content.WriteByte((byte)((value >> 8) & 0xFF)); // MSB
74 | content.WriteByte((byte)(value & 0xFF)); // LSB
75 | }
76 |
77 | ///
78 | /// Writes a MQTT text field to the stream.
79 | /// The mqtt text field consists of the length of the text (16-bit big-endian)
80 | /// followed by the UTF-8 encoded char data.
81 | ///
82 | /// Stream.
83 | /// Value.
84 | internal void AppendTextField(string value)
85 | {
86 | if (value == null)
87 | {
88 | throw new ArgumentException("Value shouldn't be null");
89 | }
90 |
91 | byte[] bytes = Encoding.UTF8.GetBytes(value);
92 |
93 | if (bytes.Length > ushort.MaxValue)
94 | {
95 | throw new ArgumentException("Value shouldn't be longer than 65535 bytes");
96 | }
97 |
98 | AppendIntegerField((ushort)bytes.Length);
99 | content.Write(bytes, 0, bytes.Length);
100 | }
101 |
102 | ///
103 | /// Writes a MQTT bytes field to the stream.
104 | /// It consists of an integer length of the data (16-bit big-endian)
105 | /// followed by the data itself.
106 | ///
107 | /// Stream.
108 | /// Value.
109 | internal void AppendBytesField(byte[] value)
110 | {
111 | if (value == null)
112 | {
113 | throw new ArgumentException("Value shouldn't be null");
114 | }
115 |
116 | if (value.Length > ushort.MaxValue)
117 | {
118 | throw new ArgumentException("Value shouldn't be longer than 65535 bytes");
119 | }
120 |
121 | AppendIntegerField((ushort)value.Length);
122 | content.Write(value, 0, value.Length);
123 | }
124 |
125 | ///
126 | /// Writes the contents of the current PacketWriter
127 | /// to the given stream.
128 | ///
129 | /// S.
130 | internal void WriteTo(Stream s)
131 | {
132 | s.WriteByte(fixedHeader);
133 | WriteRemainingLength(s);
134 |
135 | if (content.Length > 0)
136 | {
137 | content.WriteTo(s);
138 | }
139 | }
140 |
141 | ///
142 | /// Encode remaining length value and writes to the stream
143 | ///
144 | /// Remaining length value to encode
145 | /// The stream to write to
146 | void WriteRemainingLength(Stream output)
147 | {
148 | int remainingLength = (int)content.Length;
149 |
150 | if (remainingLength > Packet.MaxRemainingLength)
151 | {
152 | throw new MqttProtocolException("Packet size limit exceeded");
153 | }
154 | else if (remainingLength < 0)
155 | {
156 | throw new InvalidOperationException("Remaining length should not be negative");
157 | }
158 |
159 | int digit = 0;
160 |
161 | do
162 | {
163 | digit = remainingLength % 128;
164 | remainingLength /= 128;
165 | if (remainingLength > 0)
166 | {
167 | digit = digit | 0x80;
168 | }
169 |
170 | output.WriteByte((byte)digit);
171 | } while (remainingLength > 0);
172 | }
173 |
174 | public void Dispose()
175 | {
176 | content.Dispose();
177 | }
178 | }
179 | }
180 |
181 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/Persistence.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace StriderMqtt
6 | {
7 | public interface IMqttPersistence
8 | {
9 | // methods related to incoming messages
10 |
11 | ///
12 | /// Stores an incoming message in persistence.
13 | /// In case of QoS level 2, the packet id is registered in
14 | /// an incoming inflight set, so duplicates can be avoided.
15 | ///
16 | void RegisterIncomingFlow(ushort packetId);
17 |
18 | ///
19 | /// Releases an entry in the incoming inflight set by packet identifier.
20 | /// Expected to be called when a Pubrel is received.
21 | ///
22 | void ReleaseIncomingFlow(ushort packetId);
23 |
24 | ///
25 | /// Determines whether the packet id is registered in the incoming inflight set.
26 | /// In the case of QoS 2 flow, this method determines wether a duplicate message
27 | /// should be received (if not in incoming set) or ignored (if present in incoming set).
28 | ///
29 | bool IsIncomingFlowRegistered(ushort packetId);
30 |
31 |
32 | // methods related to outgoing messages
33 |
34 | ushort LastOutgoingPacketId { get; set; }
35 |
36 | ///
37 | /// In the case of qos 0, registers the message in the published numbers list.
38 | /// Otherwise the message is stored in the outgoing inflight messages queue.
39 | ///
40 | void RegisterOutgoingFlow(OutgoingFlow outgoingMessage);
41 |
42 | ///
43 | /// Gets a message in the outgoing inflight messages queue.
44 | /// Returns null if there isn't any message in the queue.
45 | ///
46 | /// The pending outgoing message.
47 | IEnumerable GetPendingOutgoingFlows();
48 |
49 | ///
50 | /// Marks the outgoing message (in the outgoing inflight queue) as "received" by the broker.
51 | ///
52 | /// Packet identifier.
53 | void SetOutgoingFlowReceived(ushort packetId);
54 |
55 | ///
56 | /// Removes the message from the outgoing inflight queue,
57 | /// and stores the related number in the published numbers list.
58 | ///
59 | /// Packet identifier.
60 | void SetOutgoingFlowCompleted(ushort packetId);
61 | }
62 |
63 | public class OutgoingFlow
64 | {
65 | ///
66 | /// The packetId used for publishing.
67 | ///
68 | public ushort PacketId { get; set; }
69 |
70 | public string Topic { get; set; }
71 |
72 | public MqttQos Qos { get; set; }
73 |
74 | ///
75 | /// The payload that will be published.
76 | ///
77 | public byte[] Payload { get; set; }
78 |
79 | ///
80 | /// Received Flag, to be used with QoS2.
81 | /// This flag determines if the `Pubrec` packet was received from broker.
82 | ///
83 | public bool Received { get; set; }
84 | }
85 |
86 |
87 | ///
88 | /// In memory persistence.
89 | /// This persistence support multiple incoming and outgoing messages,
90 | /// aldough ordering is only guaranteed when only one message is inflight per direction.
91 | ///
92 | public class InMemoryPersistence : IMqttPersistence, IDisposable
93 | {
94 | List incomingPacketIds = new List();
95 | List outgoingFlows = new List();
96 |
97 | public ushort LastOutgoingPacketId { get; set; }
98 |
99 | public InMemoryPersistence()
100 | {
101 | }
102 |
103 | public void RegisterIncomingFlow(ushort packetId)
104 | {
105 | incomingPacketIds.Add(packetId);
106 | }
107 |
108 | public void ReleaseIncomingFlow(ushort packetId)
109 | {
110 | if (incomingPacketIds.Contains(packetId))
111 | {
112 | incomingPacketIds.Remove(packetId);
113 | }
114 | }
115 |
116 | public bool IsIncomingFlowRegistered(ushort packetId)
117 | {
118 | return incomingPacketIds.Contains(packetId);
119 | }
120 |
121 |
122 |
123 | public void RegisterOutgoingFlow(OutgoingFlow outgoingMessage)
124 | {
125 | outgoingFlows.Add(outgoingMessage);
126 | }
127 |
128 | public IEnumerable GetPendingOutgoingFlows()
129 | {
130 | return new List(outgoingFlows);
131 | }
132 |
133 | public void SetOutgoingFlowReceived(ushort packetId)
134 | {
135 | OutgoingFlow msg = outgoingFlows.FirstOrDefault(m => m.PacketId == packetId);
136 | if (msg != null)
137 | {
138 | msg.Received = true;
139 | }
140 | }
141 |
142 | public void SetOutgoingFlowCompleted(ushort packetId)
143 | {
144 | outgoingFlows.RemoveAll(m => m.PacketId == packetId);
145 | }
146 |
147 |
148 | public void Dispose()
149 | {
150 | incomingPacketIds.Clear();
151 | outgoingFlows.Clear();
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/PublishPackets.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace StriderMqtt
5 | {
6 | public class PublishPacket : IdentifiedPacket
7 | {
8 | internal const byte PacketTypeCode = 0x03;
9 |
10 | ///
11 | /// Duplicate message flag
12 | ///
13 | public bool DupFlag
14 | {
15 | get;
16 | set;
17 | }
18 |
19 | ///
20 | /// Quality of Service, see `MqttQualityOfService`
21 | ///
22 | public MqttQos QosLevel
23 | {
24 | get;
25 | set;
26 | }
27 |
28 | ///
29 | /// Retain message flag
30 | ///
31 | public bool Retain
32 | {
33 | get;
34 | set;
35 | }
36 |
37 | ///
38 | /// Gets or sets the topic to send the application message.
39 | ///
40 | /// The topic.
41 | public string Topic
42 | {
43 | get;
44 | set;
45 | }
46 |
47 | ///
48 | /// Gets or sets the Application Message to be sent to the broker.
49 | ///
50 | /// The application message.
51 | public byte[] Message
52 | {
53 | get;
54 | set;
55 | }
56 |
57 | public PublishPacket()
58 | {
59 | this.PacketType = PacketTypeCode;
60 | }
61 |
62 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
63 | {
64 | ValidateTopic();
65 |
66 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
67 | {
68 | if (this.QosLevel == MqttQos.AtMostOnce && this.PacketId > 0)
69 | {
70 | throw new InvalidOperationException("When using QoS 0 (at most once) the PacketId must not be set");
71 | }
72 | }
73 |
74 | writer.SetFixedHeader(PacketType, DupFlag, QosLevel, Retain);
75 |
76 | // variable header
77 | writer.AppendTextField(this.Topic);
78 |
79 | if (this.QosLevel > MqttQos.AtMostOnce)
80 | {
81 | writer.AppendIntegerField(this.PacketId);
82 | }
83 |
84 | writer.Append(this.Message);
85 | }
86 |
87 | private void ValidateTopic()
88 | {
89 | // topic can't contain wildcards
90 | if ((this.Topic.IndexOf('#') != -1) || (this.Topic.IndexOf('+') != -1))
91 | {
92 | throw new MqttProtocolException("Cannot use wildcards when publishing");
93 | }
94 |
95 | // check topic length
96 | if ((this.Topic.Length < Packet.MinTopicLength) || (this.Topic.Length > Packet.MaxTopicLength))
97 | {
98 | throw new MqttProtocolException("Invalid topic length");
99 | }
100 | }
101 |
102 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
103 | {
104 | this.DupFlag = reader.Dup;
105 | this.QosLevel = reader.QosLevel;
106 | this.Retain = reader.Retain;
107 |
108 | this.Topic = reader.ReadTextField();
109 |
110 | if (QosLevel > MqttQos.AtMostOnce)
111 | {
112 | this.PacketId = reader.ReadIntegerField();
113 | }
114 |
115 | this.Message = reader.ReadToEnd();
116 | }
117 | }
118 |
119 |
120 | public class PubackPacket : IdentifiedPacket
121 | {
122 | internal const byte PacketTypeCode = 0x04;
123 |
124 | internal PubackPacket()
125 | {
126 | this.PacketType = PacketTypeCode;
127 | }
128 |
129 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
130 | {
131 | writer.SetFixedHeader(PacketType);
132 | writer.AppendIntegerField(PacketId);
133 | }
134 |
135 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
136 | {
137 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
138 | {
139 | if ((reader.FixedHeaderFirstByte & Packet.PacketFlagsBitMask) != Packet.ZeroedHeaderFlagBits)
140 | {
141 | throw new MqttProtocolException("Puback packet received with invalid header flags");
142 | }
143 | }
144 |
145 | if (reader.RemainingLength != 2)
146 | {
147 | throw new MqttProtocolException("Remaining length of the incoming puback packet is invalid");
148 | }
149 |
150 | this.PacketId = reader.ReadIntegerField();
151 | }
152 | }
153 |
154 |
155 | public class PubrecPacket : IdentifiedPacket
156 | {
157 | internal const byte PacketTypeCode = 0x05;
158 |
159 | public PubrecPacket()
160 | {
161 | this.PacketType = PacketTypeCode;
162 | }
163 |
164 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
165 | {
166 | writer.SetFixedHeader(PacketType);
167 | writer.AppendIntegerField(PacketId);
168 | }
169 |
170 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
171 | {
172 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
173 | {
174 | if ((reader.FixedHeaderFirstByte & Packet.PacketFlagsBitMask) != Packet.ZeroedHeaderFlagBits)
175 | {
176 | throw new MqttProtocolException("Pubrec packet received with invalid header flags");
177 | }
178 | }
179 |
180 | if (reader.RemainingLength != 2)
181 | {
182 | throw new MqttProtocolException("Remaining length of the incoming pubrec packet is invalid");
183 | }
184 |
185 | this.PacketId = reader.ReadIntegerField();
186 | }
187 | }
188 |
189 |
190 | public class PubrelPacket : IdentifiedPacket
191 | {
192 | internal const byte PacketTypeCode = 0x06;
193 |
194 | public PubrelPacket()
195 | {
196 | this.PacketType = PacketTypeCode;
197 | }
198 |
199 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
200 | {
201 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
202 | {
203 | writer.SetFixedHeader(PacketType, MqttQos.AtLeastOnce);
204 | }
205 | else
206 | {
207 | writer.SetFixedHeader(PacketType);
208 | }
209 |
210 | writer.AppendIntegerField(PacketId);
211 | }
212 |
213 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
214 | {
215 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
216 | {
217 | if ((reader.FixedHeaderFirstByte & Packet.PacketFlagsBitMask) != Packet.Qos1HeaderFlagBits)
218 | {
219 | throw new MqttProtocolException("Pubrel packet received with invalid header flags");
220 | }
221 | }
222 |
223 | if (reader.RemainingLength != 2)
224 | {
225 | throw new MqttProtocolException("Remaining length of the incoming pubrel packet is invalid");
226 | }
227 |
228 | this.PacketId = reader.ReadIntegerField();
229 | }
230 | }
231 |
232 |
233 | public class PubcompPacket : IdentifiedPacket
234 | {
235 | internal const byte PacketTypeCode = 0x07;
236 |
237 | public PubcompPacket()
238 | {
239 | this.PacketType = PacketTypeCode;
240 | }
241 |
242 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
243 | {
244 | writer.SetFixedHeader(PacketType);
245 | writer.AppendIntegerField(PacketId);
246 | }
247 |
248 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
249 | {
250 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
251 | {
252 | if ((reader.FixedHeaderFirstByte & Packet.PacketFlagsBitMask) != Packet.ZeroedHeaderFlagBits)
253 | {
254 | throw new MqttProtocolException("Pubcomp packet received with invalid header flags");
255 | }
256 | }
257 |
258 | if (reader.RemainingLength != 2)
259 | {
260 | throw new MqttProtocolException("Remaining length of the incoming pubcomp packet is invalid");
261 | }
262 |
263 | this.PacketId = reader.ReadIntegerField();
264 | }
265 | }
266 |
267 | }
268 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/SubscriptionPackets.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections;
4 | using System.Text;
5 |
6 | namespace StriderMqtt
7 | {
8 | public class SubscribePacket : IdentifiedPacket
9 | {
10 | internal const byte PacketTypeCode = 0x08;
11 |
12 | private const byte QosPartMask = 0x03;
13 |
14 | ///
15 | /// List of topics to subscribe
16 | ///
17 | public string[] Topics
18 | {
19 | get;
20 | set;
21 | }
22 |
23 | ///
24 | /// List of QOS Levels related to topics
25 | ///
26 | public MqttQos[] QosLevels
27 | {
28 | get;
29 | set;
30 | }
31 |
32 |
33 | public SubscribePacket()
34 | {
35 | this.PacketType = PacketTypeCode;
36 | }
37 |
38 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
39 | {
40 | if (Topics.Length != QosLevels.Length) {
41 | throw new InvalidOperationException("The length of Topics should match the length of QosLevels");
42 | }
43 |
44 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
45 | {
46 | writer.SetFixedHeader(PacketType, MqttQos.AtLeastOnce);
47 | }
48 | else
49 | {
50 | writer.SetFixedHeader(PacketType);
51 | }
52 |
53 | writer.AppendIntegerField(PacketId);
54 |
55 | for (int i = 0; i < Topics.Length; i++)
56 | {
57 | if (String.IsNullOrEmpty(this.Topics[i]) || this.Topics[i].Length > Packet.MaxTopicLength)
58 | {
59 | throw new InvalidOperationException("Invalid topic length");
60 | }
61 |
62 | writer.AppendTextField(this.Topics[i]);
63 | writer.Append((byte)(((byte)this.QosLevels[i]) & QosPartMask));
64 | }
65 | }
66 |
67 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
68 | {
69 | throw new MqttProtocolException("Clients should not receive subscribe packets");
70 | }
71 | }
72 |
73 |
74 | public class SubackPacket : IdentifiedPacket
75 | {
76 | internal const byte PacketTypeCode = 0x09;
77 |
78 | public SubackReturnCode[] GrantedQosLevels
79 | {
80 | get;
81 | internal set;
82 | }
83 |
84 | internal SubackPacket()
85 | {
86 | this.PacketType = PacketTypeCode;
87 | }
88 |
89 |
90 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
91 | {
92 | throw new MqttProtocolException("Clients should not send unsuback packets");
93 | }
94 |
95 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
96 | {
97 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
98 | {
99 | if ((reader.FixedHeaderFirstByte & Packet.PacketFlagsBitMask) != Packet.ZeroedHeaderFlagBits)
100 | {
101 | throw new MqttProtocolException("Unsuback packet received with invalid header flags");
102 | }
103 | }
104 |
105 | this.PacketId = reader.ReadIntegerField();
106 |
107 | var bytes = reader.ReadToEnd();
108 | this.GrantedQosLevels = new SubackReturnCode[bytes.Length];
109 |
110 | for (int i = 0; i < bytes.Length; i++)
111 | {
112 | if (bytes[i] > (byte)SubackReturnCode.ExactlyOnceGranted && bytes[i] != (byte)SubackReturnCode.SubscriptionFailed)
113 | {
114 | throw new MqttProtocolException(String.Format("Invalid qos level '{0}' received from broker", bytes[i]));
115 | }
116 |
117 | this.GrantedQosLevels[i] = (SubackReturnCode)bytes[i];
118 | }
119 | }
120 | }
121 |
122 |
123 | public class UnsubscribePacket : IdentifiedPacket
124 | {
125 | internal const byte PacketTypeCode = 0x0A;
126 |
127 | public string[] Topics
128 | {
129 | get;
130 | set;
131 | }
132 |
133 | public UnsubscribePacket()
134 | {
135 | this.PacketType = PacketTypeCode;
136 | }
137 |
138 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
139 | {
140 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
141 | {
142 | writer.SetFixedHeader(PacketType, MqttQos.AtLeastOnce);
143 | }
144 | else
145 | {
146 | writer.SetFixedHeader(PacketType);
147 | }
148 |
149 | writer.AppendIntegerField(PacketId);
150 |
151 | foreach (string topic in this.Topics)
152 | {
153 | writer.AppendTextField(topic);
154 | }
155 | }
156 |
157 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
158 | {
159 | throw new MqttProtocolException("Clients should not send unsubscribe packets");
160 | }
161 | }
162 |
163 |
164 | public class UnsubackPacket : IdentifiedPacket
165 | {
166 | internal const byte PacketTypeCode = 0x0B;
167 |
168 | public UnsubackPacket()
169 | {
170 | this.PacketType = PacketTypeCode;
171 | }
172 |
173 | internal override void Serialize(PacketWriter writer, MqttProtocolVersion protocolVersion)
174 | {
175 | throw new MqttProtocolException("Clients should not send unsuback packets");
176 | }
177 |
178 | internal override void Deserialize(PacketReader reader, MqttProtocolVersion protocolVersion)
179 | {
180 | if (protocolVersion == MqttProtocolVersion.V3_1_1)
181 | {
182 | if ((reader.FixedHeaderFirstByte & Packet.PacketFlagsBitMask) != Packet.ZeroedHeaderFlagBits)
183 | {
184 | throw new MqttProtocolException("Unsuback packet received with invalid header flags");
185 | }
186 | }
187 |
188 | if (reader.RemainingLength != 2)
189 | {
190 | throw new MqttProtocolException("Unsuback packet received with invalid remaining length");
191 | }
192 |
193 | this.PacketId = reader.ReadIntegerField();
194 | }
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/StriderMqtt/Transport.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net.Sockets;
4 | using System.Net.Security;
5 | using System.Security.Authentication;
6 | using System.Collections.Generic;
7 |
8 | namespace StriderMqtt
9 | {
10 | public interface IMqttTransport
11 | {
12 | Stream Stream { get; }
13 | bool IsClosed { get; }
14 |
15 | void Close();
16 |
17 | bool Poll(int pollLimit);
18 | void SetTimeouts(TimeSpan readTimeout, TimeSpan writeTimeout);
19 |
20 | MqttProtocolVersion Version { get; set; }
21 |
22 | PacketBase Read();
23 | void Write(PacketBase packet);
24 | }
25 |
26 | internal abstract class BaseTransport : IMqttTransport
27 | {
28 | abstract public Stream Stream { get; }
29 | abstract public bool IsClosed { get; }
30 |
31 | abstract public void Close();
32 |
33 | abstract public bool Poll(int pollLimit);
34 | abstract public void SetTimeouts(TimeSpan readTimeout, TimeSpan writeTimeout);
35 |
36 | public MqttProtocolVersion Version { get; set; }
37 |
38 | public PacketBase Read()
39 | {
40 | var reader = new PacketReader(this.Stream);
41 |
42 | PacketBase packet = PacketFactory.GetInstance(reader.PacketTypeCode);
43 | packet.Deserialize(reader, this.Version);
44 |
45 | return packet;
46 | }
47 |
48 | public void Write(PacketBase packet)
49 | {
50 | using (var writer = new PacketWriter())
51 | {
52 | packet.Serialize(writer, this.Version);
53 | writer.WriteTo(this.Stream);
54 | }
55 | }
56 | }
57 |
58 |
59 | internal class TcpTransport : BaseTransport
60 | {
61 | private TcpClient tcpClient;
62 | private NetworkStream netstream;
63 |
64 | override public Stream Stream
65 | {
66 | get
67 | {
68 | return this.netstream;
69 | }
70 | }
71 |
72 | override public bool IsClosed
73 | {
74 | get
75 | {
76 | return tcpClient == null || !tcpClient.Connected;
77 | }
78 | }
79 |
80 | internal TcpTransport(string hostname, int port)
81 | {
82 | this.tcpClient = new TcpClient();
83 | this.tcpClient.Connect(hostname, port);
84 | this.netstream = this.tcpClient.GetStream();
85 |
86 | }
87 |
88 | override public void SetTimeouts(TimeSpan readTimeout, TimeSpan writeTimeout)
89 | {
90 | this.netstream.ReadTimeout = (int)readTimeout.TotalMilliseconds;
91 | this.netstream.WriteTimeout = (int)writeTimeout.TotalMilliseconds;
92 | }
93 |
94 | override public bool Poll(int pollLimit)
95 | {
96 | return this.tcpClient.Client.Poll(pollLimit, SelectMode.SelectRead);
97 | }
98 |
99 | override public void Close()
100 | {
101 | this.netstream.Close();
102 | this.tcpClient.Close();
103 | }
104 | }
105 |
106 |
107 | internal class TlsTransport : BaseTransport
108 | {
109 | private TcpClient tcpClient;
110 | private NetworkStream netstream;
111 | private SslStream sslStream;
112 |
113 | override public Stream Stream
114 | {
115 | get
116 | {
117 | return this.sslStream;
118 | }
119 | }
120 |
121 | override public bool IsClosed
122 | {
123 | get
124 | {
125 | return tcpClient == null || !tcpClient.Connected;
126 | }
127 | }
128 |
129 | internal TlsTransport(string hostname, int port)
130 | {
131 | this.tcpClient = new TcpClient();
132 | this.tcpClient.Connect(hostname, port);
133 |
134 | this.netstream = this.tcpClient.GetStream();
135 | this.sslStream = new SslStream(netstream, false);
136 |
137 | this.sslStream.AuthenticateAsClient(hostname);
138 | }
139 |
140 | override public void SetTimeouts(TimeSpan readTimeout, TimeSpan writeTimeout)
141 | {
142 | this.sslStream.ReadTimeout = (int)readTimeout.TotalMilliseconds;
143 | this.sslStream.WriteTimeout = (int)writeTimeout.TotalMilliseconds;
144 | }
145 |
146 | override public bool Poll(int pollLimit)
147 | {
148 | return this.tcpClient.Client.Poll(pollLimit, SelectMode.SelectRead);
149 | }
150 |
151 | override public void Close()
152 | {
153 | this.sslStream.Close();
154 | this.netstream.Close();
155 | this.tcpClient.Close();
156 | }
157 | }
158 |
159 | }
160 |
161 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/KpMqtt/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/README-RU.md:
--------------------------------------------------------------------------------
1 | KpMqtt
2 | =============================
3 |
4 | Драйвер KpMqtt.dll является прикладной библиотекой для приложения Scada Communicator проекта RapidScada.
5 | При помощи данного драйвера можно осуществлять прием и передачу сообщений по протоколу MQTT.
6 |
7 | Краткое введение в протокол MQTT.
8 | ------------------------------------
9 |
10 | MQTT - является простым сетевым протоколом, который получил наибольшую популярность в сетях IoT, для обмена информацией между различными устройствами. На текущий момент протокол реализован на достаточно большом количестве языков программирования, в том числе и на C#. Это позволяет реализовать телематику между разнородными системами в виде универсальной шины передачи данных.
11 | Спецификация протокола находится по [ссылке](http://mqtt.org "ссылка на спецификацию протокола MQTT") .
12 |
13 | Программная модель взаимодействия по протоколу MQTT выстраиваивается по клиент-серверной архитектуре. В качестве сервера обычно выступает так называемый брокер запросов. В качестве клиента может выступать любое приложение, которое реализует протокол MQTT. Взаимодействие между клиентом и брокером осуществляется по модели публикатор-подписчик. Данная модель позволяет организовать оптимальный механизм передачи данных между клиентом и брокером. Клиент подписывается на необходимое ему количество сообщений. Брокер регистрирует запрашиваемую пользователем подписку в виде имени идентификатора сообщения. Такое имя выстраивается по определенным правилам, описанным в спецификации. При поступлении новых сообщений брокер осуществляет оповещение всех клиентов, которые подписаны на соответствующий идентификатор. В обычном сленге наименование идентификатора сообщения называется топиком (TopicName). Также клиент может публиковать сообщения. Соответственно при публикации сообщения клиент указывает имя топика, для которого предназначены передаваемые данные. Брокер получив такое сообщение оповещает всех клиентов, подписавшихся на данное имя топика.
14 |
15 | Примеры построения топиков.
16 | ------------------------------
17 |
18 | Обычно описание большинства иерархических объектов можно представить в виде древовидной структуры. Так к примеру описание доступа к каналам RapidScada можно было бы представить в виде следуещего шаблона для ветки дерева:
19 |
20 | /rapidobject/rapidkp/rapidcnl , где
21 |
22 | - **rapidobject** указывает на уникальный номер объекта из справочника объектов RapidScada;
23 | - **rapidkp** указывает на уникальный номер КП из справочника КП;
24 | - **rapidcnl** указывает на номер канала из справочника входящих каналов;
25 |
26 | В итоге имя топика может получиться таким: /1/1/1 (номер объекта/номер КП/номер канала) или таким /1/1/2. Что в целом будет описывать объект №1, который включает в себя КП 1, у которого в свою очередь есть канал №1 и №2.
27 |
28 | Именование топиков может быть более определенным при использовании собственных правил кодирования принятых для объекта автоматизации. Но следует учитывать, что общая длинна топика ограничивается стандартом.
29 |
30 | Брокеры MQTT.
31 | ----------------------------
32 |
33 | На [странице описания протокола MQTT](http://mqtt.org) находится информация по различным реализациям клиентских и серверных приложений. Чаще всего в качестве брокера MQTT для небольших IoT проектов используют сервер [Mosquitto](http://mosquitto.org) . На сайте проекта присутствует достаточный объем документации, который позволит развернуть сервер для различных операционных систем, включая операционную систему Windows. Проект [Mosquitto](http://mosquitto.org) активно поддерживается и соответствует последним версиям спецификации протокола MQTT. Программные пакеты данного брокера доступны также для роутеров на базе ОС OpenWRT.
34 | Также есть открытые проекты для установки брокеров масштаба предприятия, которые позволяют выдерживать высокие нагрузки и обеспечивать механизмы отказоустойчивости.
35 | Если отсутствует возможность установки собственного MQTT брокера, то можно воспользоваться услугами облачных сервисов развернутых сторонними компаниями. Бесплатный доступ к таким брокерам обычно имеет некоторые ограничения.
36 | Примеры таких брокеров:
37 | - [IoT Eclipse](http://http://iot.eclipse.org/)
38 | - [CloudMQTT](http://https://www.cloudmqtt.com/)
39 | - [HiveMQ](http://www.hivemq.com)
40 |
41 | Для тестирования работоспособности и других задач удобно использотвать графическое клиентское приложение для работы с MQTT брокером. Обзор таких приложений можно найти [здесь.](http://www.hivemq.com/blog/seven-best-mqtt-client-tools)
42 |
43 | Настройка **KpMqtt** драйвера для **Scada Communicator**.
44 | --------------------------------------------------------
45 |
46 | Настройку драйвера **KpMqtt** можно разделить на 2 этапа:
47 | - Создание конфигурационного файла для драйвера **KpMqtt**
48 | - Создание линии связи, добавление КП и привязка к нему конфигурационого файла
49 |
50 | При создании конфигурационного файла для драйвера **KpMqtt** используются следующие блоковые секции и их атрибуты:
51 |
52 | - Секция **MqttParams** - данная секция содержит атрибуты, предназначенные для настройки соединения с MQTT брокером.
53 | - Атрибут **Hostname** - данный атрибут должен содержать DNS(IP) адрес MQTT брокера. (Пример: iot.eclipse.org)
54 | - Атрибут **ClientID** - данный атрибут должен содержать уникальный идентификатор (имя) клиента для подключения к MQTT брокеру.
55 | - Атрибут **Port** - этот атриубт должен содержать номер порта, на котором брокер MQTT принимает соединения от клиентских приложений.
56 | - Атрибут **UserName** и **Password** - данные атрибуты содержат имя пользователя и пароль для досутпа к MQTT брокеру. Могут быть пустыми если авторизация не используется и брокер позволяет подключение в таком режиме.
57 |
58 | - Секция **RapSrvCnf** - данная секция содержит атрибуты, предназначенные для соединения с сервером **RapidScada**.
59 | - Атрибут **ServerHost** - данный атрибут должен содержать IP адрес сервера **RapidScada**.
60 | - Атрибут **ServerPort** - данный атрибут должен содержать номер порта, на котором сервер **RapidScada** принимает соединения от клиентских приложений.
61 | - Атрибут **ServerUser** и **ServerPwd** - данные атрибуты содержат имя пользователя и пароль для доступа к серверу **RapidScada**.
62 |
63 | - Секция **MqttSubTopics** - данная секция является родительской для секции **Topic** и предназначена для конфигурации подписок на топики.
64 | - Секция **MqttPubTopics** - данная секция является родительской для секции **Topic** и предназначена для конфигурации публикации топиков.
65 | - Секция **MqttPubCmds** - данная секция является родительской для секции **Topic** и предназначена для конфигурации публикации команд **Rapid Scada** .
66 | - Секция **MqttSubCmds** - данная секция является родительской для секции **Topic** и предназначена для конфигурации подписки на команды поступающие из MQTT брокера в виде строки.
67 | - Секция **MqttSubJSs** - данная секция является родительской для секции **Topic** и предназначена для конфигурации подписки на сообщения, поступающие из MQTT брокера в формате **JSON** .
68 |
69 | - Секция **Topic** - данная секция является дочерней для секций **MqttPubTopics**, **MqttSubTopics**, **MqttPubCmds**, **MqttSubCmds**, **MqttSubJSs** и предназначена для конфигурации топика.
70 | - Атрибут **TopicName** - данный атрибут должен содержать идентификатор топика в виде адреса **URI**. Этот адрес определяет размещение топика в дереве, описывающем структуру реального объекта.
71 | - Атрибут **QosLevel** - данный атрибут определяет значение, влияющее на гарантию доставки сообщения между клиентом и брокером. Данный атрибут может иметь следующие значения:
72 | 0 - гарантируется доставка сообщения, на уровне протокола **TCP/IP**. При этом уровне обеспечивается максимальная производительность.
73 | 1 - гарантируется доставка сообщения брокеру от одной и более попыток. При этом уровне происходит некоторое снижение производительности.
74 | 2 - гарантируется доставка собщения брокеру и клиенту. При этом уровне обеспечивается маскисмальная надежность с минимальной производительностью.
75 | - Атрибут **NumCnl** - данный атрибут определяет номер канала из таблицы конфигурации **RapidScada**.
76 | - Атрибут **PubBehavior** - данный атрибут определяет режим отправки сообщений брокеру и применяется только к **Topic**, у которых родительская секция **MqttPubTopics** . Может принимать следующие значения:
77 | "OnChange" - отправка сообщения брокеру происходит, если текущее значение отличается от предидущего. (В основном используется при подключении к внешним сервисам MQTT, которые ограничвают количество обрабатываемых сообщений)
78 | "OnAlways" - отправка сообщения брокеру происходит в каждом цикле опроса.
79 | - Атрибут **Retain** - данный атрибут определяет поведение сервера при подписке на этот топик. Используется для секции **MqttPubTopics** .
80 | "true" - при подписке на топик, сервер сразу же отправит последнее актуальное на момент подписки значение клиенту.
81 | "false" - при подписке на топик, сервер отправит значение только в момент следующей публикации значения для этого топика.
82 | - Атрибут **NDS** - данный атрибут определяет разделитель десятичного разряда для чисел с плавающей точкой. Используется для секции **MqttPubTopics** .
83 | "." - в качестве десятичного разряда будет использован символ "." .
84 | "," - в качестве десятичного разряда будет использован символ "," .
85 | - Атрибут **Prefix** - этот атрибут используется для MQTT message. Добавляется до передаваемого значения. Не должен содержать косую черту. Используется для секции **MqttPubTopics** .
86 | - Атрибут **Suffix** - этот атрибут используется для MQTT message. Добавляется после передаваемого значения. Не должен содержать косую черту. Используется для секции **MqttPubTopics** .
87 | - Атрибут **NumCmd** - данный атрибут должен содержать номер команды из канала управления. Используется в секциях **MqttPubCmds** и **MqttSubCmds** .
88 | - Атрибут **CmdType** - данный атрибут должен содержать тип команды. Используется в секции **MqttSubCmds** . Должен содержать одно из следующих значений:
89 | "St" - формируется стандартная команда управления. Формат входного значения: "20.02", "20".
90 | "BinHex" - формируется бинарная команда управления. Формат входного значения должен быть в виде строки из HEX символов.
91 | "BinTxt" - формируется бинарная команда управления. Формат входного значения должен быть в виде обычной строки UTF-8.
92 | "Req" - формируется команда внеочередного опроса КП. Входное значение игнорируется. Используется значение атрибута KpNum.
93 | - Атрибут **KpNum** - данный атрибут должен содержать номер КП. Используется в секции **MqttSubCmds** .
94 | - Атрибут **IDUser** - данный атрибут должен содержать идентификатор пользователя из под которого отправляется команада. Используется в секции **MqttSubCmds** .
95 | - Атрибут **NumCnlCtrl** - данный атрибут должен содержать номер канала управления на который будет отправлена команда. Используется в секции **MqttSubCmds** .
96 | - Атрибут **CnlCnt** - данный атрибут должен содержать значение количества каналов, в которые будут записаны значения. Используется в секции **MqttSubJSs** .
97 | - Атрибут **JSHandlerPath** - данный атрибут должен содержать путь до JS файла, который будет обрабатывать JSON значение. Используется в секции **MqttSubJSs** .
98 |
99 |
100 | Пример содержания конфигурационного файла для драйвера **KpMqtt** показан ниже:
101 |
102 | ```xml
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | ```
127 |
128 | Пример JSON строки для обработки скриптом на JavaScript:
129 |
130 | ```javascript
131 | {"Num":600,"Val":200.3,"Stat":0}
132 | ```
133 |
134 | Пример содержания скрипта на JavaScript для обработки сообщения в формате JSON:
135 |
136 | ```javascript
137 | var obj2 = JSON.parse(InMsg);
138 | jsvals[0].CnlNum=obj2.Num;
139 | jsvals[0].Stat=obj2.Stat;
140 | jsvals[0].Val=obj2.Val;
141 | mylog("Script return OK");
142 | ```
143 |
144 | При разработке скрипта на JavaScript необходимо учитывать следующие условия:
145 | - Для обработки скриптов используется библиотека [Jint](https://github.com/sebastienros/jint)
146 | - Внтури скрипта доступны глобальные функции и переменные:
147 | - Переменная **mqttJS** - позволяет получить доступ к информации об имени топика (TopicName), пути по которому расположен обработчик (JSHandlerPath), количество каналов доступных для отправки в **RapidScada** .
148 | - Переменная **InMsg** - позволяет получить строку JSON, полученную от MQTT сервера.
149 | - Переменная **jsvals** - позволяет получить доступ для чтения и записи к информации о номере канала (CnlNum), значении (Val) и статусе (Stat).
150 | - Функция **mylog** - позволяет записать строковые сообщения в лог системы **RapidScada** .
151 |
152 |
153 | Теперь необходимо создать линию связи, добавить КП и привязать к нему конфигурационный файл.
154 | Эти операции можно выполнить путем редактирования файла **ScadaCommSvcConfig.xml** в папке **Config** приложения **ScadaCommunicator**, либо при помощи одноименного графического приложения, путем заполнения соответствующих полей.
155 |
156 | Ниже приведен пример раздела файла конфигурации **ScadaCommunicator**, касающийся линии связи:
157 |
158 | ```xml
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | ```
175 |
176 | В этом примере приводится конфигурация линии связи с номером 17. В вашем случае это может быть другой, например следующий по порядку номер. Этот номер необходимо привести в соответствие с таблицей линий связи на **Scada** сервере.
177 |
178 | Описание секций и атрибутов, касающихся работы **KpMqtt** драйвера:
179 |
180 | - Секция **ReqSequence** - данная секция является родительской для секции KP и предназначена для конфигурации всех КП на этой линии связи.
181 | - Секция **KP** - эта секция содержит настройки определнного типа КП:
182 | - Атрибут **bind** (Для GUI: **Опрос КП/Выбранный КП/Привязка**) - данный атрибут определяет условие привязки каналов к определенному номеру КП из таблицы конфигурации сервера. Должен быть выставлен в false, поскольку в текущей реализации драйвера такой механизм привязки не используется.
183 | - Атрибут **dll** (Для GUI: **Опрос КП/Выбранный КП/DLL**)- данный атрибут определяет имя файла, который используется в качестве драйвера реализуеющего логику КП. Имя файла должно быть утановлено в **KpMqtt.dll** . Файл драйвера необходимо поместить в папку **KP** приложения **Scada Communicator**.
184 | - Атрибут **delay** (Для GUI: **Опрос КП/Выбранный КП/Пауза**)- данный атрибут определяет значение задержки между циклами опроса **MQTT** брокера. Данное значение выбирается индивидуально в зависимости от требований к механизму сбора данных. В данном случае выбрано значение равное 60.
185 | - Атрибут **cmdLine** (Для GUI: **Опрос КП/Выбранный КП/Командная строка**)- данный атрибут определяет имя файла конфигурации для текущего КП. В данном случае имя файла **KpMqtt_Config.xml** . Файл располагается в папке **Config** приложения **Scada Communicator**.
186 |
187 | Каких либо ограничений на количество создаваемых КП в пределах одной линии связи для данного драйвера нет. Но при этом следует учитывать и другие уловия, которые могут оказывать существенное влияние на весь механизм работы линии связи в целом. Если брать во внимание внешние условия, то следует обратить внимание на конфигурационный параметр таймаута брокера **MQTT**. Общий цикл опроса всех КП на одной линии связи не должен превышать таймаут брокера. При создании более одного КП типа **KpMqtt** на одной линии связи, следует задавать разные **ClientID**.
188 |
189 | Если в процессе эксплуатации вы обнаружите какие либо проблемы в работе данного драйвера, прошу сообщить на https://forum.rapidscada.ru/
190 |
191 | Видео https://www.youtube.com/watch?v=QTcimIik6uU
192 |
193 |
--------------------------------------------------------------------------------
/Drivers/KpMqtt/README.md:
--------------------------------------------------------------------------------
1 | KpMqtt
2 | =============================
3 | The KpMqtt.dll driver is an application library for the Scada Communicator of the Rapid SCADA project. Using this driver, can be receive and transmit messages using the MQTT protocol.
4 |
5 | Brief Introduction to MQTT Protocol
6 | =============================
7 |
8 | MQTT stands for MQ Telemetry Transport. It is a publish/subscribe, extremely simple and lightweight messaging protocol, designed for constrained devices and low-bandwidth, high-latency or unreliable networks. The design principles are to minimise network bandwidth and device resource requirements whilst also attempting to ensure reliability and some degree of assurance of delivery. These principles also turn out to make the protocol ideal of the emerging “machine-to-machine” (M2M) or “Internet of Things” world of connected devices, and for mobile applications where bandwidth and battery power are at a premium.
9 | The specification and other documentation are available via the http://mqtt.org/documentation
10 |
11 | Examples of Building Topics
12 | =============================
13 |
14 | Typically, a description of most hierarchical objects can be represented as a tree-like structure. For example, a description of access to Rapid SCADA channels could be presented in the form of the following template for a tree branch:
15 |
16 | /rapidobject/rapidkp/rapidcnl , where
17 |
18 | - **Rapidobject** indicates a unique object number from the Rapid SCADA object directory;
19 | - **Rapidkp** indicates a unique device number from the devices directory;
20 | - **Rapidcnl** indicates the channel number from the directory of input channels;
21 |
22 | As a result, the topic name can be as follows: /1/1/1 (object number / device number / channel number) or /1/1/2. That as a whole will be described by object No. 1, which includes device 1, which in turn has channel No. 1 and No. 2.
23 | Naming topics can be more specific when using custom encoding rules adopted for the automation object. But keep in mind that the total length of the topic is limited by the standard.
24 |
25 | MQTT Broker
26 | =============================
27 |
28 | The MQTT protocol description [page](http://mqtt.org/) contains information on various implementations of client and server applications. Most often, [Mosquitto server](http://mosquitto.org/) is used as a MQTT broker for small IoT projects. The project site has a lot of documentation that will allow you to deploy a server for various operating systems, including the Windows operating system. [The Mosquitto project](http://mosquitto.org/) is actively supported and complies with the latest versions of the MQTT protocol specification. The software packages of this broker are also available for routers based on the OpenWRT OS. There are also open-source projects for installing enterprise-wide brokers that can withstand high loads and provide fault tolerance mechanisms. If it is not possible to install your own MQTT broker, then can be use the services of cloud services deployed by third-party companies. Free access to such brokers usually has some limitations. Examples of such brokers:
29 | - [IoT Eclipse](http//iot.eclipse.org/)
30 | - [CloudMQTT](https//www.cloudmqtt.com/)
31 | - [HiveMQ](https://www.hivemq.com/)
32 |
33 | For testing performance and other tasks, it is convenient to use the graphical client application for working with the MQTT broker. An overview of such applications can be found [here.](https://hivemq.com/blog/seven-best-mqtt-client-tools/)
34 |
35 | Configuring the KpMqtt Driver for Scada Communicator
36 | =============================
37 |
38 | KpMqtt driver setup can be divided into 2 stages:
39 | - Creating a configuration file for the **KpMqtt** driver
40 | - Creating a communication line, adding device and linking to it a configuration file
41 | When creating a configuration file for the KpMqtt driver, the following block sections and their attributes are used:
42 | - **MqttParams** section - this section contains attributes designed to configure the connection with the MQTT broker.
43 | - **Hostname** attribute - this attribute must contain the DNS (IP) address of the MQTT broker. (Example: iot.eclipse.org)
44 | - **ClientID** attribute - this attribute must contain a unique identifier (name) of the client to connect to the MQTT broker.
45 | - **Port** attribute - this attribute must contain the port number on which the MQTT broker accepts connections from client applications.
46 | - **Username** and **Password** attributes - these attributes contain the username and password for accessing the MQTT broker. They can be empty if authorization is not used and the broker allows connection in this mode.
47 |
48 | - **RapSrvCnf** section - this section contains attributes designed to connect to the Rapid SCADA server.
49 | - **ServerHost** attribute - this attribute must contain the IP address of the Rapid SCADA server.
50 | - **ServerPort** attribute - this attribute must contain the port number on which the Rapid SCADA server accepts connections from client applications..
51 | - **ServerUser** and **ServerPwd** attributes - these attributes contain the username and password for accessing the Rapid SCADA server.
52 |
53 | - **MqttSubTopics** section - this section is the parent of the Topic section and is used to configure topic subscriptions.
54 |
55 | - **MqttPubTopics** section - this section is the parent of the Topic section and is used to configure topic publishing.
56 |
57 | - **MqttPubCmds** section - this section is the parent of the Topic section and is used to configure the publication of Rapid SCADA commands.
58 |
59 | - **MqttSubCmds** section - this section is the parent of the Topic section and is intended to configure the subscription to commands coming from the MQTT broker as a string.
60 |
61 | - **MqttSubJSs** section - this section is the parent of the Topic section and is used to configure the subscription to messages coming from the MQTT broker in JSON format.
62 |
63 | - **Topic** section - this section is a child of the MqttPubTopics, MqttSubTopics, MqttPubCmds, MqttSubCmds, MqttSubJSs sections and is intended for topic configuration.
64 |
65 | - **TopicName** attribute - this attribute must contain the identifier of the topic in the form of a URI address. This address determines the placement of the topic in the tree that describes the structure of the real object.
66 |
67 | - **QosLevel** attribute - this attribute defines the value that affects the guarantee of message delivery between the client and the broker. This attribute can have the following values: 0 - message delivery is guaranteed at the TCP / IP protocol level. This level provides maximum performance. 1 - delivery of a message to the broker from one or more attempts is guaranteed. At this level, a slight decrease in performance occurs. 2 - delivery of the message to the broker and the client is guaranteed. At this level, maximum reliability is ensured with minimal performance.
68 |
69 | - **NumCnl** attribute - this attribute defines the channel number from the Rapid SCADA configuration table.
70 |
71 | - **PubBehavior** attribute - this attribute defines the mode of sending messages to the broker and applies only to Topic, for which the parent section is MqttPubTopics.
72 |
73 | "OnChange" - a message is sent to the broker if the current value differs from the previous one. (It is mainly used when connecting to external MQTT services, which limit the number of processed messages)
74 | "OnAlways" - sending a message to the broker occurs in each polling cycle.
75 |
76 | - **Retain** attribute - this attribute defines the server behavior when subscribing to this topic. Used for the MqttPubTopics section.
77 |
78 | "true" - when subscribing to a topic, the server will immediately send the last value that was current at the time of subscription to the client.
79 | "false" - when subscribing to a topic, the server will send the value only at the time of the next publication of the value for this topic.
80 | - **NDS** attribute - this attribute defines the decimal separator for floating point numbers. Used for the MqttPubTopics section.
81 |
82 | "." - the symbol "." will be used as the decimal place.
83 | "," - the symbol "," will be used as a decimal place.
84 | - **Prefix** attribute - this attribute used for MQTT messages. Added before the value that is sent. Should not contain trailing slash. Used for the MqttPubTopics section.
85 | - **Suffix** attribute - this attribute used for MQTT messages. Added after the value that is sent. Should not contain trailing slash. Used for the MqttPubTopics section.
86 | - **NumCmd** attribute - this attribute must contain the command number from the control channel. Used in the MqttPubCmds and MqttSubCmds sections.
87 | - **CmdType** attribute - this attribute must contain the type of command. Used in the MqttSubCmds section. Must contain one of the following values:
88 |
89 | "St" - a standard control command is formed. The format of the input value: "20.02", "20".
90 | "BinHex" - a binary control command is formed. The input value format must be a string of HEX characters.
91 | "BinTxt" - a binary control command is formed. The input value format should be in the form of a normal UTF-8 string. "Req" - the command of an extraordinary poll of the CP is formed. The input value is ignored. The value of the KpNum attribute is used.
92 | - **KpNum** attribute - this attribute must contain the number of the device. Used in the MqttSubCmds section.
93 | - **IDUser** attribute - this attribute must contain the user ID from which the command is sent. Used in the MqttSubCmds section.
94 | - **NumCnlCtrl** attribute - this attribute must contain the number of the control channel to which the command will be sent. Used in the MqttSubCmds section.
95 | - **CnlCnt** attribute - this attribute must contain the value of the number of channels in which the values will be written. Used in the MqttSubJSs section.
96 | - **JSHandlerPath** attribute - this attribute must contain the path to the JS file that will handle the JSON value. Used in the MqttSubJSs section.
97 |
98 | An example of the contents of the configuration file for the **KpMqtt** driver is shown below:
99 |
100 | ```xml
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | ```
125 | An example of a JSON string for processing by a JavaScript script:
126 |
127 | ```javascript
128 | {"Num":600,"Val":200.3,"Stat":0}
129 | ```
130 | An example of the contents of a JavaScript script for processing a message in JSON format:
131 |
132 | ```javascript
133 | var obj2 = JSON.parse(InMsg);
134 | jsvals[0].CnlNum=obj2.Num;
135 | jsvals[0].Stat=obj2.Stat;
136 | jsvals[0].Val=obj2.Val;
137 | mylog("Script return OK");
138 | ```
139 | When developing a JavaScript script, the following conditions must be considered:
140 | - [The Jint](https://github.com/sebastienros/jint) library is used to process scripts
141 | - The script has global functions and variables:
142 | Variable **mqttJS** - makes it possible to access information about the topic name (TopicName), the path along which the handler is located (JSHandlerPath), the number of channels available for sending to RapidScada.
143 | InMsg variable - allows to get the JSON string received from the MQTT server.
144 | jsvals variable - makes it possible to access for reading and writing information on the channel number (CnlNum), value (Val) and status (Stat).
145 | mylog function - allows to write string messages to the RapidScada system log.
146 | Next, you need to create a communication line, add a device and attach a configuration file to it. These operations can be performed by editing the ScadaCommSvcConfig.xml file in the Config folder of the ScadaCommunicator application, or using the graphic application of the same name by filling in the appropriate fields.
147 | The following is an example of a section of a ScadaCommunicator configuration file regarding a communications link:
148 |
149 | ```xml
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | ```
165 |
166 | This example shows the configuration of the communication line with number 17. In your case, it may be different, for example, the next number in order. This number must be brought into line with the table of communication lines on the Scada server.
167 | Description of sections and attributes regarding the operation of the KpMqtt driver:
168 | - **ReqSequence** section - this section is the parent for the device section and is intended for the configuration of all devices on this communication line.
169 | - **Device** section - this section contains settings for a specific type of device:
170 | - **bind** attribute (For GUI: Device polling / Selected device / Binding) - this attribute defines the condition of channel binding to a specific device number from the server configuration table. It should be set to false, because in the current driver implementation such a binding mechanism is not used.
171 |
172 | - **Dll** attribute (For GUI: Device polling / Selected device / DLL) - this attribute defines the name of the file, which is used as a driver that implements the logic of the device. The file name must be set to KpMqtt.dll. The application file must be placed in the device folder of the ScadaCommunicator application.
173 |
174 | - **Delay** attribute (For GUI: Device polling / Selected device / Delay) - this attribute defines the value of the delay between the polling cycles of the MQTT broker. This value is selected individually depending on the requirements for the data collection mechanism. In this case, a value of 60 is selected.
175 |
176 | - **cmdLine** attribute (Device polling / Selected device / Command line) - this attribute defines the name of the configuration file for the current device. In this case, the file name is KpMqtt_Config.xml. The file is located in the Config folder of the ScadaCommunicator application.
177 |
178 | There are no restrictions on the number of created devices within the same communication line for this driver. But at the same time, other conditions should be taken into account, which can have a significant impact on the entire mechanism of the communication line as a whole. If we take into account external conditions, then you should pay attention to the configuration parameter of the **MQTT** broker timeout. The total polling cycle of all devices on one communication line should not exceed the broker's timeout. When creating more than one **KpMqtt** device on the same communication line, you must specify different **ClientIDs**.
179 |
180 | If during operation you find any problems in the operation of this driver, please inform at https://forum.rapidscada.org/
181 | video https://www.youtube.com/watch?v=QTcimIik6uU
182 |
--------------------------------------------------------------------------------
/Drivers/drivers.txt:
--------------------------------------------------------------------------------
1 | Create a solution for each new Communicator driver.
--------------------------------------------------------------------------------
/Formulas/BasicFormulas/Avg.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace BasicFormulas
5 | {
6 | ///
7 | /// The formulas for averaging.
8 | ///
9 | ///
10 | /// Usage example:
11 | /// 1. Add the content of the class to the Formulas table.
12 | /// 2.1. Set the Formula field of an input channel to the following: MovAvg(5); AvgStat()
13 | /// This means that the input channel value is the average of the last 5 values.
14 | /// 2.2. To average values over a time span: TimeAvg(10, 5); AvgStat()
15 | /// This means that the input channel value is the average over the last 10 seconds and includes max. 5 last values.
16 | ///
17 | /// These formulas are sponsored by Horacio Venturino from Uruguay.
18 | ///
19 | public class Avg : FormulaTester.FormulaTester
20 | {
21 | ///
22 | /// Data of moving averages accessing by input channels.
23 | ///
24 | public Dictionary MovAvgItems = new Dictionary();
25 |
26 | ///
27 | /// Item that provides moving averaging of one input channel.
28 | ///
29 | public class MovAvgItem
30 | {
31 | public MovAvgItem(int pointCount)
32 | {
33 | PointCount = pointCount;
34 | Values = new Queue(pointCount);
35 | Sum = 0.0;
36 | }
37 |
38 | public int PointCount { get; private set; }
39 | public Queue Values { get; private set; }
40 | public double Sum { get; protected set; }
41 | public int Count
42 | {
43 | get
44 | {
45 | return Values.Count;
46 | }
47 | }
48 | public double Avg
49 | {
50 | get
51 | {
52 | int count = Count;
53 | return count == 0 ? double.NaN : Sum / count;
54 | }
55 | }
56 |
57 | public void Append(double val)
58 | {
59 | if (Count >= PointCount)
60 | Sum -= Values.Dequeue();
61 | Values.Enqueue(val);
62 | Sum += val;
63 | }
64 | }
65 |
66 | ///
67 | /// Item that provides averaging of one input channel over a time span.
68 | ///
69 | public class TimeAvgItem : MovAvgItem
70 | {
71 | public TimeAvgItem(int timeSpanSec, int maxPointCount)
72 | : base(maxPointCount)
73 | {
74 | TimeSpan = TimeSpan.FromSeconds(timeSpanSec);
75 | TimeStamps = new Queue(maxPointCount);
76 | }
77 |
78 | public TimeSpan TimeSpan { get; private set; }
79 | public Queue TimeStamps { get; private set; }
80 |
81 | public new void Append(double val)
82 | {
83 | DateTime utcNowDT = DateTime.UtcNow;
84 |
85 | while (Count > 0)
86 | {
87 | DateTime dateTime = TimeStamps.Peek();
88 | if (utcNowDT - dateTime <= TimeSpan)
89 | {
90 | break;
91 | }
92 | else
93 | {
94 | TimeStamps.Dequeue();
95 | Sum -= Values.Dequeue();
96 | }
97 | }
98 |
99 | TimeStamps.Enqueue(utcNowDT);
100 | Values.Enqueue(val);
101 | Sum += val;
102 | }
103 | }
104 |
105 | ///
106 | /// Adds a new or gets an existing MovAvgItem.
107 | ///
108 | public MovAvgItem GetOrAddMovAvgItem(int cnlNum, int pointCount)
109 | {
110 | MovAvgItem movAvgItem;
111 | if (MovAvgItems.TryGetValue(cnlNum, out movAvgItem))
112 | {
113 | return movAvgItem;
114 | }
115 | else
116 | {
117 | movAvgItem = new MovAvgItem(pointCount);
118 | MovAvgItems.Add(cnlNum, movAvgItem);
119 | return movAvgItem;
120 | }
121 | }
122 |
123 | ///
124 | /// Adds a new or gets an existing TimeAvgItem.
125 | ///
126 | public TimeAvgItem GetOrAddTimeAvgItem(int cnlNum, int timeSpanSec, int maxPointCount)
127 | {
128 | MovAvgItem movAvgItem;
129 | if (MovAvgItems.TryGetValue(cnlNum, out movAvgItem) && movAvgItem is TimeAvgItem)
130 | {
131 | return (TimeAvgItem)movAvgItem;
132 | }
133 | else
134 | {
135 | TimeAvgItem timeAvgItem = new TimeAvgItem(timeSpanSec, maxPointCount);
136 | MovAvgItems[cnlNum] = timeAvgItem;
137 | return timeAvgItem;
138 | }
139 | }
140 |
141 | ///
142 | /// Calculates moving average of the current input channel.
143 | ///
144 | public double MovAvg(int pointCount)
145 | {
146 | return MovAvg(pointCount, CnlVal);
147 | }
148 |
149 | ///
150 | /// Calculates moving average of the current input channel.
151 | ///
152 | public double MovAvg(int pointCount, double cnlVal)
153 | {
154 | MovAvgItem movAvgItem = GetOrAddMovAvgItem(CnlNum, pointCount);
155 | movAvgItem.Append(cnlVal);
156 | return movAvgItem.Avg;
157 | }
158 |
159 | ///
160 | /// Calculates average of the current input channel over the time span.
161 | ///
162 | public double TimeAvg(int timeSpanSec, int maxPointCount)
163 | {
164 | return TimeAvg(timeSpanSec, maxPointCount, CnlVal);
165 | }
166 |
167 | ///
168 | /// Calculates average of the current input channel over the time span.
169 | ///
170 | public double TimeAvg(int timeSpanSec, int maxPointCount, double cnlVal)
171 | {
172 | TimeAvgItem timeAvgItem = GetOrAddTimeAvgItem(CnlNum, timeSpanSec, maxPointCount);
173 | timeAvgItem.Append(cnlVal);
174 | return timeAvgItem.Avg;
175 | }
176 |
177 | ///
178 | /// Gets the status for an averaged channel.
179 | ///
180 | public int AvgStat()
181 | {
182 | MovAvgItem movAvgItem; // don't use inline variable declaration
183 | return MovAvgItems.TryGetValue(CnlNum, out movAvgItem) && movAvgItem.Count > 0 ?
184 | CnlStat : 0 /*Undefined*/;
185 | }
186 |
187 | ///
188 | /// Gets the sum of all points included in the average.
189 | ///
190 | public double AvgSum(int cnlNum)
191 | {
192 | MovAvgItem movAvgItem;
193 | return MovAvgItems.TryGetValue(CnlNum, out movAvgItem) ?
194 | movAvgItem.Sum : 0;
195 | }
196 |
197 | ///
198 | /// Gets the number of all points included in the average.
199 | ///
200 | public int AvgCount(int cnlNum)
201 | {
202 | MovAvgItem movAvgItem;
203 | return MovAvgItems.TryGetValue(CnlNum, out movAvgItem) ?
204 | movAvgItem.Count : 0;
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/Formulas/BasicFormulas/BasicFormulas.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {2818B085-A5DF-48D9-A11E-48417434768A}
8 | Library
9 | Properties
10 | BasicFormulas
11 | BasicFormulas
12 | v4.0
13 | 512
14 | true
15 |
16 |
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | pdbonly
27 | true
28 | bin\Release\
29 | TRACE
30 | prompt
31 | 4
32 |
33 |
34 |
35 | ..\..\..\scada\ScadaServer\FormulaTester\FormulaTester\bin\Release\FormulaTester.dll
36 |
37 |
38 | ..\..\..\scada\ScadaData\ScadaData\bin\Release\ScadaData.dll
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Formulas/BasicFormulas/Prev.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace BasicFormulas
5 | {
6 | ///
7 | /// The formulas for working with previous channel data.
8 | ///
9 | ///
10 | /// Use StorePrev(val) to store previous data of an input channel.
11 | /// PrevVal(n), PrevStat(n), Deriv(n) and DerivStat(n) retrieve the previously saved data.
12 | ///
13 | public class Prev : FormulaTester.FormulaTester
14 | {
15 | ///
16 | /// Channel data points accessing by channel numbers.
17 | ///
18 | public Dictionary CnlDataPoints = new Dictionary();
19 | ///
20 | /// Time stamps of the channel data.
21 | ///
22 | public Dictionary CnlTimeStamps = new Dictionary();
23 |
24 | ///
25 | /// Represents channel data at a particular time.
26 | ///
27 | public struct CnlDataPoint
28 | {
29 | public DateTime TimeStamp { get; set; }
30 | public double Val { get; set; }
31 | public int Stat { get; set; }
32 | }
33 |
34 | ///
35 | /// Stores the specified channel data.
36 | ///
37 | public double StorePrev(double val)
38 | {
39 | DateTime dateTime;
40 | DateTime timeStamp = CnlTimeStamps.TryGetValue(CnlNum, out dateTime) ?
41 | dateTime : DateTime.MinValue;
42 |
43 | CnlDataPoints[CnlNum] = new CnlDataPoint
44 | {
45 | TimeStamp = timeStamp,
46 | Val = Val(CnlNum),
47 | Stat = Stat(CnlNum)
48 | };
49 |
50 | CnlTimeStamps[CnlNum] = DateTime.Now;
51 | return val;
52 | }
53 |
54 | ///
55 | /// Gets the specified channel value.
56 | ///
57 | public double PrevVal(int n)
58 | {
59 | CnlDataPoint point; // don't use inline variable declaration
60 | return CnlDataPoints.TryGetValue(n, out point) ?
61 | point.Val : 0.0;
62 | }
63 |
64 | ///
65 | /// Gets the specified channel status.
66 | ///
67 | public int PrevStat(int n)
68 | {
69 | CnlDataPoint point;
70 | return CnlDataPoints.TryGetValue(n, out point) ?
71 | point.Stat : 0;
72 | }
73 |
74 | ///
75 | /// Gets the derivative of the specified channel.
76 | ///
77 | public double Deriv(int n)
78 | {
79 | CnlDataPoint point;
80 | if (CnlDataPoints.TryGetValue(n, out point) && point.TimeStamp > DateTime.MinValue)
81 | {
82 | DateTime nowDT = DateTime.Now;
83 | return nowDT > point.TimeStamp ?
84 | (Val(n) - point.Val) / (nowDT - point.TimeStamp).TotalSeconds : 0;
85 | }
86 | else
87 | {
88 | return 0.0;
89 | }
90 | }
91 |
92 | ///
93 | /// Gets the status of the channel derivative.
94 | ///
95 | public double DerivStat(int n)
96 | {
97 | CnlDataPoint point;
98 | return Stat(n) > 0 && CnlDataPoints.TryGetValue(n, out point) &&
99 | point.TimeStamp > DateTime.MinValue && point.Stat > 0 ?
100 | Stat(n) : 0;
101 | }
102 |
103 | ///
104 | /// Gets the time difference between the current and previous data points.
105 | ///
106 | public double TimeDiff(int n)
107 | {
108 | CnlDataPoint point;
109 | if (CnlDataPoints.TryGetValue(n, out point) && point.TimeStamp > DateTime.MinValue)
110 | {
111 | return (DateTime.Now - point.TimeStamp).TotalSeconds;
112 | }
113 | else
114 | {
115 | return 0.0;
116 | }
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Formulas/BasicFormulas/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | // General Information about an assembly is controlled through the following
5 | // set of attributes. Change these attribute values to modify the information
6 | // associated with an assembly.
7 | [assembly: AssemblyTitle("BasicFormulas")]
8 | [assembly: AssemblyDescription("")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("")]
11 | [assembly: AssemblyProduct("Rapid SCADA")]
12 | [assembly: AssemblyCopyright("Copyright © 2019")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // Setting ComVisible to false makes the types in this assembly not visible
17 | // to COM components. If you need to access a type in this assembly from
18 | // COM, set the ComVisible attribute to true on that type.
19 | [assembly: ComVisible(false)]
20 |
21 | // The following GUID is for the ID of the typelib if this project is exposed to COM
22 | [assembly: Guid("2818b085-a5df-48d9-a11e-48417434768a")]
23 |
24 | // Version information for an assembly consists of the following four values:
25 | //
26 | // Major Version
27 | // Minor Version
28 | // Build Number
29 | // Revision
30 | //
31 | // You can specify all the values or you can default the Build and Revision Numbers
32 | // by using the '*' as shown below:
33 | // [assembly: AssemblyVersion("1.0.*")]
34 | [assembly: AssemblyVersion("1.0.0.0")]
35 | [assembly: AssemblyFileVersion("1.0.0.0")]
36 |
--------------------------------------------------------------------------------
/Formulas/BasicFormulasTest/AvgTest.cs:
--------------------------------------------------------------------------------
1 | using BasicFormulas;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using System.Threading;
4 |
5 | namespace BasicFormulasTest
6 | {
7 | ///
8 | /// Unit tests for the BasicFormulas.Avg class.
9 | ///
10 | [TestClass]
11 | public class AvgTest
12 | {
13 | [TestMethod]
14 | public void MovAvgTest()
15 | {
16 | const int PointCount = 3;
17 | Avg avg = new Avg();
18 |
19 | avg.SetCurCnl(1, 1.0, 1);
20 | avg.MovAvg(PointCount);
21 |
22 | avg.SetCurCnl(1, 2.0, 1);
23 | avg.MovAvg(PointCount);
24 |
25 | avg.SetCurCnl(1, 3.0, 1);
26 | avg.MovAvg(PointCount);
27 |
28 | avg.SetCurCnl(1, 4.0, 1);
29 | avg.MovAvg(PointCount);
30 |
31 | avg.SetCurCnl(1, 5.0, 1);
32 | double actualResult = avg.MovAvg(PointCount);
33 | Assert.AreEqual(4.0, actualResult);
34 | }
35 |
36 | [TestMethod]
37 | public void TimeAvgTest()
38 | {
39 | const int TimeSpanSec = 1;
40 | const int PointCount = 10;
41 | Avg avg = new Avg();
42 |
43 | avg.SetCurCnl(1, 1.0, 1);
44 | avg.TimeAvg(TimeSpanSec, PointCount);
45 | Thread.Sleep(300);
46 |
47 | avg.SetCurCnl(1, 2.0, 1);
48 | avg.TimeAvg(TimeSpanSec, PointCount);
49 | Thread.Sleep(300);
50 |
51 | avg.SetCurCnl(1, 3.0, 1);
52 | avg.TimeAvg(TimeSpanSec, PointCount);
53 | Thread.Sleep(300);
54 |
55 | avg.SetCurCnl(1, 4.0, 1);
56 | avg.TimeAvg(TimeSpanSec, PointCount);
57 | Thread.Sleep(300);
58 |
59 | avg.SetCurCnl(1, 5.0, 1);
60 | double actualResult = avg.TimeAvg(TimeSpanSec, PointCount);
61 | Assert.AreEqual(3.5, actualResult);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Formulas/BasicFormulasTest/BasicFormulasTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Debug
7 | AnyCPU
8 | {FE932669-7DDD-4CA0-92DD-701186A2D3BB}
9 | Library
10 | Properties
11 | BasicFormulasTest
12 | BasicFormulasTest
13 | v4.7.2
14 | 512
15 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
16 | 15.0
17 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
18 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
19 | False
20 | UnitTest
21 |
22 |
23 |
24 |
25 | true
26 | full
27 | false
28 | bin\Debug\
29 | DEBUG;TRACE
30 | prompt
31 | 4
32 |
33 |
34 | pdbonly
35 | true
36 | bin\Release\
37 | TRACE
38 | prompt
39 | 4
40 |
41 |
42 |
43 | ..\..\..\scada\ScadaServer\FormulaTester\FormulaTester\bin\Release\FormulaTester.dll
44 |
45 |
46 | ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
47 |
48 |
49 | ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {2818b085-a5df-48d9-a11e-48417434768a}
64 | BasicFormulas
65 |
66 |
67 |
68 |
69 |
70 |
71 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/Formulas/BasicFormulasTest/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyTitle("BasicFormulasTest")]
6 | [assembly: AssemblyDescription("")]
7 | [assembly: AssemblyConfiguration("")]
8 | [assembly: AssemblyCompany("")]
9 | [assembly: AssemblyProduct("Rapid SCADA")]
10 | [assembly: AssemblyCopyright("Copyright © 2019")]
11 | [assembly: AssemblyTrademark("")]
12 | [assembly: AssemblyCulture("")]
13 |
14 | [assembly: ComVisible(false)]
15 |
16 | [assembly: Guid("fe932669-7ddd-4ca0-92dd-701186a2d3bb")]
17 |
18 | // [assembly: AssemblyVersion("1.0.*")]
19 | [assembly: AssemblyVersion("1.0.0.0")]
20 | [assembly: AssemblyFileVersion("1.0.0.0")]
21 |
--------------------------------------------------------------------------------
/Formulas/BasicFormulasTest/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Formulas/Formulas.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.28307.329
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicFormulas", "BasicFormulas\BasicFormulas.csproj", "{2818B085-A5DF-48D9-A11E-48417434768A}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicFormulasTest", "BasicFormulasTest\BasicFormulasTest.csproj", "{FE932669-7DDD-4CA0-92DD-701186A2D3BB}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {2818B085-A5DF-48D9-A11E-48417434768A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {2818B085-A5DF-48D9-A11E-48417434768A}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {2818B085-A5DF-48D9-A11E-48417434768A}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {2818B085-A5DF-48D9-A11E-48417434768A}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {FE932669-7DDD-4CA0-92DD-701186A2D3BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {FE932669-7DDD-4CA0-92DD-701186A2D3BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {FE932669-7DDD-4CA0-92DD-701186A2D3BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {FE932669-7DDD-4CA0-92DD-701186A2D3BB}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {6143FA74-9F71-4F87-B8A5-F421A65CBBA2}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Modules/modules.txt:
--------------------------------------------------------------------------------
1 | Create a solution for each new Server module.
--------------------------------------------------------------------------------
/Plugins/CustomPageExample/custom/MyPage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Custom Page Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | JavaScript API Examples
15 | Title 1
16 | Description
17 |
18 |
19 | Title 2
20 | Description
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Plugins/CustomPageExample/custom/mypage.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 20px;
3 | }
4 |
5 | div.descr {
6 | margin: 5px 0;
7 | }
8 |
9 | div.data {
10 | border: 1px solid gray;
11 | height: 100px;
12 | margin: 5px 0;
13 | overflow: scroll;
14 | white-space: nowrap;
15 | }
16 |
17 | #divData1 {
18 | height: 70px;
19 | }
20 |
21 | #divData2 {
22 | height: 500px;
23 | }
24 |
--------------------------------------------------------------------------------
/Plugins/CustomPageExample/custom/mypage.js:
--------------------------------------------------------------------------------
1 | var REFR_RATE = 1000; // current data refresh rate, ms
2 | var CNL_NUM = 101; // the input channel number
3 | var VIEW_ID = 2; // the view that includes the input channel
4 |
5 | // Start cyclic refresh of current data
6 | function startRefreshingCurData() {
7 | getCurData(function (success) {
8 | if (!success) {
9 | console.warn("Error getting current data");
10 | }
11 |
12 | setTimeout(startRefreshingCurData, REFR_RATE);
13 | });
14 | }
15 |
16 | // Request and display current data
17 | function getCurData(callback) {
18 | scada.clientAPI.getCurCnlData(CNL_NUM, function (success, cnlData) {
19 | if (success) {
20 | $("#divData1").html("Value = " + cnlData.Val + "
Status = " + cnlData.Stat);
21 | callback(true);
22 | } else {
23 | callback(false);
24 | }
25 | });
26 | }
27 |
28 | // Request and display hourly data
29 | function getHourData() {
30 | var hourPeriod = new scada.HourPeriod();
31 | hourPeriod.date = new Date(); // current date
32 | hourPeriod.startHour = 0;
33 | hourPeriod.endHour = 23;
34 |
35 | var cnlFilter = new scada.CnlFilter();
36 | cnlFilter.cnlNums = [CNL_NUM];
37 | cnlFilter.viewIDs = [VIEW_ID];
38 |
39 | var selectMode = false; // true to get only existing data, otherwise get data hour by hour
40 | var requestDataAge = [];
41 |
42 | scada.clientAPI.getHourCnlData(hourPeriod, cnlFilter, selectMode, requestDataAge,
43 | function (success, hourCnlDataArr, dataAge) {
44 | if (success) {
45 | // display data
46 | var element = $("#divData2");
47 | element.html("");
48 |
49 | for (var item of hourCnlDataArr) {
50 | element.append("Hour = " + item.Hour + "
");
51 |
52 | for (var cnlData of item.CnlDataExtArr) {
53 | element.append(
54 | "Channel = " + cnlData.CnlNum + ", " +
55 | "Value = " + cnlData.Val + ", " +
56 | "Status = " + cnlData.Stat + ", " +
57 | "Text = " + cnlData.Text + ", " +
58 | "TextWithUnit = " + cnlData.TextWithUnit + ", " +
59 | "Color = " + cnlData.Color + "
");
60 | }
61 |
62 | element.append("
");
63 | }
64 | } else {
65 | console.warn("Error getting hourly data");
66 | }
67 | });
68 | }
69 |
70 | $(document).ready(function () {
71 | // initialize the API
72 | // if Ajax queue is not available, set scada.clientAPI.rootPath
73 | scada.clientAPI.ajaxQueue = scada.ajaxQueueLocator.getAjaxQueue();
74 |
75 | // prepare a web page
76 | $("#hdrTitle1").text("Get Current Data Example");
77 | $("#divDescr1").text("Cyclically request the current data of channel " + CNL_NUM);
78 |
79 | $("#hdrTitle2").text("Get Hourly Data Example");
80 | $("#divDescr2").text("Request the hourly data of channel " + CNL_NUM + " for the current date");
81 |
82 | // request data
83 | startRefreshingCurData();
84 | getHourData();
85 | });
86 |
--------------------------------------------------------------------------------
/Plugins/CustomPageExample/readme.txt:
--------------------------------------------------------------------------------
1 | This example demonstrates the use of Rapid SCADA JavaScript API.
2 | It's compatible with the HelloWorld project.
3 |
4 | How to deploy:
5 | 1. Copy the custom directory into C:\SCADA\ScadaWeb\
6 | 2. Open the project in Administrator and add a new record in the Interface table:
7 | +--------------------------+-------------+--------------------+
8 | | Path | View Type | Title |
9 | +--------------------------+-------------+--------------------+
10 | | ../../custom/MyPage.html | WebPageView | HelloWorld\My Page |
11 | +--------------------------+-------------+--------------------+
12 | 3. Upload the configuration.
--------------------------------------------------------------------------------
/Plugins/plugins.txt:
--------------------------------------------------------------------------------
1 | Create a solution for each new Webstation plugin.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scada-community
2 | Modules and tools for Rapid SCADA developed by the community.
3 |
4 | ## How to Contribute
5 | Make a fork of this repository. Create a new branch within your fork. When you are ready and your changes are tested, submit a pull request to pass the new code to this repository. See [this manual](https://help.github.com/articles/fork-a-repo/#propose-changes-to-someone-elses-project) for the details.
6 |
7 | ## Known GitHub Repositories Related to Rapid SCADA:
8 | * OPC UA Driver https://github.com/syndrome5/KpOpcUA
9 | * MQTT and Workflow Drivers https://github.com/bersim/OpenKPs
10 | * Raspberry Pi, Mercury Meter Drivers https://github.com/Manjey73/OpnenKPs
11 | * Sound Alarm Module https://github.com/kolod/modalarm
12 |
--------------------------------------------------------------------------------
/Samples/WebApiClientSample/WebApiClientSample.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.2.32519.379
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiClientSample", "WebApiClientSample\WebApiClientSample.csproj", "{9081F4BE-81CF-4F13-B772-6FF915C91FD2}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {9081F4BE-81CF-4F13-B772-6FF915C91FD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {9081F4BE-81CF-4F13-B772-6FF915C91FD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {9081F4BE-81CF-4F13-B772-6FF915C91FD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {9081F4BE-81CF-4F13-B772-6FF915C91FD2}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {11266C23-552B-4592-809B-143B20182C69}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Samples/WebApiClientSample/WebApiClientSample/Program.cs:
--------------------------------------------------------------------------------
1 | using Scada;
2 | using System.Net;
3 | using System.Net.Http.Json;
4 | using System.Text;
5 | using System.Text.Json;
6 |
7 | namespace WebApiClientSample
8 | {
9 | ///
10 | /// Demonstrates how to use Rapid SCADA 6 web API.
11 | ///
12 | internal class Program
13 | {
14 | private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
15 |
16 | private static string ReadContentAsString(HttpContent httpContent)
17 | {
18 | using Stream responseStream = httpContent.ReadAsStream();
19 | using StreamReader reader = new(responseStream, Encoding.UTF8);
20 | return reader.ReadToEnd();
21 | }
22 |
23 | private static void WriteResponse(HttpResponseMessage httpResponse, out string responseString)
24 | {
25 | Console.WriteLine(string.Format("Response status = {0} ({1})",
26 | (int)httpResponse.StatusCode, httpResponse.StatusCode));
27 | responseString = ReadContentAsString(httpResponse.Content);
28 | Console.WriteLine("Response content = " + responseString);
29 | }
30 |
31 | static void Main(string[] args)
32 | {
33 | const string RootPath = "http://localhost:8080/";
34 | Console.WriteLine("Web API Client Sample");
35 |
36 | CookieContainer cookies = new();
37 | HttpClientHandler handler = new() { CookieContainer = cookies };
38 | HttpClient httpClient = new(handler);
39 |
40 | // login
41 | Console.WriteLine("Login");
42 | Uri loginUri = new(RootPath + "Api/Auth/Login");
43 | HttpRequestMessage loginRequest = new(HttpMethod.Post, loginUri)
44 | {
45 | Content = JsonContent.Create(new
46 | {
47 | Username = "admin",
48 | Password = "scada"
49 | })
50 | };
51 | HttpResponseMessage loginResponse = httpClient.Send(loginRequest);
52 | WriteResponse(loginResponse, out string loginResponseString);
53 | loginRequest.Dispose();
54 | loginResponse.Dispose();
55 |
56 | // parse login result
57 | if (string.IsNullOrEmpty(loginResponseString))
58 | {
59 | Console.WriteLine("No response");
60 | }
61 | else if (JsonSerializer.Deserialize(loginResponseString, JsonOptions) is
62 | SimpleResult loginResult)
63 | {
64 | Console.WriteLine("Ok = " + loginResult.Ok);
65 | Console.WriteLine("Msg = " + loginResult.Msg);
66 | }
67 | else
68 | {
69 | Console.WriteLine("Unable to parse login result");
70 | }
71 |
72 | // show cookies
73 | if (cookies.GetCookies(loginUri) is CookieCollection responseCookies &&
74 | responseCookies.Count > 0)
75 | {
76 | Console.WriteLine("Cookies:");
77 |
78 | foreach (Cookie cookie in responseCookies)
79 | {
80 | Console.WriteLine(string.Format("{0} = {1}", cookie.Name, cookie.Value));
81 | }
82 | }
83 | else
84 | {
85 | Console.WriteLine("No cookies");
86 | }
87 |
88 | // get current data
89 | Console.WriteLine();
90 | Console.WriteLine("Get current data");
91 | Uri requestUri = new(RootPath + "Api/Main/GetCurData?cnlNums=101-105,110");
92 | HttpRequestMessage request = new(HttpMethod.Get, requestUri);
93 | HttpResponseMessage response = httpClient.Send(request);
94 | WriteResponse(response, out _);
95 | request.Dispose();
96 | response.Dispose();
97 |
98 | // logout
99 | Console.WriteLine();
100 | Console.WriteLine("Logout");
101 | Uri logoutUri = new(RootPath + "Api/Auth/Logout");
102 | HttpRequestMessage logoutRequest = new(HttpMethod.Post, logoutUri);
103 | HttpResponseMessage logoutResponse = httpClient.Send(logoutRequest);
104 | WriteResponse(logoutResponse, out _);
105 | logoutRequest.Dispose();
106 | logoutResponse.Dispose();
107 |
108 | handler.Dispose();
109 | httpClient.Dispose();
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Samples/WebApiClientSample/WebApiClientSample/WebApiClientSample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 | ..\..\..\..\scada-v6\ScadaCommon\ScadaCommon\bin\Release\netstandard2.0\ScadaCommon.dll
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------