Add Machine Data seed (tables, stored procedures, sample data) and fix SA password for shell compatibility
This commit is contained in:
@@ -24,11 +24,17 @@ The MS SQL container does not auto-run init scripts. After the first `docker com
|
||||
|
||||
```bash
|
||||
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1!' -C \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
|
||||
-i /docker-entrypoint-initdb.d/setup.sql
|
||||
```
|
||||
|
||||
This creates the `ScadaLinkConfig` and `ScadaLinkMachineData` databases and the `scadalink_app` login.
|
||||
This creates the `ScadaLinkConfig` and `ScadaLinkMachineData` databases and the `scadalink_app` login. Then seed the Machine Data database:
|
||||
|
||||
```bash
|
||||
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
|
||||
-i /docker-entrypoint-initdb.d/machinedata_seed.sql
|
||||
```
|
||||
|
||||
## Stopping & Teardown
|
||||
|
||||
|
||||
@@ -34,11 +34,12 @@ services:
|
||||
- "1433:1433"
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
MSSQL_SA_PASSWORD: "ScadaLink_Dev1!"
|
||||
MSSQL_SA_PASSWORD: "ScadaLink_Dev1#"
|
||||
MSSQL_PID: "Developer"
|
||||
volumes:
|
||||
- scadalink-mssql-data:/var/opt/mssql
|
||||
- ./mssql/setup.sql:/docker-entrypoint-initdb.d/setup.sql:ro
|
||||
- ./mssql/machinedata_seed.sql:/docker-entrypoint-initdb.d/machinedata_seed.sql:ro
|
||||
restart: unless-stopped
|
||||
|
||||
smtp:
|
||||
|
||||
330
infra/mssql/machinedata_seed.sql
Normal file
330
infra/mssql/machinedata_seed.sql
Normal file
@@ -0,0 +1,330 @@
|
||||
-- ScadaLink Machine Data Database seed script
|
||||
-- Populates ScadaLinkMachineData with realistic SCADA/MES tables,
|
||||
-- sample data, and stored procedures for development and testing.
|
||||
--
|
||||
-- Run after setup.sql:
|
||||
-- docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-- -S localhost -U sa -P 'ScadaLink_Dev1#' -C \
|
||||
-- -i /docker-entrypoint-initdb.d/machinedata_seed.sql
|
||||
|
||||
USE ScadaLinkMachineData;
|
||||
GO
|
||||
|
||||
-- =========================================================================
|
||||
-- Tables
|
||||
-- =========================================================================
|
||||
|
||||
-- Tag history: time-series data collected from OPC UA / custom protocols
|
||||
IF OBJECT_ID('dbo.TagHistory', 'U') IS NULL
|
||||
CREATE TABLE dbo.TagHistory (
|
||||
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||
TagPath NVARCHAR(256) NOT NULL,
|
||||
Timestamp DATETIME2(3) NOT NULL,
|
||||
Value FLOAT NULL,
|
||||
StringValue NVARCHAR(512) NULL,
|
||||
Quality TINYINT NOT NULL DEFAULT 192, -- 192 = Good
|
||||
SiteId NVARCHAR(64) NOT NULL,
|
||||
INDEX IX_TagHistory_TagPath_Timestamp (TagPath, Timestamp DESC),
|
||||
INDEX IX_TagHistory_SiteId_Timestamp (SiteId, Timestamp DESC)
|
||||
);
|
||||
GO
|
||||
|
||||
-- Production counts: shift/line production totals
|
||||
IF OBJECT_ID('dbo.ProductionCounts', 'U') IS NULL
|
||||
CREATE TABLE dbo.ProductionCounts (
|
||||
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||
SiteId NVARCHAR(64) NOT NULL,
|
||||
LineName NVARCHAR(128) NOT NULL,
|
||||
ShiftDate DATE NOT NULL,
|
||||
ShiftNumber TINYINT NOT NULL,
|
||||
GoodCount INT NOT NULL DEFAULT 0,
|
||||
RejectCount INT NOT NULL DEFAULT 0,
|
||||
Efficiency DECIMAL(5,2) NULL,
|
||||
RecordedAt DATETIME2(3) NOT NULL DEFAULT SYSUTCDATETIME(),
|
||||
INDEX IX_ProductionCounts_Site_Date (SiteId, ShiftDate DESC)
|
||||
);
|
||||
GO
|
||||
|
||||
-- Equipment events: state changes, faults, maintenance
|
||||
IF OBJECT_ID('dbo.EquipmentEvents', 'U') IS NULL
|
||||
CREATE TABLE dbo.EquipmentEvents (
|
||||
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||
SiteId NVARCHAR(64) NOT NULL,
|
||||
EquipmentId NVARCHAR(128) NOT NULL,
|
||||
EventType NVARCHAR(32) NOT NULL, -- 'StateChange', 'Fault', 'Maintenance', 'Alarm'
|
||||
PreviousState NVARCHAR(64) NULL,
|
||||
NewState NVARCHAR(64) NOT NULL,
|
||||
Description NVARCHAR(512) NULL,
|
||||
Timestamp DATETIME2(3) NOT NULL,
|
||||
INDEX IX_EquipmentEvents_Equipment_Time (EquipmentId, Timestamp DESC),
|
||||
INDEX IX_EquipmentEvents_Site_Type (SiteId, EventType, Timestamp DESC)
|
||||
);
|
||||
GO
|
||||
|
||||
-- Batch records: production batch tracking
|
||||
IF OBJECT_ID('dbo.BatchRecords', 'U') IS NULL
|
||||
CREATE TABLE dbo.BatchRecords (
|
||||
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||
BatchId NVARCHAR(64) NOT NULL UNIQUE,
|
||||
SiteId NVARCHAR(64) NOT NULL,
|
||||
RecipeId NVARCHAR(64) NOT NULL,
|
||||
Status NVARCHAR(32) NOT NULL DEFAULT 'InProgress', -- 'InProgress', 'Complete', 'Aborted'
|
||||
StartTime DATETIME2(3) NOT NULL,
|
||||
EndTime DATETIME2(3) NULL,
|
||||
TotalQuantity DECIMAL(10,2) NULL,
|
||||
Operator NVARCHAR(128) NULL,
|
||||
Notes NVARCHAR(1024) NULL,
|
||||
INDEX IX_BatchRecords_Site_Status (SiteId, Status),
|
||||
INDEX IX_BatchRecords_StartTime (StartTime DESC)
|
||||
);
|
||||
GO
|
||||
|
||||
-- Alarm history: historical alarm events
|
||||
IF OBJECT_ID('dbo.AlarmHistory', 'U') IS NULL
|
||||
CREATE TABLE dbo.AlarmHistory (
|
||||
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||
SiteId NVARCHAR(64) NOT NULL,
|
||||
AlarmName NVARCHAR(256) NOT NULL,
|
||||
Severity TINYINT NOT NULL, -- 1=Low, 2=Medium, 3=High, 4=Critical
|
||||
State NVARCHAR(32) NOT NULL, -- 'Active', 'Acknowledged', 'Cleared'
|
||||
ActivatedAt DATETIME2(3) NOT NULL,
|
||||
AcknowledgedAt DATETIME2(3) NULL,
|
||||
ClearedAt DATETIME2(3) NULL,
|
||||
AcknowledgedBy NVARCHAR(128) NULL,
|
||||
Message NVARCHAR(512) NULL,
|
||||
INDEX IX_AlarmHistory_Site_Active (SiteId, State, ActivatedAt DESC),
|
||||
INDEX IX_AlarmHistory_Severity (Severity, ActivatedAt DESC)
|
||||
);
|
||||
GO
|
||||
|
||||
-- =========================================================================
|
||||
-- Stored Procedures
|
||||
-- =========================================================================
|
||||
|
||||
-- Get tag history for a tag path within a date range
|
||||
IF OBJECT_ID('dbo.usp_GetTagHistory', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_GetTagHistory;
|
||||
GO
|
||||
CREATE PROCEDURE dbo.usp_GetTagHistory
|
||||
@TagPath NVARCHAR(256),
|
||||
@StartTime DATETIME2(3),
|
||||
@EndTime DATETIME2(3),
|
||||
@MaxRows INT = 10000
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT TOP (@MaxRows)
|
||||
TagPath, Timestamp, Value, StringValue, Quality, SiteId
|
||||
FROM dbo.TagHistory
|
||||
WHERE TagPath = @TagPath
|
||||
AND Timestamp >= @StartTime
|
||||
AND Timestamp <= @EndTime
|
||||
ORDER BY Timestamp DESC;
|
||||
END;
|
||||
GO
|
||||
|
||||
-- Get production summary for a site over a date range
|
||||
IF OBJECT_ID('dbo.usp_GetProductionSummary', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_GetProductionSummary;
|
||||
GO
|
||||
CREATE PROCEDURE dbo.usp_GetProductionSummary
|
||||
@SiteId NVARCHAR(64),
|
||||
@StartDate DATE,
|
||||
@EndDate DATE
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT
|
||||
LineName,
|
||||
SUM(GoodCount) AS TotalGood,
|
||||
SUM(RejectCount) AS TotalReject,
|
||||
SUM(GoodCount) + SUM(RejectCount) AS TotalProduced,
|
||||
CASE
|
||||
WHEN SUM(GoodCount) + SUM(RejectCount) > 0
|
||||
THEN CAST(SUM(GoodCount) * 100.0 / (SUM(GoodCount) + SUM(RejectCount)) AS DECIMAL(5,2))
|
||||
ELSE 0
|
||||
END AS YieldPercent,
|
||||
AVG(Efficiency) AS AvgEfficiency,
|
||||
COUNT(DISTINCT ShiftDate) AS DaysReported
|
||||
FROM dbo.ProductionCounts
|
||||
WHERE SiteId = @SiteId
|
||||
AND ShiftDate >= @StartDate
|
||||
AND ShiftDate <= @EndDate
|
||||
GROUP BY LineName
|
||||
ORDER BY LineName;
|
||||
END;
|
||||
GO
|
||||
|
||||
-- Insert a batch record (used by CachedWrite from scripts)
|
||||
IF OBJECT_ID('dbo.usp_InsertBatchRecord', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_InsertBatchRecord;
|
||||
GO
|
||||
CREATE PROCEDURE dbo.usp_InsertBatchRecord
|
||||
@BatchId NVARCHAR(64),
|
||||
@SiteId NVARCHAR(64),
|
||||
@RecipeId NVARCHAR(64),
|
||||
@TotalQuantity DECIMAL(10,2) = NULL,
|
||||
@Operator NVARCHAR(128) = NULL,
|
||||
@Notes NVARCHAR(1024) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
INSERT INTO dbo.BatchRecords (BatchId, SiteId, RecipeId, Status, StartTime, TotalQuantity, Operator, Notes)
|
||||
VALUES (@BatchId, @SiteId, @RecipeId, 'InProgress', SYSUTCDATETIME(), @TotalQuantity, @Operator, @Notes);
|
||||
|
||||
SELECT SCOPE_IDENTITY() AS Id, @BatchId AS BatchId;
|
||||
END;
|
||||
GO
|
||||
|
||||
-- Complete or abort a batch
|
||||
IF OBJECT_ID('dbo.usp_CompleteBatch', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_CompleteBatch;
|
||||
GO
|
||||
CREATE PROCEDURE dbo.usp_CompleteBatch
|
||||
@BatchId NVARCHAR(64),
|
||||
@Status NVARCHAR(32), -- 'Complete' or 'Aborted'
|
||||
@TotalQuantity DECIMAL(10,2) = NULL,
|
||||
@Notes NVARCHAR(1024) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
UPDATE dbo.BatchRecords
|
||||
SET Status = @Status,
|
||||
EndTime = SYSUTCDATETIME(),
|
||||
TotalQuantity = COALESCE(@TotalQuantity, TotalQuantity),
|
||||
Notes = COALESCE(@Notes, Notes)
|
||||
WHERE BatchId = @BatchId
|
||||
AND Status = 'InProgress';
|
||||
|
||||
IF @@ROWCOUNT = 0
|
||||
THROW 50001, 'Batch not found or not in progress.', 1;
|
||||
|
||||
SELECT BatchId, Status, StartTime, EndTime, TotalQuantity
|
||||
FROM dbo.BatchRecords
|
||||
WHERE BatchId = @BatchId;
|
||||
END;
|
||||
GO
|
||||
|
||||
-- Get recent equipment events
|
||||
IF OBJECT_ID('dbo.usp_GetEquipmentEvents', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_GetEquipmentEvents;
|
||||
GO
|
||||
CREATE PROCEDURE dbo.usp_GetEquipmentEvents
|
||||
@SiteId NVARCHAR(64),
|
||||
@EquipmentId NVARCHAR(128) = NULL,
|
||||
@EventType NVARCHAR(32) = NULL,
|
||||
@Hours INT = 24
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
DECLARE @Since DATETIME2(3) = DATEADD(HOUR, -@Hours, SYSUTCDATETIME());
|
||||
|
||||
SELECT
|
||||
EquipmentId, EventType, PreviousState, NewState, Description, Timestamp
|
||||
FROM dbo.EquipmentEvents
|
||||
WHERE SiteId = @SiteId
|
||||
AND Timestamp >= @Since
|
||||
AND (@EquipmentId IS NULL OR EquipmentId = @EquipmentId)
|
||||
AND (@EventType IS NULL OR EventType = @EventType)
|
||||
ORDER BY Timestamp DESC;
|
||||
END;
|
||||
GO
|
||||
|
||||
-- Get active alarms for a site
|
||||
IF OBJECT_ID('dbo.usp_GetActiveAlarms', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_GetActiveAlarms;
|
||||
GO
|
||||
CREATE PROCEDURE dbo.usp_GetActiveAlarms
|
||||
@SiteId NVARCHAR(64),
|
||||
@MinSeverity TINYINT = 1
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT
|
||||
AlarmName, Severity, State, ActivatedAt, AcknowledgedAt,
|
||||
AcknowledgedBy, Message
|
||||
FROM dbo.AlarmHistory
|
||||
WHERE SiteId = @SiteId
|
||||
AND State IN ('Active', 'Acknowledged')
|
||||
AND Severity >= @MinSeverity
|
||||
ORDER BY Severity DESC, ActivatedAt DESC;
|
||||
END;
|
||||
GO
|
||||
|
||||
-- =========================================================================
|
||||
-- Sample Data
|
||||
-- =========================================================================
|
||||
|
||||
-- Tag history (last few hours of data for two sites)
|
||||
DECLARE @now DATETIME2(3) = SYSUTCDATETIME();
|
||||
|
||||
INSERT INTO dbo.TagHistory (TagPath, Timestamp, Value, Quality, SiteId) VALUES
|
||||
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -120, @now), 45.2, 192, 'SiteA'),
|
||||
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -110, @now), 46.1, 192, 'SiteA'),
|
||||
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -100, @now), 44.8, 192, 'SiteA'),
|
||||
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -90, @now), 47.3, 192, 'SiteA'),
|
||||
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -80, @now), 45.9, 192, 'SiteA'),
|
||||
('SiteA/Pump-001/Flow', DATEADD(MINUTE, -120, @now), 120.5, 192, 'SiteA'),
|
||||
('SiteA/Pump-001/Flow', DATEADD(MINUTE, -110, @now), 121.2, 192, 'SiteA'),
|
||||
('SiteA/Pump-001/Flow', DATEADD(MINUTE, -100, @now), 119.8, 192, 'SiteA'),
|
||||
('SiteA/Tank-001/Level', DATEADD(MINUTE, -120, @now), 72.0, 192, 'SiteA'),
|
||||
('SiteA/Tank-001/Level', DATEADD(MINUTE, -110, @now), 73.5, 192, 'SiteA'),
|
||||
('SiteA/Tank-001/Level', DATEADD(MINUTE, -100, @now), 75.1, 192, 'SiteA'),
|
||||
('SiteA/Tank-001/Level', DATEADD(MINUTE, -90, @now), 76.8, 192, 'SiteA'),
|
||||
('SiteA/Tank-001/Temperature', DATEADD(MINUTE, -120, @now), 65.3, 192, 'SiteA'),
|
||||
('SiteA/Tank-001/Temperature', DATEADD(MINUTE, -110, @now), 65.5, 192, 'SiteA'),
|
||||
('SiteA/Conv-001/Speed', DATEADD(MINUTE, -120, @now), 2.4, 192, 'SiteA'),
|
||||
('SiteA/Conv-001/Speed', DATEADD(MINUTE, -110, @now), 0.0, 0, 'SiteA'), -- Bad quality (stopped)
|
||||
('SiteA/Conv-001/Speed', DATEADD(MINUTE, -100, @now), 2.3, 192, 'SiteA'),
|
||||
('SiteB/Mixer-001/RPM', DATEADD(MINUTE, -120, @now), 450.0, 192, 'SiteB'),
|
||||
('SiteB/Mixer-001/RPM', DATEADD(MINUTE, -110, @now), 452.0, 192, 'SiteB'),
|
||||
('SiteB/Mixer-001/RPM', DATEADD(MINUTE, -100, @now), 448.0, 192, 'SiteB'),
|
||||
('SiteB/Mixer-001/Temperature',DATEADD(MINUTE, -120, @now), 82.1, 192, 'SiteB'),
|
||||
('SiteB/Mixer-001/Temperature',DATEADD(MINUTE, -110, @now), 83.0, 192, 'SiteB');
|
||||
|
||||
-- Production counts (last 3 days, 2 shifts per day)
|
||||
DECLARE @today DATE = CAST(SYSUTCDATETIME() AS DATE);
|
||||
|
||||
INSERT INTO dbo.ProductionCounts (SiteId, LineName, ShiftDate, ShiftNumber, GoodCount, RejectCount, Efficiency) VALUES
|
||||
('SiteA', 'Line-1', DATEADD(DAY, -2, @today), 1, 4100, 82, 92.5),
|
||||
('SiteA', 'Line-1', DATEADD(DAY, -2, @today), 2, 3900, 95, 91.2),
|
||||
('SiteA', 'Line-2', DATEADD(DAY, -2, @today), 1, 3050, 120, 88.1),
|
||||
('SiteA', 'Line-2', DATEADD(DAY, -2, @today), 2, 2900, 105, 87.5),
|
||||
('SiteA', 'Line-1', DATEADD(DAY, -1, @today), 1, 4200, 75, 93.1),
|
||||
('SiteA', 'Line-1', DATEADD(DAY, -1, @today), 2, 4050, 88, 92.0),
|
||||
('SiteA', 'Line-2', DATEADD(DAY, -1, @today), 1, 3100, 98, 89.2),
|
||||
('SiteA', 'Line-2', DATEADD(DAY, -1, @today), 2, 3000, 110, 88.0),
|
||||
('SiteA', 'Line-1', @today, 1, 2100, 40, 93.5),
|
||||
('SiteA', 'Line-2', @today, 1, 1550, 65, 88.8),
|
||||
('SiteB', 'Mixing', DATEADD(DAY, -1, @today), 1, 850, 12, 95.0),
|
||||
('SiteB', 'Mixing', DATEADD(DAY, -1, @today), 2, 820, 15, 94.2),
|
||||
('SiteB', 'Packing', DATEADD(DAY, -1, @today), 1, 1600, 45, 90.5),
|
||||
('SiteB', 'Packing', DATEADD(DAY, -1, @today), 2, 1550, 50, 89.8);
|
||||
|
||||
-- Equipment events
|
||||
INSERT INTO dbo.EquipmentEvents (SiteId, EquipmentId, EventType, PreviousState, NewState, Description, Timestamp) VALUES
|
||||
('SiteA', 'PUMP-001', 'StateChange', 'Idle', 'Running', 'Shift start', DATEADD(HOUR, -8, @now)),
|
||||
('SiteA', 'PUMP-001', 'Fault', 'Running', 'Faulted', 'Overcurrent detected', DATEADD(HOUR, -5, @now)),
|
||||
('SiteA', 'PUMP-001', 'StateChange', 'Faulted', 'Running', 'Manual reset by operator', DATEADD(HOUR, -4, @now)),
|
||||
('SiteA', 'TANK-001', 'Alarm', NULL, 'HighLevel','Tank level exceeded 90%', DATEADD(HOUR, -3, @now)),
|
||||
('SiteA', 'TANK-001', 'Alarm', NULL, 'Normal', 'Tank level returned to normal',DATEADD(HOUR, -2, @now)),
|
||||
('SiteA', 'CONV-001', 'Maintenance', 'Running', 'Maintenance', 'Scheduled belt inspection', DATEADD(HOUR, -6, @now)),
|
||||
('SiteA', 'CONV-001', 'StateChange', 'Maintenance', 'Running', 'Maintenance complete', DATEADD(HOUR, -5, @now)),
|
||||
('SiteB', 'MIXER-001','StateChange', 'Idle', 'Running', 'Batch R-100 started', DATEADD(HOUR, -7, @now)),
|
||||
('SiteB', 'MIXER-001','StateChange', 'Running', 'Idle', 'Batch R-100 complete', DATEADD(HOUR, -3, @now));
|
||||
|
||||
-- Batch records
|
||||
INSERT INTO dbo.BatchRecords (BatchId, SiteId, RecipeId, Status, StartTime, EndTime, TotalQuantity, Operator, Notes) VALUES
|
||||
('BATCH-20260314-001', 'SiteA', 'R-100', 'Complete', DATEADD(HOUR, -26, @now), DATEADD(HOUR, -22, @now), 450.00, 'jsmith', 'Normal run'),
|
||||
('BATCH-20260314-002', 'SiteA', 'R-200', 'Complete', DATEADD(HOUR, -20, @now), DATEADD(HOUR, -16, @now), 380.50, 'jsmith', 'Slight yield loss on Material-B'),
|
||||
('BATCH-20260315-001', 'SiteB', 'R-100', 'Complete', DATEADD(HOUR, -10, @now), DATEADD(HOUR, -6, @now), 445.00, 'mdoe', NULL),
|
||||
('BATCH-20260315-002', 'SiteA', 'R-150', 'Aborted', DATEADD(HOUR, -8, @now), DATEADD(HOUR, -7, @now), NULL, 'jsmith', 'Material-A out of spec, aborted early'),
|
||||
('BATCH-20260316-001', 'SiteA', 'R-100', 'InProgress', DATEADD(HOUR, -2, @now), NULL, NULL, 'bwilson', 'Current batch');
|
||||
|
||||
-- Alarm history
|
||||
INSERT INTO dbo.AlarmHistory (SiteId, AlarmName, Severity, State, ActivatedAt, AcknowledgedAt, ClearedAt, AcknowledgedBy, Message) VALUES
|
||||
('SiteA', 'Tank-001 High Level', 3, 'Cleared', DATEADD(HOUR, -3, @now), DATEADD(HOUR, -3, @now), DATEADD(HOUR, -2, @now), 'jsmith', 'Level exceeded 90% setpoint'),
|
||||
('SiteA', 'Pump-001 Overcurrent', 4, 'Cleared', DATEADD(HOUR, -5, @now), DATEADD(HOUR, -5, @now), DATEADD(HOUR, -4, @now), 'jsmith', 'Current draw 15.2A (limit 12A)'),
|
||||
('SiteA', 'Conv-001 Belt Slip', 2, 'Active', DATEADD(HOUR, -1, @now), NULL, NULL, NULL, 'Belt speed deviation >5%'),
|
||||
('SiteA', 'Tank-001 Temperature High', 3, 'Acknowledged', DATEADD(MINUTE, -30, @now), DATEADD(MINUTE, -25, @now), NULL, 'bwilson', 'Temperature 68.2C (limit 65C)'),
|
||||
('SiteB', 'Mixer-001 Vibration', 2, 'Active', DATEADD(HOUR, -2, @now), NULL, NULL, NULL, 'Vibration level elevated');
|
||||
GO
|
||||
|
||||
PRINT 'ScadaLinkMachineData seed complete.';
|
||||
PRINT 'Tables: TagHistory, ProductionCounts, EquipmentEvents, BatchRecords, AlarmHistory';
|
||||
PRINT 'Stored Procedures: usp_GetTagHistory, usp_GetProductionSummary, usp_InsertBatchRecord, usp_CompleteBatch, usp_GetEquipmentEvents, usp_GetActiveAlarms';
|
||||
GO
|
||||
@@ -14,7 +14,7 @@ GO
|
||||
|
||||
-- Create application login
|
||||
IF NOT EXISTS (SELECT name FROM sys.server_principals WHERE name = 'scadalink_app')
|
||||
CREATE LOGIN scadalink_app WITH PASSWORD = 'ScadaLink_Dev1!', DEFAULT_DATABASE = ScadaLinkConfig;
|
||||
CREATE LOGIN scadalink_app WITH PASSWORD = 'ScadaLink_Dev1#', DEFAULT_DATABASE = ScadaLinkConfig;
|
||||
GO
|
||||
|
||||
-- Grant db_owner on ScadaLinkConfig
|
||||
|
||||
@@ -10,7 +10,7 @@ import pymssql
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 1433
|
||||
DEFAULT_USER = "sa"
|
||||
DEFAULT_PASSWORD = "ScadaLink_Dev1!"
|
||||
DEFAULT_PASSWORD = "ScadaLink_Dev1#"
|
||||
EXPECTED_DBS = ["ScadaLinkConfig", "ScadaLinkMachineData"]
|
||||
|
||||
|
||||
|
||||
@@ -19,12 +19,16 @@ cd infra
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
After the first startup, run the SQL setup script to create databases and the application user:
|
||||
After the first startup, run the SQL setup and seed scripts:
|
||||
|
||||
```bash
|
||||
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1!' -C \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
|
||||
-i /docker-entrypoint-initdb.d/setup.sql
|
||||
|
||||
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
|
||||
-i /docker-entrypoint-initdb.d/machinedata_seed.sql
|
||||
```
|
||||
|
||||
## Per-Service Documentation
|
||||
@@ -44,8 +48,8 @@ For use in `appsettings.Development.json`:
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"ScadaLinkConfig": "Server=localhost,1433;Database=ScadaLinkConfig;User Id=scadalink_app;Password=ScadaLink_Dev1!;TrustServerCertificate=true",
|
||||
"ScadaLinkMachineData": "Server=localhost,1433;Database=ScadaLinkMachineData;User Id=scadalink_app;Password=ScadaLink_Dev1!;TrustServerCertificate=true"
|
||||
"ScadaLinkConfig": "Server=localhost,1433;Database=ScadaLinkConfig;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true",
|
||||
"ScadaLinkMachineData": "Server=localhost,1433;Database=ScadaLinkMachineData;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true"
|
||||
},
|
||||
"Ldap": {
|
||||
"Server": "localhost",
|
||||
@@ -99,6 +103,7 @@ infra/
|
||||
teardown.sh # Teardown script (volumes, images, venv)
|
||||
glauth/config.toml # LDAP users and groups
|
||||
mssql/setup.sql # Database and user creation
|
||||
mssql/machinedata_seed.sql # Machine Data tables, stored procedures, sample data
|
||||
opcua/nodes.json # Custom OPC UA tag definitions
|
||||
restapi/app.py # Flask REST API server
|
||||
restapi/Dockerfile # REST API container build
|
||||
|
||||
@@ -14,8 +14,8 @@ The test database uses Microsoft SQL Server 2022 Developer Edition running in Do
|
||||
|
||||
| Account | Username | Password | Purpose |
|
||||
|---------|----------|----------|---------|
|
||||
| SA | `sa` | `ScadaLink_Dev1!` | Server admin (setup only) |
|
||||
| App | `scadalink_app` | `ScadaLink_Dev1!` | Application login (db_owner on both databases) |
|
||||
| SA | `sa` | `ScadaLink_Dev1#` | Server admin (setup only) |
|
||||
| App | `scadalink_app` | `ScadaLink_Dev1#` | Application login (db_owner on both databases) |
|
||||
|
||||
## Databases
|
||||
|
||||
@@ -24,7 +24,7 @@ The test database uses Microsoft SQL Server 2022 Developer Edition running in Do
|
||||
| `ScadaLinkConfig` | Configuration Database component — templates, deployments, users, audit log |
|
||||
| `ScadaLinkMachineData` | Machine/operational data storage |
|
||||
|
||||
Both databases are created empty by `infra/mssql/setup.sql`. EF Core migrations populate the schema.
|
||||
Both databases are created by `infra/mssql/setup.sql`. EF Core migrations populate the `ScadaLinkConfig` schema. The `ScadaLinkMachineData` database is seeded with sample tables and stored procedures by `infra/mssql/machinedata_seed.sql`.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
@@ -42,22 +42,30 @@ After the first `docker compose up -d`, run the setup script:
|
||||
|
||||
```bash
|
||||
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1!' -C \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
|
||||
-i /docker-entrypoint-initdb.d/setup.sql
|
||||
```
|
||||
|
||||
This creates the databases and the `scadalink_app` login. You only need to run this once (or again after deleting the volume).
|
||||
This creates the databases and the `scadalink_app` login. Then seed the Machine Data database with sample tables, stored procedures, and data:
|
||||
|
||||
```bash
|
||||
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
|
||||
-i /docker-entrypoint-initdb.d/machinedata_seed.sql
|
||||
```
|
||||
|
||||
You only need to run these once (or again after deleting the volume).
|
||||
|
||||
## Connection Strings
|
||||
|
||||
For `appsettings.Development.json`:
|
||||
|
||||
```
|
||||
Server=localhost,1433;Database=ScadaLinkConfig;User Id=scadalink_app;Password=ScadaLink_Dev1!;TrustServerCertificate=true
|
||||
Server=localhost,1433;Database=ScadaLinkConfig;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true
|
||||
```
|
||||
|
||||
```
|
||||
Server=localhost,1433;Database=ScadaLinkMachineData;User Id=scadalink_app;Password=ScadaLink_Dev1!;TrustServerCertificate=true
|
||||
Server=localhost,1433;Database=ScadaLinkMachineData;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true
|
||||
```
|
||||
|
||||
## Verification
|
||||
@@ -72,7 +80,7 @@ docker ps --filter name=scadalink-mssql
|
||||
|
||||
```bash
|
||||
docker exec -it scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1!' -C \
|
||||
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
|
||||
-Q "SELECT name FROM sys.databases"
|
||||
```
|
||||
|
||||
@@ -80,7 +88,7 @@ docker exec -it scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
|
||||
```bash
|
||||
docker exec -it scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U scadalink_app -P 'ScadaLink_Dev1!' -C \
|
||||
-S localhost -U scadalink_app -P 'ScadaLink_Dev1#' -C \
|
||||
-d ScadaLinkConfig \
|
||||
-Q "SELECT DB_NAME()"
|
||||
```
|
||||
@@ -110,7 +118,32 @@ python infra/tools/mssql_tool.py tables --database ScadaLinkConfig
|
||||
python infra/tools/mssql_tool.py query --database ScadaLinkConfig --sql "SELECT name FROM sys.tables"
|
||||
```
|
||||
|
||||
Use `--host`, `--port`, `--user`, `--password` to override defaults (localhost:1433, sa, ScadaLink_Dev1!). Run with `--help` for full usage.
|
||||
Use `--host`, `--port`, `--user`, `--password` to override defaults (localhost:1433, sa, ScadaLink_Dev1#). Run with `--help` for full usage.
|
||||
|
||||
## Machine Data Tables & Stored Procedures
|
||||
|
||||
The `machinedata_seed.sql` script creates the following in `ScadaLinkMachineData`:
|
||||
|
||||
**Tables**:
|
||||
|
||||
| Table | Description | Sample Data |
|
||||
|-------|-------------|-------------|
|
||||
| `TagHistory` | Time-series tag values from OPC UA / custom protocols | Pressure, flow, level, temperature, speed readings for SiteA/SiteB |
|
||||
| `ProductionCounts` | Shift/line production totals (good, reject, efficiency) | 3 days of 2-shift data across 2 sites |
|
||||
| `EquipmentEvents` | State changes, faults, maintenance, alarm events | Pump faults, belt inspections, batch starts |
|
||||
| `BatchRecords` | Production batch tracking (start, complete, abort) | 5 batches including one in-progress |
|
||||
| `AlarmHistory` | Historical alarm activations, acks, and clears | Active, acknowledged, and cleared alarms |
|
||||
|
||||
**Stored Procedures**:
|
||||
|
||||
| Procedure | Description |
|
||||
|-----------|-------------|
|
||||
| `usp_GetTagHistory` | Get tag values for a tag path within a date range |
|
||||
| `usp_GetProductionSummary` | Aggregate production by line over a date range |
|
||||
| `usp_InsertBatchRecord` | Insert a new batch (for `CachedWrite` testing) |
|
||||
| `usp_CompleteBatch` | Complete or abort a batch |
|
||||
| `usp_GetEquipmentEvents` | Get recent equipment events with optional filters |
|
||||
| `usp_GetActiveAlarms` | Get active/acknowledged alarms by severity |
|
||||
|
||||
## Relevance to ScadaLink Components
|
||||
|
||||
@@ -118,6 +151,9 @@ Use `--host`, `--port`, `--user`, `--password` to override defaults (localhost:1
|
||||
- **Deployment Manager** — reads/writes deployment records in `ScadaLinkConfig`.
|
||||
- **Template Engine** — reads/writes template definitions in `ScadaLinkConfig`.
|
||||
- **Security & Auth** — user/role data stored in `ScadaLinkConfig`.
|
||||
- **External System Gateway** — scripts use `Database.Connection("machineDataConnection")` to query `ScadaLinkMachineData` tables and stored procedures.
|
||||
- **Site Runtime** — scripts call stored procedures via `Database.Connection()` and `Database.CachedWrite()` for batch recording and data queries.
|
||||
- **Inbound API** — methods can query machine data via named database connections.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user