create-autoid-app 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+
7
+ const BANNER = `
8
+ \x1b[36m╔═════════════════════════════════════════════════════════════════════════╗
9
+ ║ ⚡ BEETECH OMNIAUTO-ID™ DEVELOPER APPLICATION GENERATOR ⚡ ║
10
+ ║ Zero-Hardware to Real-Time RFID Streaming in 60s ║
11
+ ╚═════════════════════════════════════════════════════════════════════════╝\x1b[0m
12
+ `;
13
+
14
+ async function promptQuestion(query, defaultValue) {
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout
18
+ });
19
+ return new Promise(resolve => {
20
+ rl.question(`\x1b[33m? \x1b[1m${query}\x1b[0m \x1b[90m(${defaultValue}):\x1b[0m `, answer => {
21
+ rl.close();
22
+ resolve(answer.trim() || defaultValue);
23
+ });
24
+ });
25
+ }
26
+
27
+ async function main() {
28
+ console.log(BANNER);
29
+
30
+ const args = process.argv.slice(2);
31
+ let projectName = args[0];
32
+
33
+ if (!projectName) {
34
+ projectName = await promptQuestion('Enter your project name', 'my-rfid-app');
35
+ }
36
+
37
+ console.log(`\n\x1b[32m✔\x1b[0m Target Directory: \x1b[36m${projectName}\x1b[0m`);
38
+
39
+ console.log('\n\x1b[1mAvailable Application Templates:\x1b[0m');
40
+ console.log(' \x1b[36m[1]\x1b[0m \x1b[1mweb-radar-dashboard\x1b[0m - Modern Realtime Web Radar & Tag Inspector (Vite + Live UI)');
41
+ console.log(' \x1b[36m[2]\x1b[0m \x1b[1mnode-stream-worker\x1b[0m - Node.js High-Throughput Tag Stream Consumer');
42
+ console.log(' \x1b[36m[3]\x1b[0m \x1b[1mdotnet-gate-service\x1b[0m - C# .NET 9 Warehouse Inbound/Outbound RFID Gate Service');
43
+
44
+ let choice = args[1] || await promptQuestion('Select template [1-3]', '1');
45
+ let templateKey = 'web-radar-dashboard';
46
+ if (choice === '2' || choice === 'node-stream-worker') templateKey = 'node-stream-worker';
47
+ if (choice === '3' || choice === 'dotnet-gate-service') templateKey = 'dotnet-gate-service';
48
+
49
+ const targetDir = path.resolve(process.cwd(), projectName);
50
+ if (fs.existsSync(targetDir)) {
51
+ console.error(`\x1b[31m✖ Error: Directory '${projectName}' already exists.\x1b[0m`);
52
+ process.exit(1);
53
+ }
54
+
55
+ console.log(`\n\x1b[34m[1/3]\x1b[0m Creating project directory: \x1b[36m${targetDir}\x1b[0m`);
56
+ fs.mkdirSync(targetDir, { recursive: true });
57
+
58
+ console.log(`\x1b[34m[2/3]\x1b[0m Generating template \x1b[35m${templateKey}\x1b[0m...`);
59
+ const templateSourceDir = path.join(__dirname, '..', 'templates', templateKey);
60
+
61
+ if (fs.existsSync(templateSourceDir)) {
62
+ copyDirectoryRecursive(templateSourceDir, targetDir, { projectName });
63
+ } else {
64
+ console.error(`\x1b[31m✖ Template directory not found: ${templateSourceDir}\x1b[0m`);
65
+ process.exit(1);
66
+ }
67
+
68
+ console.log(`\x1b[34m[3/3]\x1b[0m Ready!`);
69
+ console.log(`
70
+ \x1b[32m✔ Successfully created '${projectName}'!\x1b[0m
71
+
72
+ To launch your application in 30 seconds:
73
+ \x1b[33mcd ${projectName}\x1b[0m
74
+ ${templateKey.startsWith('dotnet') ? '\x1b[33mdotnet run\x1b[0m' : '\x1b[33mnpm install\n npm run dev\x1b[0m'}
75
+
76
+ \x1b[90mTip: If no physical RFID reader is connected, the app automatically switches to\x1b[0m
77
+ \x1b[90mZero-Hardware Simulator Mode with realistic GS1 SGTIN tags & radar animation!\x1b[0m
78
+ `);
79
+ }
80
+
81
+ function copyDirectoryRecursive(src, dest, replacements) {
82
+ const entries = fs.readdirSync(src, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ const srcPath = path.join(src, entry.name);
85
+ let destName = entry.name;
86
+ if (destName.includes('__NAME__')) {
87
+ destName = destName.replace('__NAME__', replacements.projectName);
88
+ }
89
+ const destPath = path.join(dest, destName);
90
+
91
+ if (entry.isDirectory()) {
92
+ fs.mkdirSync(destPath, { recursive: true });
93
+ copyDirectoryRecursive(srcPath, destPath, replacements);
94
+ } else {
95
+ let content = fs.readFileSync(srcPath, 'utf8');
96
+ for (const [key, val] of Object.entries(replacements)) {
97
+ content = content.replace(new RegExp(`{{${key}}}`, 'g'), val);
98
+ }
99
+ fs.writeFileSync(destPath, content, 'utf8');
100
+ }
101
+ }
102
+ }
103
+
104
+ main().catch(err => {
105
+ console.error('\x1b[31mUnexpected CLI error:\x1b[0m', err);
106
+ process.exit(1);
107
+ });
package/package.json CHANGED
@@ -1,6 +1,27 @@
1
1
  {
2
2
  "name": "create-autoid-app",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Scaffold real-time RFID, PLC, and Barcode IoT applications with Beetech OmniAuto-ID and Adv.SmartSdk in under 60 seconds",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "create-autoid-app": "./bin/index.js",
8
+ "create-omniautoid-app": "./bin/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/index.js",
12
+ "test": "node test/cli.test.js"
13
+ },
14
+ "keywords": [
15
+ "rfid",
16
+ "autoid",
17
+ "beetech",
18
+ "smartsdk",
19
+ "edge-gateway",
20
+ "iot",
21
+ "scaffolding",
22
+ "cli",
23
+ "npx"
24
+ ],
25
+ "author": "Beetech Auto-ID Engineering Team",
26
+ "license": "MIT"
27
+ }
@@ -0,0 +1,65 @@
1
+ using System;
2
+ using System.Collections.Concurrent;
3
+ using System.Threading;
4
+ using System.Threading.Tasks;
5
+ using Beetech.OmniAutoId.Client;
6
+ using Beetech.OmniAutoId.Client.Models;
7
+ using Microsoft.Extensions.Hosting;
8
+ using Microsoft.Extensions.Logging;
9
+
10
+ namespace {{projectName}};
11
+
12
+ /// <summary>
13
+ /// Background worker processing high-speed RFID Portal Gate events, deduplication, and PLC trigger actions.
14
+ /// </summary>
15
+ public sealed class GateWorker : BackgroundService
16
+ {
17
+ private readonly IOmniAutoIdClient _client;
18
+ private readonly ILogger<GateWorker> _logger;
19
+ private readonly ConcurrentDictionary<string, (DateTime FirstSeen, int Count)> _tagCache = new();
20
+ private long _totalTagReads;
21
+
22
+ public GateWorker(IOmniAutoIdClient client, ILogger<GateWorker> logger)
23
+ {
24
+ _client = client;
25
+ _logger = logger;
26
+
27
+ _client.TagRead += OnTagDetected;
28
+ _client.ConnectionStatusChanged += (s, e) =>
29
+ {
30
+ _logger.LogInformation(">>> OmniAuto-ID Gateway Status: {Status} (Simulated: {IsSim})", e.Message, e.IsSimulated);
31
+ };
32
+ }
33
+
34
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
35
+ {
36
+ _logger.LogInformation("Warehouse Portal Gate Worker is starting...");
37
+
38
+ await _client.ConnectAsync(stoppingToken);
39
+
40
+ while (!stoppingToken.IsCancellationRequested)
41
+ {
42
+ await Task.Delay(5000, stoppingToken);
43
+ _logger.LogInformation("[GATE STATUS] Total Tag Scans: {Total} | Unique EPCs: {Unique}",
44
+ Interlocked.Read(ref _totalTagReads), _tagCache.Count);
45
+ }
46
+
47
+ await _client.DisconnectAsync(stoppingToken);
48
+ }
49
+
50
+ private void OnTagDetected(object? sender, TagReadEvent tag)
51
+ {
52
+ Interlocked.Increment(ref _totalTagReads);
53
+
54
+ var isNew = !_tagCache.ContainsKey(tag.Epc);
55
+ _tagCache.AddOrUpdate(tag.Epc,
56
+ (DateTime.UtcNow, 1),
57
+ (_, old) => (old.FirstSeen, old.Count + 1));
58
+
59
+ if (isNew)
60
+ {
61
+ _logger.LogInformation("[PALLET ARRIVAL] EPC: {Epc} | Ant: {Ant} | RSSI: {Rssi} dBm | Direction: {Dir}",
62
+ tag.Epc, tag.Antenna, tag.Rssi, tag.Direction);
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,20 @@
1
+ using Beetech.OmniAutoId.Client;
2
+ using Microsoft.Extensions.DependencyInjection;
3
+ using Microsoft.Extensions.Hosting;
4
+ using {{projectName}};
5
+
6
+ var builder = Host.CreateApplicationBuilder(args);
7
+
8
+ // Register OmniAuto-ID Client with configuration
9
+ builder.Services.AddOmniAutoIdClient(options =>
10
+ {
11
+ options.GatewayUrl = builder.Configuration["OmniAutoId:GatewayUrl"] ?? "http://localhost:19090";
12
+ options.ApiKey = builder.Configuration["OmniAutoId:ApiKey"];
13
+ options.UseSimulationFallback = true; // Auto-switches to high-speed virtual simulator if hardware is offline
14
+ });
15
+
16
+ // Register Warehouse Gate Background Worker
17
+ builder.Services.AddHostedService<GateWorker>();
18
+
19
+ var host = builder.Build();
20
+ host.Run();
@@ -0,0 +1,11 @@
1
+ # {{projectName}}
2
+
3
+ High-throughput Warehouse Portal Gate Service built with **Beetech OmniAuto-ID™ Client**.
4
+
5
+ ## 🚀 Running the Service
6
+
7
+ ```bash
8
+ dotnet run
9
+ ```
10
+
11
+ If the physical edge gateway is offline, the service automatically engages the built-in Zero-Hardware Simulator, generating realistic EPC Gen2 pallet scans so you can test your warehouse logic instantly.
@@ -0,0 +1,15 @@
1
+ <Project Sdk="Microsoft.NET.Sdk.Worker">
2
+
3
+ <PropertyGroup>
4
+ <TargetFramework>net8.0</TargetFramework>
5
+ <Nullable>enable</Nullable>
6
+ <ImplicitUsings>enable</ImplicitUsings>
7
+ <UserSecretsId>dotnet-{{projectName}}-secrets</UserSecretsId>
8
+ </PropertyGroup>
9
+
10
+ <ItemGroup>
11
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
12
+ <PackageReference Include="Beetech.OmniAutoId.Client" Version="1.0.0" />
13
+ </ItemGroup>
14
+
15
+ </Project>
@@ -0,0 +1,13 @@
1
+ {
2
+ "Logging": {
3
+ "LogLevel": {
4
+ "Default": "Information",
5
+ "Microsoft.Hosting.Lifetime": "Information"
6
+ }
7
+ },
8
+ "OmniAutoId": {
9
+ "GatewayUrl": "http://localhost:19090",
10
+ "ApiKey": "",
11
+ "UseSimulationFallback": true
12
+ }
13
+ }
@@ -0,0 +1,10 @@
1
+ # {{projectName}}
2
+
3
+ High-throughput Node.js stream processing worker for **Beetech OmniAuto-ID™ Edge Gateway**.
4
+
5
+ ## 🚀 Running the Worker
6
+
7
+ ```bash
8
+ npm install
9
+ npm start
10
+ ```
@@ -0,0 +1,60 @@
1
+ const WebSocket = require('ws');
2
+
3
+ const GATEWAY_WS_URL = process.env.AUTOID_GATEWAY_WS || 'ws://localhost:19090/stream';
4
+ console.log('===============================================================');
5
+ console.log(' ⚡ OmniAuto-ID™ High-Throughput Stream Worker Daemon ⚡ ');
6
+ console.log(` Target Gateway: ${GATEWAY_WS_URL}`);
7
+ console.log('===============================================================');
8
+
9
+ let totalProcessed = 0;
10
+ let tagsInLastSecond = 0;
11
+ const inventoryCache = new Map();
12
+
13
+ function startWorker() {
14
+ console.log(`Connecting to ${GATEWAY_WS_URL}...`);
15
+ const ws = new WebSocket(GATEWAY_WS_URL);
16
+
17
+ ws.on('open', () => {
18
+ console.log('✔ Connected to OmniAuto-ID Live Stream.');
19
+ });
20
+
21
+ ws.on('message', (data) => {
22
+ try {
23
+ const tag = JSON.parse(data.toString());
24
+ if (!tag.epc) return;
25
+
26
+ totalProcessed++;
27
+ tagsInLastSecond++;
28
+
29
+ const cache = inventoryCache.get(tag.epc) || { count: 0, firstSeen: Date.now() };
30
+ cache.count++;
31
+ cache.lastRssi = tag.rssi;
32
+ inventoryCache.set(tag.epc, cache);
33
+
34
+ if (cache.count === 1) {
35
+ console.log(`[NEW TAG DETECTED] EPC=${tag.epc} | Ant=${tag.antenna} | RSSI=${tag.rssi} dBm | TotalUnique=${inventoryCache.size}`);
36
+ }
37
+ } catch (err) {
38
+ console.error('Error parsing stream event:', err.message);
39
+ }
40
+ });
41
+
42
+ ws.on('error', (err) => {
43
+ console.warn(`Connection error (${err.message}). Retrying in 3s...`);
44
+ });
45
+
46
+ ws.on('close', () => {
47
+ console.warn('Stream closed. Reconnecting in 3s...');
48
+ setTimeout(startWorker, 3000);
49
+ });
50
+ }
51
+
52
+ // Stats summary interval
53
+ setInterval(() => {
54
+ if (totalProcessed > 0) {
55
+ console.log(`[METRICS] Rate: ${tagsInLastSecond} tags/sec | Unique EPCs: ${inventoryCache.size} | Total Reads: ${totalProcessed}`);
56
+ tagsInLastSecond = 0;
57
+ }
58
+ }, 2000);
59
+
60
+ startWorker();
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "1.0.0",
4
+ "description": "High-throughput RFID tag stream processing worker daemon",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js"
8
+ },
9
+ "dependencies": {
10
+ "ws": "^8.16.0"
11
+ }
12
+ }
@@ -0,0 +1,20 @@
1
+ # {{projectName}}
2
+
3
+ Real-time RFID Radar & Tag Inventory Dashboard built with **Beetech OmniAuto-ID™ Platform**.
4
+
5
+ ## 🚀 Getting Started
6
+
7
+ ```bash
8
+ # 1. Install dependencies
9
+ npm install
10
+
11
+ # 2. Run local dev server
12
+ npm run dev
13
+ ```
14
+
15
+ Open your browser at the local URL (usually `http://localhost:5173`).
16
+
17
+ ## ⚡ Features
18
+ * **Zero-Hardware Simulation:** Automatically starts in mock mode if the OmniAuto-ID Gateway (`ws://localhost:19090/stream`) is not detected.
19
+ * **Canvas Radar Sweep:** Real-time spatial visualization of tag reads across antennas.
20
+ * **Live In-Memory Deduplication:** Aggregates read counts, RSSI signals, and inbound/outbound direction tracking.
@@ -0,0 +1,92 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{projectName}} - OmniAuto-ID Live RFID Radar</title>
7
+ <link rel="stylesheet" href="./src/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="app-container">
11
+ <header class="navbar">
12
+ <div class="brand">
13
+ <div class="pulse-indicator live" id="statusPulse"></div>
14
+ <div class="title-group">
15
+ <h1>OmniAuto-ID™ Live Radar</h1>
16
+ <span class="subtitle">{{projectName}} • Port 19090 Stream</span>
17
+ </div>
18
+ </div>
19
+ <div class="header-controls">
20
+ <button id="toggleSimBtn" class="btn btn-secondary">Simulation: <span id="simStateText">AUTO</span></button>
21
+ <button id="clearBtn" class="btn btn-secondary">Clear Table</button>
22
+ <span class="badge" id="connBadge">Connecting...</span>
23
+ </div>
24
+ </header>
25
+
26
+ <main class="dashboard-grid">
27
+ <!-- Top Metric Cards -->
28
+ <section class="metric-card">
29
+ <span class="card-label">TOTAL TAGS PROCESSED</span>
30
+ <div class="card-value" id="totalTagsCount">0</div>
31
+ <span class="card-hint">Deduplicated in memory</span>
32
+ </section>
33
+
34
+ <section class="metric-card">
35
+ <span class="card-label">CURRENT SCAN VELOCITY</span>
36
+ <div class="card-value text-accent" id="tagRate">0 <span class="unit">tags/s</span></div>
37
+ <span class="card-hint">Sub-millisecond processing</span>
38
+ </section>
39
+
40
+ <section class="metric-card">
41
+ <span class="card-label">ACTIVE ANTENNAS</span>
42
+ <div class="card-value" id="activeAntennas">4</div>
43
+ <span class="card-hint">Port 1, 2 (In) • 3, 4 (Out)</span>
44
+ </section>
45
+
46
+ <section class="metric-card">
47
+ <span class="card-label">GATEWAY LATENCY</span>
48
+ <div class="card-value text-green" id="latencyMs">1.2 <span class="unit">ms</span></div>
49
+ <span class="card-hint">Native AOT WebSocket engine</span>
50
+ </section>
51
+
52
+ <!-- Radar Visualizer -->
53
+ <section class="panel radar-panel">
54
+ <div class="panel-header">
55
+ <h2>Antenna Beam Coverage &amp; Tag Radar</h2>
56
+ <span class="hint">Spatial RSSI Distribution</span>
57
+ </div>
58
+ <div class="radar-container">
59
+ <canvas id="radarCanvas" width="400" height="400"></canvas>
60
+ </div>
61
+ </section>
62
+
63
+ <!-- Live Scanned Stream Table -->
64
+ <section class="panel table-panel">
65
+ <div class="panel-header">
66
+ <h2>Live EPC Tag Inventory Feed</h2>
67
+ <span class="counter-badge" id="tableRowCount">0 detected</span>
68
+ </div>
69
+ <div class="table-scroll">
70
+ <table class="tag-table">
71
+ <thead>
72
+ <tr>
73
+ <th>TIME</th>
74
+ <th>EPC / GS1 IDENTIFIER</th>
75
+ <th>ANTENNA</th>
76
+ <th>RSSI</th>
77
+ <th>DIRECTION</th>
78
+ <th>READS</th>
79
+ </tr>
80
+ </thead>
81
+ <tbody id="tagTableBody">
82
+ <!-- Dynamically populated rows -->
83
+ </tbody>
84
+ </table>
85
+ </div>
86
+ </section>
87
+ </main>
88
+ </div>
89
+
90
+ <script type="module" src="./src/main.js"></script>
91
+ </body>
92
+ </html>
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@beetech-autoid/smartsdk-client": "^1.0.3"
13
+ },
14
+ "devDependencies": {
15
+ "vite": "^5.1.0"
16
+ }
17
+ }
@@ -0,0 +1,276 @@
1
+ // OmniAuto-ID Live RFID Radar Engine
2
+ const canvas = document.getElementById('radarCanvas');
3
+ const ctx = canvas.getContext('2d');
4
+ const tableBody = document.getElementById('tagTableBody');
5
+ const totalTagsEl = document.getElementById('totalTagsCount');
6
+ const tagRateEl = document.getElementById('tagRate');
7
+ const connBadge = document.getElementById('connBadge');
8
+ const tableRowCountEl = document.getElementById('tableRowCount');
9
+ const toggleSimBtn = document.getElementById('toggleSimBtn');
10
+ const simStateText = document.getElementById('simStateText');
11
+ const clearBtn = document.getElementById('clearBtn');
12
+
13
+ const tagsMap = new Map();
14
+ const radarBlips = [];
15
+ let totalScans = 0;
16
+ let scansLastSecond = 0;
17
+ let isSimulating = false;
18
+ let simulationTimer = null;
19
+ let ws = null;
20
+
21
+ // Synthetic Audio Bip Effect via Web Audio API
22
+ let audioCtx = null;
23
+ function playScanBeep(frequency = 1200) {
24
+ try {
25
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
26
+ const osc = audioCtx.createOscillator();
27
+ const gain = audioCtx.createGain();
28
+ osc.type = 'sine';
29
+ osc.frequency.setValueAtTime(frequency, audioCtx.currentTime);
30
+ gain.gain.setValueAtTime(0.04, audioCtx.currentTime);
31
+ gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.05);
32
+ osc.connect(gain);
33
+ gain.connect(audioCtx.destination);
34
+ osc.start();
35
+ osc.stop(audioCtx.currentTime + 0.06);
36
+ } catch (e) {
37
+ // Audio context may require user gesture on some browsers
38
+ }
39
+ }
40
+
41
+ // 1. Process and Render Incoming Tag Event
42
+ function handleTagEvent(tag) {
43
+ totalScans++;
44
+ scansLastSecond++;
45
+ totalTagsEl.textContent = totalScans.toLocaleString();
46
+
47
+ const isNew = !tagsMap.has(tag.epc);
48
+ const existing = tagsMap.get(tag.epc) || { count: 0, firstSeen: new Date() };
49
+ existing.count++;
50
+ existing.lastSeen = new Date();
51
+ existing.rssi = tag.rssi;
52
+ existing.antenna = tag.antenna || 1;
53
+ existing.direction = tag.direction || (tag.antenna <= 2 ? 'Inbound' : 'Outbound');
54
+ tagsMap.set(tag.epc, existing);
55
+
56
+ // Add radar blip
57
+ const angle = Math.random() * Math.PI * 2;
58
+ const distRatio = Math.min(1, Math.max(0.1, (Math.abs(tag.rssi) - 30) / 50));
59
+ const maxR = canvas.width / 2 - 20;
60
+ radarBlips.push({
61
+ x: canvas.width / 2 + Math.cos(angle) * (distRatio * maxR),
62
+ y: canvas.height / 2 + Math.sin(angle) * (distRatio * maxR),
63
+ alpha: 1.0,
64
+ color: existing.direction === 'Inbound' ? '#00ff88' : '#00f0ff'
65
+ });
66
+
67
+ playScanBeep(existing.direction === 'Inbound' ? 1400 : 980);
68
+ updateTableRow(tag.epc, existing, isNew);
69
+ }
70
+
71
+ function updateTableRow(epc, info, isNew) {
72
+ let row = document.getElementById(`row-${epc}`);
73
+ const timeStr = info.lastSeen.toLocaleTimeString();
74
+
75
+ if (!row) {
76
+ row = document.createElement('tr');
77
+ row.id = `row-${epc}`;
78
+ row.className = 'tag-row newly-added';
79
+ tableBody.insertBefore(row, tableBody.firstChild);
80
+ tableRowCountEl.textContent = `${tagsMap.size} unique EPCs`;
81
+ } else {
82
+ row.classList.remove('newly-added');
83
+ void row.offsetWidth; // Trigger reflow for CSS animation
84
+ row.classList.add('newly-added');
85
+ }
86
+
87
+ const dirClass = info.direction === 'Inbound' ? 'dir-in' : 'dir-out';
88
+ const rssiWidth = Math.min(100, Math.max(10, 100 - (Math.abs(info.rssi) - 30) * 2));
89
+
90
+ row.innerHTML = `
91
+ <td>${timeStr}</td>
92
+ <td><strong>${epc}</strong></td>
93
+ <td>Port ${info.antenna}</td>
94
+ <td>
95
+ <div class="rssi-bar-container">
96
+ <span>${info.rssi.toFixed(1)} dBm</span>
97
+ <div class="rssi-fill" style="width: ${rssiWidth}px;"></div>
98
+ </div>
99
+ </td>
100
+ <td><span class="dir-badge ${dirClass}">${info.direction}</span></td>
101
+ <td><strong>${info.count}</strong></td>
102
+ `;
103
+ }
104
+
105
+ // 2. Radar Canvas Visualizer Loop
106
+ let sweepAngle = 0;
107
+ function drawRadar() {
108
+ const cx = canvas.width / 2;
109
+ const cy = canvas.height / 2;
110
+ const radius = cx - 15;
111
+
112
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
113
+
114
+ // Concentric radar circles
115
+ ctx.strokeStyle = '#1e293b';
116
+ ctx.lineWidth = 1;
117
+ for (let r = radius / 4; r <= radius; r += radius / 4) {
118
+ ctx.beginPath();
119
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
120
+ ctx.stroke();
121
+ }
122
+
123
+ // Crosshairs
124
+ ctx.beginPath();
125
+ ctx.moveTo(cx - radius, cy);
126
+ ctx.lineTo(cx + radius, cy);
127
+ ctx.moveTo(cx, cy - radius);
128
+ ctx.lineTo(cx, cy + radius);
129
+ ctx.stroke();
130
+
131
+ // Rotating sweep beam
132
+ ctx.save();
133
+ ctx.translate(cx, cy);
134
+ ctx.rotate(sweepAngle);
135
+ const gradient = ctx.createLinearGradient(0, 0, radius, 0);
136
+ gradient.addColorStop(0, 'rgba(0, 240, 255, 0.4)');
137
+ gradient.addColorStop(1, 'rgba(0, 240, 255, 0)');
138
+ ctx.fillStyle = gradient;
139
+ ctx.beginPath();
140
+ ctx.moveTo(0, 0);
141
+ ctx.arc(0, 0, radius, -0.3, 0.3);
142
+ ctx.closePath();
143
+ ctx.fill();
144
+ ctx.restore();
145
+
146
+ sweepAngle += 0.04;
147
+
148
+ // Render glowing blips
149
+ for (let i = radarBlips.length - 1; i >= 0; i--) {
150
+ const blip = radarBlips[i];
151
+ ctx.beginPath();
152
+ ctx.arc(blip.x, blip.y, 4, 0, Math.PI * 2);
153
+ ctx.fillStyle = blip.color;
154
+ ctx.globalAlpha = blip.alpha;
155
+ ctx.fill();
156
+ ctx.globalAlpha = 1.0;
157
+
158
+ blip.alpha -= 0.015;
159
+ if (blip.alpha <= 0) {
160
+ radarBlips.splice(i, 1);
161
+ }
162
+ }
163
+
164
+ requestAnimationFrame(drawRadar);
165
+ }
166
+
167
+ // 3. Rate Meter Interval
168
+ setInterval(() => {
169
+ tagRateEl.innerHTML = `${scansLastSecond} <span class="unit">tags/s</span>`;
170
+ scansLastSecond = 0;
171
+ }, 1000);
172
+
173
+ // 4. WebSocket Connection with Auto-Fallback
174
+ function connectGateway() {
175
+ const wsUrl = 'ws://localhost:19090/stream';
176
+ connBadge.textContent = 'Connecting...';
177
+ connBadge.className = 'badge';
178
+
179
+ try {
180
+ ws = new WebSocket(wsUrl);
181
+
182
+ ws.onopen = () => {
183
+ connBadge.textContent = 'LIVE GATEWAY';
184
+ connBadge.className = 'badge connected';
185
+ stopSimulation();
186
+ console.log('Connected to OmniAuto-ID Edge Gateway on port 19090');
187
+ };
188
+
189
+ ws.onmessage = (event) => {
190
+ try {
191
+ const data = JSON.parse(event.data);
192
+ if (data.epc) {
193
+ handleTagEvent(data);
194
+ }
195
+ } catch (err) {
196
+ console.error('Failed to parse tag event:', err);
197
+ }
198
+ };
199
+
200
+ ws.onerror = () => {
201
+ fallbackToSimulation();
202
+ };
203
+
204
+ ws.onclose = () => {
205
+ fallbackToSimulation();
206
+ };
207
+ } catch (e) {
208
+ fallbackToSimulation();
209
+ }
210
+ }
211
+
212
+ function fallbackToSimulation() {
213
+ if (isSimulating) return;
214
+ connBadge.textContent = 'SIMULATOR MODE';
215
+ connBadge.className = 'badge simulated';
216
+ startSimulation();
217
+ }
218
+
219
+ function startSimulation() {
220
+ isSimulating = true;
221
+ simStateText.textContent = 'ON';
222
+ const sampleEpcs = [
223
+ '3034257BF400B7800004FB11',
224
+ '3034257BF400B7800004FB12',
225
+ '3034257BF400B7800004FB13',
226
+ 'E2801160600002047E7A1001',
227
+ 'E2801160600002047E7A1002',
228
+ 'E2000019380902481840E1B4'
229
+ ];
230
+
231
+ simulationTimer = setInterval(() => {
232
+ const epc = sampleEpcs[Math.floor(Math.random() * sampleEpcs.length)];
233
+ const antenna = Math.floor(Math.random() * 4) + 1;
234
+ const rssi = -42 - Math.random() * 25;
235
+ handleTagEvent({
236
+ epc,
237
+ antenna,
238
+ rssi,
239
+ direction: antenna <= 2 ? 'Inbound' : 'Outbound'
240
+ });
241
+ }, 120);
242
+ }
243
+
244
+ function stopSimulation() {
245
+ isSimulating = false;
246
+ simStateText.textContent = 'OFF';
247
+ if (simulationTimer) {
248
+ clearInterval(simulationTimer);
249
+ simulationTimer = null;
250
+ }
251
+ }
252
+
253
+ // User Actions
254
+ toggleSimBtn.addEventListener('click', () => {
255
+ if (isSimulating) {
256
+ stopSimulation();
257
+ connBadge.textContent = 'PAUSED';
258
+ connBadge.className = 'badge';
259
+ } else {
260
+ startSimulation();
261
+ connBadge.textContent = 'SIMULATOR MODE';
262
+ connBadge.className = 'badge simulated';
263
+ }
264
+ });
265
+
266
+ clearBtn.addEventListener('click', () => {
267
+ tagsMap.clear();
268
+ tableBody.innerHTML = '';
269
+ tableRowCountEl.textContent = '0 detected';
270
+ totalScans = 0;
271
+ totalTagsEl.textContent = '0';
272
+ });
273
+
274
+ // Boot up
275
+ drawRadar();
276
+ connectGateway();
@@ -0,0 +1,304 @@
1
+ :root {
2
+ --bg-primary: #0a0d14;
3
+ --bg-surface: #121824;
4
+ --bg-surface-elevated: #1a2234;
5
+ --border: #232e45;
6
+ --accent-cyan: #00f0ff;
7
+ --accent-green: #00ff88;
8
+ --accent-amber: #ffaa00;
9
+ --text-main: #f0f4f8;
10
+ --text-muted: #8a99ad;
11
+ --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
12
+ }
13
+
14
+ * {
15
+ box-sizing: border-box;
16
+ margin: 0;
17
+ padding: 0;
18
+ }
19
+
20
+ body {
21
+ background-color: var(--bg-primary);
22
+ color: var(--text-main);
23
+ font-family: var(--font-family);
24
+ min-height: 100vh;
25
+ display: flex;
26
+ flex-direction: column;
27
+ }
28
+
29
+ .app-container {
30
+ max-width: 1400px;
31
+ margin: 0 auto;
32
+ padding: 1.5rem;
33
+ width: 100%;
34
+ }
35
+
36
+ .navbar {
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ padding: 1rem 1.5rem;
41
+ background: var(--bg-surface);
42
+ border: 1px solid var(--border);
43
+ border-radius: 12px;
44
+ margin-bottom: 1.5rem;
45
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
46
+ }
47
+
48
+ .brand {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 1rem;
52
+ }
53
+
54
+ .pulse-indicator {
55
+ width: 12px;
56
+ height: 12px;
57
+ border-radius: 50%;
58
+ background: var(--accent-green);
59
+ box-shadow: 0 0 12px var(--accent-green);
60
+ animation: pulse 2s infinite;
61
+ }
62
+
63
+ @keyframes pulse {
64
+ 0% { transform: scale(0.95); opacity: 0.8; }
65
+ 50% { transform: scale(1.2); opacity: 1; }
66
+ 100% { transform: scale(0.95); opacity: 0.8; }
67
+ }
68
+
69
+ .title-group h1 {
70
+ font-size: 1.25rem;
71
+ font-weight: 700;
72
+ letter-spacing: -0.02em;
73
+ background: linear-gradient(90deg, #fff, var(--accent-cyan));
74
+ -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent;
76
+ }
77
+
78
+ .subtitle {
79
+ font-size: 0.75rem;
80
+ color: var(--text-muted);
81
+ }
82
+
83
+ .header-controls {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 0.75rem;
87
+ }
88
+
89
+ .btn {
90
+ background: var(--bg-surface-elevated);
91
+ border: 1px solid var(--border);
92
+ color: var(--text-main);
93
+ padding: 0.45rem 0.9rem;
94
+ border-radius: 6px;
95
+ font-size: 0.8rem;
96
+ font-weight: 600;
97
+ cursor: pointer;
98
+ transition: all 0.2s ease;
99
+ }
100
+
101
+ .btn:hover {
102
+ border-color: var(--accent-cyan);
103
+ box-shadow: 0 0 10px rgba(0, 240, 255, 0.2);
104
+ }
105
+
106
+ .badge {
107
+ background: rgba(0, 240, 255, 0.1);
108
+ color: var(--accent-cyan);
109
+ border: 1px solid rgba(0, 240, 255, 0.3);
110
+ padding: 0.35rem 0.75rem;
111
+ border-radius: 20px;
112
+ font-size: 0.75rem;
113
+ font-weight: 700;
114
+ text-transform: uppercase;
115
+ letter-spacing: 0.05em;
116
+ }
117
+
118
+ .badge.connected {
119
+ background: rgba(0, 255, 136, 0.1);
120
+ color: var(--accent-green);
121
+ border-color: rgba(0, 255, 136, 0.3);
122
+ }
123
+
124
+ .badge.simulated {
125
+ background: rgba(255, 170, 0, 0.1);
126
+ color: var(--accent-amber);
127
+ border-color: rgba(255, 170, 0, 0.3);
128
+ }
129
+
130
+ .dashboard-grid {
131
+ display: grid;
132
+ grid-template-columns: repeat(4, 1fr);
133
+ gap: 1.25rem;
134
+ }
135
+
136
+ .metric-card {
137
+ background: var(--bg-surface);
138
+ border: 1px solid var(--border);
139
+ border-radius: 10px;
140
+ padding: 1.25rem;
141
+ display: flex;
142
+ flex-direction: column;
143
+ }
144
+
145
+ .card-label {
146
+ font-size: 0.7rem;
147
+ font-weight: 700;
148
+ letter-spacing: 0.05em;
149
+ color: var(--text-muted);
150
+ margin-bottom: 0.5rem;
151
+ }
152
+
153
+ .card-value {
154
+ font-size: 2rem;
155
+ font-weight: 800;
156
+ font-variant-numeric: tabular-nums;
157
+ line-height: 1.1;
158
+ }
159
+
160
+ .unit {
161
+ font-size: 0.9rem;
162
+ color: var(--text-muted);
163
+ font-weight: 400;
164
+ }
165
+
166
+ .text-accent { color: var(--accent-cyan); }
167
+ .text-green { color: var(--accent-green); }
168
+
169
+ .card-hint {
170
+ font-size: 0.75rem;
171
+ color: var(--text-muted);
172
+ margin-top: 0.4rem;
173
+ }
174
+
175
+ .panel {
176
+ background: var(--bg-surface);
177
+ border: 1px solid var(--border);
178
+ border-radius: 12px;
179
+ padding: 1.25rem;
180
+ }
181
+
182
+ .radar-panel {
183
+ grid-column: span 1;
184
+ display: flex;
185
+ flex-direction: column;
186
+ }
187
+
188
+ .table-panel {
189
+ grid-column: span 3;
190
+ display: flex;
191
+ flex-direction: column;
192
+ }
193
+
194
+ .panel-header {
195
+ display: flex;
196
+ justify-content: space-between;
197
+ align-items: center;
198
+ margin-bottom: 1rem;
199
+ }
200
+
201
+ .panel-header h2 {
202
+ font-size: 0.95rem;
203
+ font-weight: 700;
204
+ }
205
+
206
+ .panel-header .hint,
207
+ .panel-header .counter-badge {
208
+ font-size: 0.75rem;
209
+ color: var(--text-muted);
210
+ }
211
+
212
+ .radar-container {
213
+ display: flex;
214
+ justify-content: center;
215
+ align-items: center;
216
+ padding: 1rem 0;
217
+ flex-grow: 1;
218
+ }
219
+
220
+ #radarCanvas {
221
+ background: #07090e;
222
+ border-radius: 50%;
223
+ border: 2px solid var(--border);
224
+ box-shadow: 0 0 30px rgba(0, 240, 255, 0.08);
225
+ }
226
+
227
+ .table-scroll {
228
+ overflow-y: auto;
229
+ max-height: 380px;
230
+ border: 1px solid var(--border);
231
+ border-radius: 8px;
232
+ }
233
+
234
+ .tag-table {
235
+ width: 100%;
236
+ border-collapse: collapse;
237
+ font-size: 0.8rem;
238
+ text-align: left;
239
+ }
240
+
241
+ .tag-table th {
242
+ background: var(--bg-surface-elevated);
243
+ padding: 0.75rem 1rem;
244
+ font-size: 0.7rem;
245
+ color: var(--text-muted);
246
+ letter-spacing: 0.05em;
247
+ position: sticky;
248
+ top: 0;
249
+ z-index: 10;
250
+ }
251
+
252
+ .tag-table td {
253
+ padding: 0.75rem 1rem;
254
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
255
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
256
+ }
257
+
258
+ .tag-row.newly-added {
259
+ animation: highlightRow 1.2s ease-out;
260
+ }
261
+
262
+ @keyframes highlightRow {
263
+ 0% { background-color: rgba(0, 240, 255, 0.25); }
264
+ 100% { background-color: transparent; }
265
+ }
266
+
267
+ .dir-badge {
268
+ display: inline-block;
269
+ padding: 0.15rem 0.5rem;
270
+ border-radius: 4px;
271
+ font-size: 0.7rem;
272
+ font-weight: 700;
273
+ }
274
+
275
+ .dir-in {
276
+ background: rgba(0, 255, 136, 0.15);
277
+ color: var(--accent-green);
278
+ }
279
+
280
+ .dir-out {
281
+ background: rgba(0, 240, 255, 0.15);
282
+ color: var(--accent-cyan);
283
+ }
284
+
285
+ .rssi-bar-container {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 0.5rem;
289
+ }
290
+
291
+ .rssi-fill {
292
+ height: 6px;
293
+ background: var(--accent-cyan);
294
+ border-radius: 3px;
295
+ }
296
+
297
+ @media (max-width: 1024px) {
298
+ .dashboard-grid {
299
+ grid-template-columns: 1fr;
300
+ }
301
+ .radar-panel, .table-panel {
302
+ grid-column: span 1;
303
+ }
304
+ }
@@ -0,0 +1,37 @@
1
+ const { execSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const testAppDir = path.join(__dirname, 'test-output-app');
6
+
7
+ console.log('Testing create-autoid-app CLI scaffolding...');
8
+
9
+ // Clean up previous test if exists
10
+ if (fs.existsSync(testAppDir)) {
11
+ fs.rmSync(testAppDir, { recursive: true, force: true });
12
+ }
13
+
14
+ // Run CLI with args: [projectName, templateKey]
15
+ const binPath = path.join(__dirname, '..', 'bin', 'index.js');
16
+ execSync(`node "${binPath}" "${testAppDir}" web-radar-dashboard`, { stdio: 'inherit' });
17
+
18
+ // Assertions
19
+ const expectedFiles = [
20
+ path.join(testAppDir, 'package.json'),
21
+ path.join(testAppDir, 'index.html'),
22
+ path.join(testAppDir, 'src', 'main.js'),
23
+ path.join(testAppDir, 'src', 'style.css'),
24
+ path.join(testAppDir, 'README.md')
25
+ ];
26
+
27
+ for (const f of expectedFiles) {
28
+ if (!fs.existsSync(f)) {
29
+ console.error(`❌ Expected file not found: ${f}`);
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ // Clean up test output
35
+ fs.rmSync(testAppDir, { recursive: true, force: true });
36
+
37
+ console.log('✅ All CLI Scaffolding tests passed successfully!');
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.