Compare commits

..
Author SHA1 Message Date
Hans Sebastian Pöhlmann 45f8d5070e vibe-coded mock machine frontend in solidjs 2026-02-24 18:37:05 +01:00
115 changed files with 6869 additions and 5069 deletions

No files matched your search

-14
View File
@@ -1,14 +0,0 @@
## What does this PR do / add / change?
<!-- Describe the purpose of this PR. What problem does it solve or what feature does it add? -->
## Screenshots
<!-- Include screenshots for any UI changes. Remove this section if not applicable. -->
## Checklist before opening the pr
- [ ] Tested locally
- [ ] Tested on the machine (if hardware interaction is involved)
- [ ] No format errors (`cargo fmt` / `npm run format`)
- [ ] No build errors (`cargo build` / `npm run build`)
-3
View File
@@ -8,9 +8,6 @@ on:
branches:
- master
permissions:
contents: read
defaults:
run:
working-directory: electron
-213
View File
@@ -1,213 +0,0 @@
name: Stale Assigned Issue Check
on:
schedule:
# Run every Sunday at 9:00 AM UTC
- cron: '0 9 * * 0'
workflow_dispatch: # Allow manual triggering for testing
jobs:
check-stale-issues:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Check assigned issues for status updates
uses: actions/github-script@v7
with:
script: |
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
const now = new Date();
// Helper function to check rate limits
async function checkRateLimit() {
try {
const { data: rateLimit } = await github.rest.rateLimit.get();
const remaining = rateLimit.resources.core.remaining;
const reset = new Date(rateLimit.resources.core.reset * 1000);
console.log(`Rate limit: ${remaining} requests remaining (resets at ${reset.toISOString()})`);
return remaining > 10; // Stop if fewer than 10 requests remaining
} catch (error) {
console.log(`Could not check rate limit: ${error.message}`);
return true; // Continue on error
}
}
// Get all open issues
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
console.log(`Found ${issues.length} open issues`);
// Helper function to check if issue has linked PRs using GraphQL
async function hasLinkedPRs(issueNumber) {
try {
const query = `
query($owner: String!, $repo: String!, $issueNumber: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $issueNumber) {
timelineItems(first: 100, itemTypes: CONNECTED_EVENT) {
nodes {
... on ConnectedEvent {
subject {
... on PullRequest {
number
state
}
}
}
}
}
}
}
}
`;
const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
issueNumber: issueNumber
});
const linkedPRs = result.repository.issue.timelineItems.nodes
.filter(node => node.subject && node.subject.state === 'OPEN')
.map(node => node.subject.number);
return linkedPRs.length > 0;
} catch (error) {
console.log(`Could not check linked PRs for issue #${issueNumber}: ${error.message}`);
return false; // Assume no linked PRs on error to avoid false positives
}
}
let processedCount = 0;
let commentedCount = 0;
let errorCount = 0;
for (const issue of issues) {
try {
// Check rate limit periodically
if (processedCount % 10 === 0 && !(await checkRateLimit())) {
console.log('Rate limit nearly exhausted, stopping to avoid hitting limit');
break;
}
// Skip pull requests (they appear in issues list too)
if (issue.pull_request) {
continue;
}
// Skip if not assigned
if (!issue.assignees || issue.assignees.length === 0) {
console.log(`Issue #${issue.number}: Not assigned, skipping`);
continue;
}
console.log(`Checking issue #${issue.number}: ${issue.title}`);
processedCount++;
// Check if issue has linked PRs
const hasOpenLinkedPR = await hasLinkedPRs(issue.number);
if (hasOpenLinkedPR) {
console.log(`Issue #${issue.number}: Has linked open PR, skipping`);
continue;
}
// Get all comments on the issue
let comments = [];
try {
comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
} catch (commentError) {
console.error(`Issue #${issue.number}: Could not fetch comments - ${commentError.message}`);
errorCount++;
continue;
}
// Find the most recent comment or update
let lastActivity = new Date(issue.created_at);
if (issue.updated_at) {
lastActivity = new Date(issue.updated_at);
}
if (comments.length > 0) {
const lastComment = comments[comments.length - 1];
const lastCommentDate = new Date(lastComment.created_at);
if (lastCommentDate > lastActivity) {
lastActivity = lastCommentDate;
}
}
// Check if last activity was more than a week ago
const daysSinceActivity = Math.floor((now - lastActivity) / (1000 * 60 * 60 * 24));
if (daysSinceActivity >= 7) {
console.log(`Issue #${issue.number}: No activity for ${daysSinceActivity} days`);
// Check if we already posted a status request in the last 7 days
const recentComments = comments.filter(comment => {
const commentDate = new Date(comment.created_at);
return (now - commentDate) < ONE_WEEK_MS;
});
const alreadyAsked = recentComments.some(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('status update')
);
if (alreadyAsked) {
console.log(`Issue #${issue.number}: Already requested status this week, skipping`);
continue;
}
// Post a status request comment
try {
const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', ');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Hi ${assigneeNames}! 👋\n\n` +
`This issue has been assigned to you for ${daysSinceActivity} days with no recent activity or linked pull requests.\n\n` +
`Could you please provide a status update? If you're:\n` +
`- ✅ Working on it: Let us know your progress\n` +
`- 🔄 Need help: Tag someone or describe what's blocking you\n` +
`- 🚫 No longer working on it: Let us know so we can reassign\n\n` +
`Thank you!`
});
console.log(`Issue #${issue.number}: Posted status request comment`);
commentedCount++;
} catch (commentError) {
console.error(`Issue #${issue.number}: Could not post comment - ${commentError.message}`);
errorCount++;
// Continue to next issue
}
} else {
console.log(`Issue #${issue.number}: Activity ${daysSinceActivity} days ago, still fresh`);
}
} catch (error) {
console.error(`Issue #${issue.number}: Error processing - ${error.message}`);
errorCount++;
// Continue to next issue instead of failing the entire workflow
continue;
}
}
console.log(`\nStale issue check complete!`);
console.log(`Processed: ${processedCount} issues`);
console.log(`Commented on: ${commentedCount} issues`);
console.log(`Errors: ${errorCount}`);
-3
View File
@@ -8,9 +8,6 @@ on:
branches:
- master
permissions:
contents: read
env:
NIX_PATH: nixpkgs=channel:nixos-25.05
-3
View File
@@ -12,9 +12,6 @@ env:
CARGO_TERM_COLOR: always
RUST_VERSION: "1.86"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
+2 -2
View File
@@ -1,11 +1,11 @@
target/
dist/
.DS_Store
Cargo.lock
dhat-heap.json
build.sh
shell.nix
node_modules/
runtime_metrics.csv
compile_metrics.csv
.idea/
electron/dist-electron/
.idea/
-24
View File
@@ -1,24 +0,0 @@
#!/bin/sh
# Generate 18 random alphanumeric characters base64 encode It
password=$(head -c 18 /dev/urandom | base64)
echo "Basic Authentication; user=machine password=$password"
echo $password > /tmp/machine_password
caddy_hash=$(caddy hash-password --plaintext $password)
echo "(machine_basic_auth) {
basic_auth {
machine $caddy_hash
}
}" | sudo tee /var/lib/caddy/auth_snippet.conf
# Transfer ownership to caddy user
sudo chown caddy:caddy /var/lib/caddy/auth_snippet.conf
# Tigther permissions (Only readable by root or caddy user)
sudo chmod 600 /var/lib/caddy/auth_snippet.conf
sudo systemctl restart caddy.service
@@ -1,931 +0,0 @@
# Wago Stepper Controller - Developer Documentation
**Models:** 750-671 & 750-672
**Last Updated:** February 2026
## Table of Contents
1. [Overview](#overview)
2. [Hardware Quick Reference](#hardware-quick-reference)
3. [Architecture & Code Organization](#architecture--code-organization)
4. [Control & Status Bytes](#control--status-bytes)
5. [Implementation Details](#implementation-details)
6. [Prescalers & Calculations](#prescalers--calculations)
7. [Common Pitfalls & Debug Tips](#common-pitfalls--debug-tips)
---
## Overview
The Wago Stepper Controllers (750-671 and 750-672) drive stepper motors via EtherCAT. This documentation focuses on **velocity/speed control mode** implementation.
### Key Differences Between Models
| Feature | 750-671 | 750-672 |
| --------------- | ------------------------------------------ | ------------------------------------ |
| Max Voltage | 50 VDC | 70 VDC |
| Max Current | 5 A | 7.5 A |
| Speed Control | Via Application Selector in config | Via Control Byte C1 command |
| Control Byte C1 | Different bit layout (M_Speed_Control bit) | Command field (0x18 = Speed Control) |
**For 750-671:** Must set `Application_Selector = 2` and `PWM_Period = 0` in WAGO I/O CHECK before using speed control.
**For 750-672:** Speed control is selected directly via command in Control Byte C1 (no config needed).
### Process Image
Both models use **12 bytes bidirectional** (96 bits each way):
```
Input (TxPDO - Controller → PLC):
[0] S0 (status)
[1] Reserved
[2-3] Actual Velocity (i16)
[4-5] Reserved
[6-8] Actual Position (24-bit: L, M, H)
[9] S3 (digital inputs, warnings)
[10] S2 (busy, error, on_speed, etc.)
[11] S1 (acknowledgments)
Output (RxPDO - PLC → Controller):
[0] C0 (control)
[1] Reserved
[2-3] Target Velocity (i16, clamped to ±25000)
[4-5] Acceleration (u16)
[6-8] Position (24-bit: L, M, H)
[9] C3 (position/reset control)
[10] C2 (prescalers, error quit)
[11] C1 (main control: enable, start, mode)
```
---
## Hardware Quick Reference
### Connections
| Pin | Name | Purpose |
|-----|------|---------|
| DI1+ | Digital Input 1 | Enable input (connect to 24V or control via software) |
| DI2+ | Digital Input 2 | Referencing input |
| DI- | Ground | Reference potential for DI1/DI2 |
| A1, A2 | Motor Winding A | Connect to motor phase A |
| B1, B2 | Motor Winding B | Connect to motor phase B |
### Status LEDs
- **LED H:** Shows error codes via blink patterns (see Wago manual section 3.3 for error codes)
- Other LEDs show Ready, Enable, Busy states (see manual section 2.1.2.3)
---
## Architecture & Code Organization
### File Structure
```
devices/
wago_modules/
wago_750_672.rs # Device struct, EtherCAT interface
io/
stepper_velocity_wago_750_672.rs # High-level wrapper & byte helpers
```
### Key Components
#### 1. `Wago750_672` Struct (EtherCAT Device)
Located in `devices/wago_modules/wago_750_672.rs`
```rust
pub struct Wago750_672 {
is_used: bool,
tx_bit_offset: usize, // Input data offset
rx_bit_offset: usize, // Output data offset
pub rxpdo: Wago750_672RxPdo, // Data we send
pub txpdo: Wago750_672TxPdo, // Data we receive
module: Option<Module>,
pub state: InitState,
pub initialized: bool,
}
```
**Responsibilities:**
- Implements EtherCAT device traits
- Handles bit-level I/O with process image
- Runs state machine in `input()` method
- Thread-safe (used via `Arc<RwLock<>>`)
#### 2. `StepperVelocityWago750672` Wrapper
Located in `io/stepper_velocity_wago_750_672.rs`
```rust
pub struct StepperVelocityWago750672 {
pub device: Arc<RwLock<Wago750_672>>,
pub state: InitState,
pub target_velocity: i16,
pub target_acceleration: u16,
pub enabled: bool,
pub freq_range_sel: u8,
pub acc_range_sel: u8,
}
```
**Responsibilities:**
- Provides high-level API (`set_velocity()`, `set_enabled()`)
- Manages prescaler settings
- Abstracts away state machine complexity
### Why Two Layers?
1. **`Wago750_672`:** Low-level EtherCAT device that handles cycle-by-cycle I/O
2. **`StepperVelocityWago750672`:** High-level wrapper for application logic
This separation allows:
- EtherCAT device to run in tight real-time loop
- Application code to use simple API without worrying about cycles/acknowledgments
- Easy testing of each layer independently
---
## Control & Status Bytes
### Design Pattern: Type-Safe Byte Construction
Instead of hardcoding hex values like `0x1B`, we use builder pattern with enums:
```rust
// ❌ BAD - magic numbers, error-prone
dev.rxpdo.c1 = 0x1B;
// ✅ GOOD - explicit, self-documenting
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
dev.rxpdo.c1 = c1;
```
### Wago 750-672 Control Bytes
#### Control Byte C1 (Offset 11) - Main Control
```rust
pub struct ControlByteC1(u8);
pub enum C1Flag {
Enable = 0b0000_0001, // Bit 0: Must be 1 to enable
Stop2N = 0b0000_0010, // Bit 1: Must be 1 for normal operation
Start = 0b0000_0100, // Bit 2: Rising edge triggers action
}
pub enum C1Command {
Idle = 0b0000_0000, // Bits 3-7: Operating mode
SinglePosition = 0b0000_1000,
RunProgram = 0b0001_0000,
SpeedControl = 0b0001_1000, // ← We use this
Reference = 0b0010_0000,
JogMode = 0b0010_1000,
Mailbox = 0b0011_0000,
}
```
**Implementation:**
```rust
impl ControlByteC1 {
pub const fn new() -> Self {
Self(0)
}
pub const fn with_flag(mut self, flag: C1Flag) -> Self {
self.0 |= flag as u8; // OR in the bit
self
}
pub const fn with_command(mut self, cmd: C1Command) -> Self {
self.0 = (self.0 & 0b0000_0111) | (cmd as u8); // Mask lower 3 bits, set upper 5
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
```
**Key Points:**
- `const fn` allows compile-time evaluation
- Chainable builder pattern
- Command overwrites bits 3-7 while preserving 0-2
- Flags are additive (multiple can be set)
#### Control Byte C2 (Offset 10) - Prescalers & Error Handling
```rust
pub struct ControlByteC2(u8);
pub enum C2Flag {
FreqRangeSelL = 0b0000_0001, // Bit 0: Freq prescaler LSB
FreqRangeSelH = 0b0000_0010, // Bit 1: Freq prescaler MSB
AccRangeSelL = 0b0000_0100, // Bit 2: Acc prescaler LSB
AccRangeSelH = 0b0000_1000, // Bit 3: Acc prescaler MSB
PreCalc = 0b0100_0000, // Bit 6: Pre-calculation
ErrorQuit = 0b1000_0000, // Bit 7: Acknowledge error
}
impl ControlByteC2 {
pub const fn with_freq_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_1100) | (sel & 0b0000_0011);
self
}
pub const fn with_acc_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_0011) | ((sel & 0b0000_0011) << 2);
self
}
}
```
**Critical:** Prescalers can only be changed when controller is **disabled** (before Enable state).
#### Control Byte C3 (Offset 9) - Position & Reset
```rust
pub struct ControlByteC3(u8);
pub enum C3Flag {
SetActualPos = 0b0000_0001, // Bit 0: Set current position
DirectionPos = 0b0000_0010, // Bit 2: Force positive direction
DirectionNeg = 0b0000_0100, // Bit 3: Force negative direction
ResetQuit = 0b1000_0000, // Bit 7: Acknowledge reset
}
```
### Status Bytes (Read-Only)
#### Status Byte S1 (Offset 11) - Acknowledgments
```rust
pub struct StatusByteS1(u8);
pub enum S1Flag {
Ready = 0b0000_0001, // Bit 0: Acknowledges Enable
Stop2NAck = 0b0000_0010, // Bit 1: Acknowledges Stop2_N
StartAck = 0b0000_0100, // Bit 2: Acknowledges Start
}
// Bits 3-7: Mirrors C1 command when acknowledged
impl StatusByteS1 {
pub const fn has_flag(self, flag: S1Flag) -> bool {
(self.0 & flag as u8) != 0
}
}
```
**Usage Pattern:**
```rust
// Wait for acknowledgment
if self.txpdo.s1 == c1 {
// All bits acknowledged, proceed
}
```
#### Status Byte S2 (Offset 10) - Operation Status
```rust
pub struct StatusByteS2(u8);
pub enum S2Flag {
OnTarget = 0b0000_0001, // Bit 0: Position reached (positioning mode)
Busy = 0b0000_0010, // Bit 1: Executing command
StandStill = 0b0000_0100, // Bit 2: Motor not moving
OnSpeed = 0b0000_1000, // Bit 3: Target velocity reached
Direction = 0b0001_0000, // Bit 4: Rotation direction (0=neg, 1=pos)
ReferenceOk = 0b0010_0000, // Bit 5: Referencing complete
PreCalcAck = 0b0100_0000, // Bit 6: Pre-calc acknowledged
Error = 0b1000_0000, // Bit 7: Error occurred (check LED H)
}
```
**Critical for velocity control:** Monitor `OnSpeed` bit to know when target velocity is reached.
#### Status Byte S3 (Offset 9) - Digital Inputs & Warnings
```rust
pub struct StatusByteS3(u8);
pub enum S3Flag {
Input1 = 0b0000_0001, // Bits 0-5: Digital input states
Input2 = 0b0000_0010,
Input3 = 0b0000_0100,
Input4 = 0b0000_1000,
Input5 = 0b0001_0000,
Input6 = 0b0010_0000,
Warning = 0b0100_0000, // Bit 6: Warning condition
Reset = 0b1000_0000, // Bit 7: Reset requested
}
```
---
## Implementation Details
### Critical Initialization Sequence
⚠️ **This sequence MUST be followed exactly or the controller won't work:**
1. **Set non-zero acceleration** (before enabling)
2. **Set Enable + Stop2_N** (wait for ack)
3. **Set operating mode** (0x18 for speed control, wait for ack)
4. **Issue Start pulse** (set to 1, wait for ack, set to 0, wait for ack clear)
5. **Running** (repeat step 4 every time velocity changes)
### State Machine (Why It Exists)
The state machine exists because **acknowledgments happen in different EtherCAT cycles**. You can't just set everything in one cycle.
```rust
#[derive(Debug, Clone)]
pub enum InitState {
Off, // Controller disabled
Enable, // Setting Enable + Stop2_N, waiting for ack
SetMode, // Setting speed control mode, waiting for ack
StartPulseStart, // Setting Start bit, waiting for ack
StartPulseEnd, // Clearing Start bit, waiting for ack clear
Running, // Normal operation
ErrorQuit, // Handling error
ResetQuit, // Handling reset
}
```
### State Machine Implementation
Located in `Wago750_672::input()` method (runs every EtherCAT cycle):
```rust
fn input(&mut self, input: &BitSlice) -> Result<(), anyhow::Error> {
// 1. Read process image from controller
let base = self.tx_bit_offset;
let mut b = [0u8; 12];
for i in 0..12 {
b[i] = input[base + i * 8..base + (i + 1) * 8].load_le();
}
self.txpdo = Wago750_672TxPdo {
s0: b[0],
actual_velocity: i16::from_le_bytes([b[2], b[3]]),
position_l: b[6],
position_m: b[7],
position_h: b[8],
s3: b[9],
s2: b[10],
s1: b[11],
};
// 2. State machine logic
match self.state {
InitState::Off => {
self.initialized = false;
// Do nothing until external enable request
}
InitState::Enable => {
// Build C1 byte with Enable and Stop2_N
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.bits();
self.rxpdo.c1 = c1;
// Wait for acknowledgment
if self.txpdo.s1 == c1 {
self.state = InitState::SetMode;
}
}
InitState::SetMode => {
// Add speed control command
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Wait for mode acknowledgment
if self.txpdo.s1 == c1 {
self.initialized = true;
self.state = InitState::StartPulseStart;
}
}
InitState::StartPulseStart => {
// Set Start bit
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_flag(C1Flag::Start) // ← Rising edge
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Wait for Start acknowledgment
if self.txpdo.s1 == c1 {
self.state = InitState::StartPulseEnd;
}
}
InitState::StartPulseEnd => {
// Clear Start bit
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.c1 = c1;
// Wait for acknowledgment to clear
if self.txpdo.s1 == c1 {
self.state = InitState::Running;
}
}
InitState::Running => {
// Monitor for errors and resets
let c2_error_mask = ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
let c3_reset_mask = ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
if self.txpdo.s2 & c2_error_mask != 0 {
self.state = InitState::ErrorQuit;
} else if self.txpdo.s3 & c3_reset_mask != 0 {
self.state = InitState::ResetQuit;
}
}
InitState::ErrorQuit => {
// Set Error_Quit bit to acknowledge
self.rxpdo.c2 |= ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
tracing::error!("Stepper Controller Errored. Trying to reenable...");
self.state = InitState::Enable; // Restart initialization
}
InitState::ResetQuit => {
// Set Reset_Quit bit to acknowledge
self.rxpdo.c3 |= ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
tracing::error!("Stepper Controller Reset. Trying to reenable...");
self.state = InitState::Enable; // Restart initialization
}
}
Ok(())
}
```
**Key Implementation Notes:**
1. **State changes happen AFTER acknowledgment:** We wait for `self.txpdo.s1 == c1` before moving to next state
2. **Enable and Stop2_N stay high:** They're included in every C1 byte after Enable state
3. **Start pulse is a rising edge:** Must go 1 → 0, not just stay at 1
4. **Error recovery:** Automatically attempts to re-enable on error/reset
### High-Level API Implementation
Located in `StepperVelocityWago750672`:
```rust
pub fn set_enabled(&mut self, enabled: bool) {
// Prevent redundant enables
if self.enabled && enabled {
return;
}
// Safety check: acceleration must be non-zero
if self.get_target_acceleration() == 0 {
return;
}
self.enabled = enabled;
if enabled {
self.change_init_state(InitState::Enable);
} else {
self.change_init_state(InitState::Off);
self.write_control_byte(ControlByte::C1, 0b00000000);
}
}
pub fn set_velocity(&mut self, velocity: i16) {
self.target_velocity = velocity;
let mut dev = block_on(self.device.write());
// Clamp to valid range
dev.rxpdo.velocity = velocity.clamp(-25000, 25000);
// Trigger Start pulse if already initialized
if dev.initialized {
dev.state = InitState::StartPulseStart;
}
}
pub fn set_acceleration(&mut self, acceleration: u16) {
self.target_acceleration = acceleration;
let mut dev = block_on(self.device.write());
dev.rxpdo.acceleration = acceleration;
}
```
**Why the wrapper?**
- Application code doesn't need to know about state machine
- Handles common errors (zero acceleration check)
- Thread-safe access to device
- Clamping and validation in one place
### Changing Velocity at Runtime
⚠️ **Critical:** Must issue a **new Start pulse** for every velocity change:
```rust
// User calls this
stepper.set_velocity(15000);
// Internally, set_velocity() does:
dev.rxpdo.velocity = 15000;
if dev.initialized {
dev.state = InitState::StartPulseStart; // ← Triggers new pulse
}
// State machine then handles:
// StartPulseStart → StartPulseEnd → Running
// (Takes 2-3 EtherCAT cycles)
```
**Common mistake:** Setting velocity without triggering Start pulse - motor won't change speed.
---
## Prescalers & Calculations
Prescalers scale the velocity and acceleration values. They **can only be changed when disabled**.
### Frequency Prescaler (Bits 0-1 of C2)
Controls velocity scaling:
| Freq_Range_Sel | Prescaler | Max Frequency | Resolution |
|----------------|-----------|---------------|------------|
| 00 | 200 (default) | 10 kHz | Configurable |
| 01 | 80 | 25 kHz | 1 Hz |
| 10 | 20 | 100 kHz | 20 Hz |
| 11 | 4 | 500 kHz | 20 Hz |
**Formula:**
```
Output Frequency (fp) = Velocity × 80 / Freq_Prescaler [Hz]
```
**Example:**
```rust
// Freq_Range_Sel = 01 (prescaler = 80)
// Velocity = 10000
// Output: 10000 × 80 / 80 = 10000 Hz
stepper.set_freq_range_sel(1); // Must be done BEFORE enable
stepper.set_velocity(10000);
```
### Acceleration Prescaler (Bits 2-3 of C2)
Controls acceleration scaling:
| Acc_Range_Sel | Multiplier | Time to Max Speed |
|---------------|------------|-------------------|
| 00 | 8 (default) | 7600 ms |
| 01 | 80 | 760 ms |
| 10 | 800 | 76 ms |
| 11 | 8000 | 7.6 ms |
**Formula:**
```
Acceleration (a) = Acceleration_Value × Acc_Multiplier / Freq_Prescaler [Hz/s]
```
**Example:**
```rust
// Acc_Range_Sel = 01 (multiplier = 80)
// Freq_Range_Sel = 01 (prescaler = 80)
// Acceleration_Value = 10000
// Output: 10000 × 80 / 80 = 10000 Hz/s
stepper.set_acc_range_sel(1); // Must be done BEFORE enable
stepper.set_acceleration(10000);
```
### Setting Prescalers (Implementation)
```rust
pub fn set_freq_range_sel(&mut self, factor: u8) {
// Safety checks
if self.enabled || factor > 3 {
return; // Can't change while enabled
}
self.freq_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.c2)
.with_freq_range(factor)
.bits();
dev.rxpdo.c2 = c2;
}
pub fn set_acc_range_sel(&mut self, factor: u8) {
if self.enabled || factor > 3 {
return;
}
self.acc_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.c2)
.with_acc_range(factor)
.bits();
dev.rxpdo.c2 = c2;
}
```
**Usage:**
```rust
let mut stepper = StepperVelocityWago750672::new(device);
// Set prescalers FIRST (before enabling)
stepper.set_freq_range_sel(1);
stepper.set_acc_range_sel(1);
stepper.set_acceleration(10000);
// Then enable
stepper.set_enabled(true);
// Now can set velocity
stepper.set_velocity(15000);
```
---
## Common Pitfalls & Debug Tips
### 1. Controller Won't Enable
**Symptoms:**
- State stuck in `Enable`
- `initialized` never becomes true
- Motor doesn't move
**Debug checklist:**
```rust
// Check acceleration is non-zero
println!("Acceleration: {}", dev.rxpdo.acceleration); // Must be > 0
// Check DI1 if hardware enable is configured
println!("DI1 state: {}", (dev.txpdo.s3 & 0x01) != 0); // Should be 1
// Check status byte acknowledgment
println!("C1: {:#010b}", dev.rxpdo.c1);
println!("S1: {:#010b}", dev.txpdo.s1); // Should match C1
```
**Common causes:**
```rust
// ❌ Forgot to set acceleration
stepper.set_acceleration(0); // Won't enable!
// ✅ Always set non-zero acceleration
stepper.set_acceleration(10000);
stepper.set_enabled(true);
```
### 2. Velocity Won't Change
**Symptoms:**
- Motor running but won't change speed
- `set_velocity()` called but no effect
**Debug:**
```rust
// Check initialized flag
println!("Initialized: {}", dev.initialized);
// Check state after velocity change
println!("State: {:?}", dev.state); // Should become StartPulseStart
// Manually trigger Start pulse (bypass wrapper)
dev.state = InitState::StartPulseStart;
```
**Common causes:**
```rust
// ❌ Setting velocity before initialization
stepper.set_velocity(10000); // Controller not ready yet
// ✅ Check initialized first
if stepper.device.read().initialized {
stepper.set_velocity(10000);
}
```
### 3. Prescaler Not Working
**Symptoms:**
- Velocity calculation seems wrong
- Can't reach expected frequency
**Debug:**
```rust
// Check prescalers were set BEFORE enabling
println!("Freq prescaler: {}", stepper.freq_range_sel);
println!("Acc prescaler: {}", stepper.acc_range_sel);
println!("Enabled: {}", stepper.enabled);
// Check C2 byte
let dev = stepper.device.read();
println!("C2: {:#010b}", dev.rxpdo.c2);
// Bits 0-1: freq, Bits 2-3: acc
```
**Common causes:**
```rust
// ❌ Setting prescaler after enable
stepper.set_enabled(true);
stepper.set_freq_range_sel(1); // Too late! Ignored.
// ✅ Set prescalers FIRST
stepper.set_freq_range_sel(1);
stepper.set_acc_range_sel(1);
stepper.set_enabled(true);
```
### 4. State Machine Stuck
**Symptoms:**
- State doesn't progress
- Acknowledgments not received
**Debug:**
```rust
// Add logging to state machine
match self.state {
InitState::Enable => {
println!("Enable state: C1={:#010b}, S1={:#010b}", self.rxpdo.c1, self.txpdo.s1);
// ...
}
}
// Check EtherCAT communication
// - Is cycle running?
// - Are other devices working?
// - Check bus topology
```
### 5. Error Bit Set
**Symptoms:**
- Error bit (S2 bit 7) is 1
- LED H blinking
- Controller stops
**Debug:**
```rust
// Check error state
let s2 = StatusByteS2::from_bits(dev.txpdo.s2);
if s2.has_flag(S2Flag::Error) {
println!("ERROR DETECTED");
// Count LED H blinks for error code
// See Wago manual section 3.3
// Check other status flags for clues
println!("Busy: {}", s2.has_flag(S2Flag::Busy));
println!("StandStill: {}", s2.has_flag(S2Flag::StandStill));
}
```
**Common error codes:**
- **2 blinks:** Overcurrent
- **3 blinks:** Position error
- **4 blinks:** Encoder fault
- See Wago manual for complete list
---
### Supporting Wago 750-671
The 750-671 uses different C1 bit layout:
1. **Create separate enum:**
```rust
// For 750-671
pub enum C1Flag671 {
Enable = 0b0000_0001, // Bit 0
Stop2N = 0b0000_0010, // Bit 1
Start = 0b0000_0100, // Bit 2
MSpeedControl = 0b0000_1000, // Bit 3 (instead of command field)
MProgram = 0b0001_0000, // Bit 4
MReference = 0b0010_0000, // Bit 5
MJog = 0b0100_0000, // Bit 6
MDriveByMBX = 0b1000_0000, // Bit 7
}
```
---
## Quick Reference Card
### Initialization Checklist
```rust
// 1. Create and configure
let stepper = StepperVelocityWago750672::new(device);
// 2. Set prescalers (optional, before enable)
stepper.set_freq_range_sel(1); // 0-3
stepper.set_acc_range_sel(1); // 0-3
// 3. Set non-zero acceleration (REQUIRED)
stepper.set_acceleration(10000);
// 4. Enable
stepper.set_enabled(true);
// 5. Set velocity (after initialized)
stepper.set_velocity(15000); // ±25000 max
```
### Key Constants
```rust
const SPEED_CONTROL_CMD: u8 = 0x18; // C1 command for 750-672
const MAX_VELOCITY: i16 = 25000;
const MIN_VELOCITY: i16 = -25000;
const DEFAULT_ACCELERATION: u16 = 10000;
```
### Critical Rules
1. ⚠️ **Never enable with zero acceleration**
2. ⚠️ **Always wait for acknowledgments**
3. ⚠️ **Issue Start pulse for every velocity change**
4. ⚠️ **Prescalers can only be changed when disabled**
5. ⚠️ **Keep Enable and Stop2_N high during operation**
### Common Bit Patterns
```rust
// Enable + Stop2_N
0b0000_0011
// Enable + Stop2_N + Speed Control
0b0001_1011
// Enable + Stop2_N + Speed Control + Start
0b0001_1111
```
### Debug Commands
```bash
# View byte values
println!("C1: {:#010b}", dev.rxpdo.c1);
println!("S1: {:#010b}", dev.txpdo.s1);
# Check specific flags
let s2 = StatusByteS2::from_bits(dev.txpdo.s2);
println!("Error: {}", s2.has_flag(S2Flag::Error));
println!("OnSpeed: {}", s2.has_flag(S2Flag::OnSpeed));
# Monitor state
println!("State: {:?}", dev.state);
println!("Initialized: {}", dev.initialized);
```
---
## Additional Resources
- **Wago Manual:** Section 2.1.2.3 (LEDs), 3.3 (Error Codes)
- **WAGO I/O CHECK:** Configuration software for 750-671 application selector
- **EtherCAT Specification:** www.ethercat.org
- **Code Location:**
- Device: `devices/wago_modules/wago_750_672.rs`
- Wrapper: `io/stepper_velocity_wago_750_672.rs`
---
## Changelog
| Version | Date | Changes |
| ------- | ------- | ------------------------------------------- |
| 1.0 | 2025 | Initial implementation with state machine |
| 2.0 | 2026-02 | Added comprehensive developer documentation |
---
**Need help?** Check the troubleshooting section or add debug logging using the template above.
+1 -17
View File
@@ -136,7 +136,7 @@ A slave recognizes message data as one message when a **3.5 character long no-da
# Inverter settings
## Motor Constants for ExtruderV2:
## Motor Constants
The following is a table that shows which Registers need to be set on the Inverter for the safe operation of the Motor.
@@ -151,22 +151,6 @@ The following is a table that shows which Registers need to be set on the Invert
| `3` | Base frequency | 5000 (50 Hz) |
| `19` | Base frequency voltage | 400 (Volt) |
## Motor Constants for ExtruderV3:
The following is a table that shows which Registers need to be set on the Inverter for the safe operation of the Motor.
| Parameter number (Pr) | Name | Value |
| --------------------- | ---------------------------- | ------------- |
| `71` | Applied Motor | 0 |
| `80` | Motor Capacity | 22 (2.2 kw) |
| `96` | Auto tuning setting/status | 1 |
| `1` | Max Frequency | 5000 |
| `2` | Min Frequency | 0 |
| `9` | Electronic Thermal O/L Relay | 4.40 (Ampere) |
| `3` | Base frequency | 5000 (50 Hz) |
| `19` | Base frequency voltage | 400 (Volt) |
## All Settings needed for the Inverter Communication/Operation with Extruder
The Following settings are required to get the inverter Communication and Operation working properly
+226 -181
View File
@@ -71,7 +71,7 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"@types/react-virtualized": "^9.22.2",
"electron": "^40.6.1",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.2",
@@ -162,6 +162,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -658,6 +659,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -681,6 +683,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -2582,9 +2585,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
"cpu": [
"arm"
],
@@ -2595,9 +2598,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
"cpu": [
"arm64"
],
@@ -2608,9 +2611,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
"cpu": [
"arm64"
],
@@ -2621,9 +2624,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
"cpu": [
"x64"
],
@@ -2634,9 +2637,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
"cpu": [
"arm64"
],
@@ -2647,9 +2650,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
"cpu": [
"x64"
],
@@ -2660,9 +2663,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
"cpu": [
"arm"
],
@@ -2673,9 +2676,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
"cpu": [
"arm"
],
@@ -2686,9 +2689,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
"cpu": [
"arm64"
],
@@ -2699,9 +2702,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
"cpu": [
"arm64"
],
@@ -2712,22 +2715,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
"cpu": [
"loong64"
],
@@ -2738,22 +2728,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
"cpu": [
"ppc64"
],
@@ -2764,9 +2741,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
"cpu": [
"riscv64"
],
@@ -2777,9 +2754,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
"cpu": [
"riscv64"
],
@@ -2790,9 +2767,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
"cpu": [
"s390x"
],
@@ -2803,9 +2780,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
"cpu": [
"x64"
],
@@ -2816,9 +2793,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
"cpu": [
"x64"
],
@@ -2828,23 +2805,10 @@
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
"cpu": [
"arm64"
],
@@ -2855,9 +2819,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
"cpu": [
"arm64"
],
@@ -2868,9 +2832,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
"cpu": [
"ia32"
],
@@ -2881,9 +2845,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
"cpu": [
"x64"
],
@@ -2894,9 +2858,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
"cpu": [
"x64"
],
@@ -3174,6 +3138,66 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
@@ -3266,6 +3290,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.1.tgz",
"integrity": "sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/react-store": "^0.8.0",
@@ -3356,6 +3381,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.141.1.tgz",
"integrity": "sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/store": "^0.8.0",
@@ -3576,8 +3602,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -3743,6 +3768,7 @@
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3759,6 +3785,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3769,6 +3796,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3856,6 +3884,7 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
@@ -3999,37 +4028,24 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz",
"integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^5.0.2"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -4223,6 +4239,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4264,9 +4281,9 @@
}
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4286,7 +4303,6 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -4601,6 +4617,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4956,7 +4973,8 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -5192,9 +5210,9 @@
}
},
"node_modules/diff": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -5219,8 +5237,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -5248,15 +5265,15 @@
}
},
"node_modules/electron": {
"version": "40.6.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-40.6.1.tgz",
"integrity": "sha512-u9YfoixttdauciHV9Ut9Zf3YipJoU093kR1GSYTTXTAXqhiXI0G1A0NnL/f0O2m2UULCXaXMf2W71PloR6V9pQ==",
"version": "37.10.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz",
"integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^24.9.0",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -5337,6 +5354,16 @@
"global-agent": "^3.0.0"
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "22.19.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/electron/node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -5372,6 +5399,13 @@
"semver": "bin/semver.js"
}
},
"node_modules/electron/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/electron/node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -5749,6 +5783,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5809,6 +5844,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -5910,6 +5946,7 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -6868,6 +6905,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -6901,6 +6939,7 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -7503,6 +7542,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -8029,7 +8069,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -8951,9 +8990,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -9450,6 +9489,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -9566,7 +9606,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -9582,7 +9621,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -9672,6 +9710,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9681,6 +9720,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9693,6 +9733,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9736,8 +9777,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -10066,9 +10106,9 @@
}
},
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -10081,31 +10121,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"@rollup/rollup-android-arm-eabi": "4.53.3",
"@rollup/rollup-android-arm64": "4.53.3",
"@rollup/rollup-darwin-arm64": "4.53.3",
"@rollup/rollup-darwin-x64": "4.53.3",
"@rollup/rollup-freebsd-arm64": "4.53.3",
"@rollup/rollup-freebsd-x64": "4.53.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
"@rollup/rollup-linux-arm-musleabihf": "4.53.3",
"@rollup/rollup-linux-arm64-gnu": "4.53.3",
"@rollup/rollup-linux-arm64-musl": "4.53.3",
"@rollup/rollup-linux-loong64-gnu": "4.53.3",
"@rollup/rollup-linux-ppc64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-musl": "4.53.3",
"@rollup/rollup-linux-s390x-gnu": "4.53.3",
"@rollup/rollup-linux-x64-gnu": "4.53.3",
"@rollup/rollup-linux-x64-musl": "4.53.3",
"@rollup/rollup-openharmony-arm64": "4.53.3",
"@rollup/rollup-win32-arm64-msvc": "4.53.3",
"@rollup/rollup-win32-ia32-msvc": "4.53.3",
"@rollup/rollup-win32-x64-gnu": "4.53.3",
"@rollup/rollup-win32-x64-msvc": "4.53.3",
"fsevents": "~2.3.2"
}
},
@@ -10254,6 +10291,7 @@
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz",
"integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
}
@@ -10795,7 +10833,8 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -10887,6 +10926,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -11130,6 +11170,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11466,6 +11507,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -11555,7 +11597,8 @@
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
@@ -11593,6 +11636,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -12032,6 +12076,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+1 -1
View File
@@ -34,7 +34,7 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"@types/react-virtualized": "^9.22.2",
"electron": "^40.6.1",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.2",
+4
View File
@@ -8,11 +8,15 @@ import { router } from "./routes/router";
import { RouterProvider } from "@tanstack/react-router";
import { Toaster } from "./components/ui/sonner";
import { enableMapSet } from "immer";
import { useGlobalLogStreaming } from "./hooks/useGlobalLogStreaming";
import { GlobalLaserToastManager } from "./setup/GlobalLaserToastManager";
export default function App() {
const { i18n } = useTranslation();
// Start global log streaming
useGlobalLogStreaming();
useEffect(() => {
syncThemeWithLocal();
updateAppLanguage(i18n);
+3 -12
View File
@@ -20,7 +20,6 @@ import { TouchButton } from "../touch/TouchButton";
import { seriesToUPlotData } from "@/lib/timeseries";
import { ControlCard } from "@/control/ControlCard";
import { getAllTimeSeries } from "./createChart";
import { TargetDashOverlay } from "./TargetDashOverlay";
// Collects lines connected to visible series for rendering
const getVisibleLines = (data: any, visibleSeries: boolean[]) => {
@@ -596,18 +595,10 @@ export function BigGraph({
<div className="flex-1 overflow-hidden rounded-b-3xl pt-4">
<div
className={`relative h-full w-full overflow-hidden transition-opacity duration-100`}
ref={containerRef}
className={`h-full w-full overflow-hidden transition-opacity duration-100`}
style={{ backgroundColor: colors.background }}
>
<div ref={containerRef} className="h-full w-full overflow-hidden" />
<TargetDashOverlay
uplotRef={uplotRef}
newData={newData}
config={enhancedConfig}
selectedTimeWindow={selectedTimeWindow}
isLiveMode={isLiveMode}
/>
</div>
/>
</div>
</div>
</div>
@@ -80,7 +80,7 @@ export function GraphControls({
)}
<TouchButton
onClick={() => onSwitchToHistorical("button")}
onClick={onSwitchToHistorical}
variant="outline"
className={`h-auto px-3 py-3 text-base font-medium transition-colors ${
!isLiveMode
@@ -197,7 +197,7 @@ export function FloatingControlPanel({
)}
<TouchButton
onClick={() => onSwitchToHistorical("button")}
onClick={onSwitchToHistorical}
variant="outline"
className={`h-auto px-3 py-3 text-base font-medium transition-colors ${
!isLiveMode
@@ -1,411 +0,0 @@
import React, { useEffect, useMemo, useState, useId } from "react";
import uPlot from "uplot";
import { BigGraphProps, GraphConfig } from "./types";
import { getAllTimeSeries } from "./createChart";
type OverlayLine = {
key: string;
color: string;
width: number;
dash: number[];
d: string;
};
type ClipRect = {
x: number;
y: number;
width: number;
height: number;
};
function findFirstGe(arr: number[], value: number): number {
let lo = 0;
let hi = arr.length - 1;
let ans = arr.length;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] >= value) {
ans = mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return ans;
}
function findLastLe(arr: number[], value: number): number {
let lo = 0;
let hi = arr.length - 1;
let ans = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] <= value) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
function buildSteppedPath(
u: uPlot,
xData: number[],
yData: Array<number | null>,
): string {
const xMin = u.scales.x?.min;
const xMax = u.scales.x?.max;
if (xData.length < 2 || xMin === undefined || xMax === undefined) {
return "";
}
const firstVisibleIdx = Math.max(0, findFirstGe(xData, xMin) - 1);
const lastVisibleIdx = Math.min(
xData.length - 1,
findLastLe(xData, xMax) + 1,
);
if (lastVisibleIdx < firstVisibleIdx) {
return "";
}
const parts: string[] = [];
let started = false;
let prevX = 0;
let prevY = 0;
for (let i = firstVisibleIdx; i <= lastVisibleIdx; i++) {
const value = yData[i];
if (value === null || value === undefined) continue;
const x = u.valToPos(xData[i], "x", true);
const y = u.valToPos(value, "y", true);
if (!started) {
parts.push(`M ${x} ${y}`);
started = true;
prevX = x;
prevY = y;
continue;
}
if (x !== prevX) {
parts.push(`L ${x} ${prevY}`);
}
if (y !== prevY) {
parts.push(`L ${x} ${y}`);
}
prevX = x;
prevY = y;
}
return parts.join(" ");
}
function buildFlatLatestValuePath(
u: uPlot,
yData: Array<number | null>,
): string {
let latestValue: number | null = null;
for (let i = yData.length - 1; i >= 0; i--) {
const value = yData[i];
if (value !== null && value !== undefined) {
latestValue = value;
break;
}
}
if (latestValue === null) {
return "";
}
const y = u.valToPos(latestValue, "y", true);
const left = u.bbox.left;
const right = u.bbox.left + u.bbox.width;
return `M ${left} ${y} L ${right} ${y}`;
}
function getHistoricalDashTargets(
data: BigGraphProps["newData"],
config: GraphConfig,
): Array<{
dataIndex: number;
dash: number[];
color: string;
width: number;
}> {
const allOriginalSeries = getAllTimeSeries(data);
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
let visibleLineIndex = 0;
const targets: Array<{
dataIndex: number;
dash: number[];
color: string;
width: number;
}> = [];
config.lines?.forEach((line) => {
if (line.show === false) return;
const dash = line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
if (isHistoricalDashedTarget) {
targets.push({
dataIndex: firstConfigLineDataIndex + visibleLineIndex,
dash: dash!,
color: line.color,
width: line.width ?? 1,
});
}
visibleLineIndex++;
});
return targets;
}
function areDashArraysEqual(a: number[], b: number[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function areOverlayLinesEqual(a: OverlayLine[], b: OverlayLine[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (
a[i].key !== b[i].key ||
a[i].color !== b[i].color ||
a[i].width !== b[i].width ||
a[i].d !== b[i].d ||
!areDashArraysEqual(a[i].dash, b[i].dash)
) {
return false;
}
}
return true;
}
function isSameClipRect(a: ClipRect | null, b: ClipRect): boolean {
return (
!!a &&
a.x === b.x &&
a.y === b.y &&
a.width === b.width &&
a.height === b.height
);
}
export function TargetDashOverlay({
uplotRef,
newData,
config,
selectedTimeWindow,
isLiveMode,
}: {
uplotRef: React.RefObject<uPlot | null>;
newData: BigGraphProps["newData"];
config: GraphConfig;
selectedTimeWindow: number | "all";
isLiveMode: boolean;
}) {
const targetMeta = useMemo(
() => getHistoricalDashTargets(newData, config),
[newData, config],
);
const clipPathId = useId().replace(/:/g, "");
const [lines, setLines] = useState<OverlayLine[]>([]);
const [clipRect, setClipRect] = useState<ClipRect | null>(null);
useEffect(() => {
if (targetMeta.length === 0) {
setLines([]);
setClipRect(null);
}
}, [targetMeta]);
useEffect(() => {
let rafId: number | null = null;
let bootstrapRafId: number | null = null;
let recalcScheduled = false;
let hooksAttached = false;
const removeHookFns: Array<() => void> = [];
let resizeObserver: ResizeObserver | null = null;
const recalc = () => {
recalcScheduled = false;
const u = uplotRef.current;
if (!u || targetMeta.length === 0) {
return;
}
const xData = u.data[0] as number[] | undefined;
if (!xData || xData.length < 2) {
return;
}
const nextLines = targetMeta
.map((meta, index) => {
const yData = u.data[meta.dataIndex] as
| Array<number | null>
| undefined;
if (!yData || yData.length < 2) return null;
const useFlatShortWindowPath =
isLiveMode &&
selectedTimeWindow !== "all" &&
selectedTimeWindow <= 60_000;
const d = useFlatShortWindowPath
? buildFlatLatestValuePath(u, yData)
: buildSteppedPath(u, xData, yData);
if (!d) return null;
return {
key: `${meta.dataIndex}-${index}`,
color: meta.color,
width: meta.width,
dash: meta.dash,
d,
} as OverlayLine;
})
.filter((line): line is OverlayLine => !!line);
if (nextLines.length > 0) {
setLines((prev) =>
areOverlayLinesEqual(prev, nextLines) ? prev : nextLines,
);
const nextClipRect: ClipRect = {
x: u.bbox.left,
y: u.bbox.top,
width: u.bbox.width,
height: u.bbox.height,
};
setClipRect((prev) =>
isSameClipRect(prev, nextClipRect) ? prev : nextClipRect,
);
}
};
const scheduleRecalc = () => {
if (recalcScheduled) return;
recalcScheduled = true;
rafId = window.requestAnimationFrame(recalc);
};
scheduleRecalc();
if (targetMeta.length === 0) {
return;
}
const addHook = (u: any, hookName: string, fn: () => void) => {
const hooks = u.hooks?.[hookName];
if (!Array.isArray(hooks)) return;
hooks.push(fn);
removeHookFns.push(() => {
const idx = hooks.indexOf(fn);
if (idx >= 0) hooks.splice(idx, 1);
});
};
const onWindowResize = () => scheduleRecalc();
window.addEventListener("resize", onWindowResize);
const attachHooks = (): boolean => {
if (hooksAttached) return true;
const u = uplotRef.current as any;
if (!u) return false;
addHook(u, "draw", scheduleRecalc);
addHook(u, "setScale", scheduleRecalc);
addHook(u, "setData", scheduleRecalc);
const rootElement = u.root as HTMLElement | undefined;
if (rootElement && typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(() => scheduleRecalc());
resizeObserver.observe(rootElement);
}
hooksAttached = true;
scheduleRecalc();
return true;
};
if (!attachHooks()) {
const tryAttach = () => {
if (attachHooks()) return;
bootstrapRafId = window.requestAnimationFrame(tryAttach);
};
bootstrapRafId = window.requestAnimationFrame(tryAttach);
}
return () => {
if (rafId !== null) {
window.cancelAnimationFrame(rafId);
}
if (bootstrapRafId !== null) {
window.cancelAnimationFrame(bootstrapRafId);
}
removeHookFns.forEach((fn) => fn());
window.removeEventListener("resize", onWindowResize);
resizeObserver?.disconnect();
};
}, [uplotRef, targetMeta, isLiveMode, selectedTimeWindow]);
if (lines.length === 0) {
return null;
}
return (
<svg
className="pointer-events-none absolute inset-0 h-full w-full"
aria-hidden
>
{clipRect && (
<defs>
<clipPath id={clipPathId}>
<rect
x={clipRect.x}
y={clipRect.y}
width={clipRect.width}
height={clipRect.height}
/>
</clipPath>
</defs>
)}
<g clipPath={clipRect ? `url(#${clipPathId})` : undefined}>
{lines.map((line) => {
return (
<path
key={line.key}
d={line.d}
fill="none"
stroke={line.color}
strokeWidth={line.width}
strokeLinecap="butt"
strokeDasharray={line.dash.join(" ")}
strokeDashoffset={0}
/>
);
})}
</g>
</svg>
);
}
+3 -16
View File
@@ -5,10 +5,8 @@ import {
AnimationRefs,
AnimationState,
BigGraphProps,
GraphLine,
SeriesData,
} from "./types";
import { alignTargetSeriesToTimestamps } from "@/lib/timeseries";
export function useAnimationRefs(): AnimationRefs {
const animationFrameRef = useRef<number | null>(null);
@@ -52,9 +50,7 @@ export function buildUPlotData(
values: number[],
realPointsCount: number | undefined,
realPointsCountRef: React.RefObject<number>,
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
config: { lines?: Array<{ show?: boolean; value: number }> },
allSeriesData?: number[][],
): uPlot.AlignedData {
const uData: uPlot.AlignedData = [timestamps];
@@ -76,14 +72,7 @@ export function buildUPlotData(
// Add config lines
config.lines?.forEach((line) => {
if (line.show !== false) {
const lineData = line.targetSeries
? alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
)
: timestamps.map(() => line.value);
uData.push(lineData);
uData.push(timestamps.map(() => line.value));
}
});
@@ -99,9 +88,7 @@ export function animateNewPoint(
viewMode: string,
selectedTimeWindow: number | "all",
startTimeRef: React.RefObject<number | null>,
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
config: { lines?: Array<{ show?: boolean; value: number }> },
updateYAxisScale: (xMin?: number, xMax?: number) => void, // Updated signature
getAllSeriesData?: () => number[][],
): void {
+5 -22
View File
@@ -1,9 +1,5 @@
import uPlot from "uplot";
import {
seriesToUPlotData,
TimeSeries,
alignTargetSeriesToTimestamps,
} from "@/lib/timeseries";
import { seriesToUPlotData, TimeSeries } from "@/lib/timeseries";
import {
createEventHandlers,
attachEventHandlers,
@@ -63,13 +59,7 @@ export function createChart({
// Add config lines data
config.lines?.forEach((line) => {
if (line.show !== false) {
const lineData = line.targetSeries
? alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
)
: new Array(timestamps.length).fill(line.value);
const lineData = new Array(timestamps.length).fill(line.value);
uPlotData.push(lineData as any);
}
});
@@ -179,16 +169,11 @@ export function createChart({
// Add config lines
config.lines?.forEach((line) => {
if (line.show !== false) {
const dash =
line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
seriesConfig.push({
label: line.label,
// Historical dashed targets are rendered in an SVG overlay.
stroke: isHistoricalDashedTarget ? "rgba(0,0,0,0)" : line.color,
stroke: line.color,
width: line.width ?? 1,
dash: isHistoricalDashedTarget ? undefined : dash,
dash: line.dash ?? (line.type === "threshold" ? [5, 5] : undefined),
show: true,
points: { show: false },
});
@@ -527,9 +512,7 @@ export function createChart({
values: [...primaryValues],
};
return () => {
cleanup();
};
return cleanup;
}
// Helper function to get all valid TimeSeries from DataSeries
@@ -3,12 +3,7 @@ import { useRef, useCallback } from "react";
import uPlot from "uplot";
import { seriesToUPlotData } from "@/lib/timeseries";
import { getPrimarySeriesData, stopAnimations } from "./animation";
import {
BigGraphProps,
HistoricalModeHandlers,
AnimationRefs,
SwitchOrigin,
} from "./types";
import { BigGraphProps, HistoricalModeHandlers, AnimationRefs } from "./types";
export function useHistoricalMode({
newData,
@@ -125,43 +120,12 @@ export function useHistoricalMode({
);
// Switch to historical mode
const switchToHistoricalMode = useCallback(
(origin?: SwitchOrigin) => {
isInHistoricalModeRef.current = true;
// Preserve current scale
if (origin === "gesture") {
try {
if (uplotRef.current) {
const xScale = uplotRef.current.scales.x;
const yScale = uplotRef.current.scales.y;
manualScaleRef.current = {
x: {
min: xScale?.min ?? 0,
max: xScale?.max ?? 0,
},
y: {
min: yScale?.min ?? 0,
max: yScale?.max ?? 1,
},
};
}
} catch (err) {
console.warn(
"Error preserving chart scales when switching to historical mode:",
err,
);
}
} else {
manualScaleRef.current = null;
}
captureHistoricalFreezeTimestamp();
stopAnimations(animationRefs);
lastProcessedCountRef.current = 0; // Reset processed count
},
[captureHistoricalFreezeTimestamp, animationRefs, lastProcessedCountRef],
);
const switchToHistoricalMode = useCallback(() => {
isInHistoricalModeRef.current = true;
captureHistoricalFreezeTimestamp();
stopAnimations(animationRefs);
lastProcessedCountRef.current = 0; // Reset processed count
}, [captureHistoricalFreezeTimestamp, animationRefs, lastProcessedCountRef]);
// Switch back to live mode
const switchToLiveMode = useCallback(() => {
+2 -6
View File
@@ -3,8 +3,6 @@ import { Unit } from "@/control/units";
import { TimeSeries } from "@/lib/timeseries";
import { RefObject } from "react";
export type SwitchOrigin = "button" | "gesture";
// Prop-based sync types
export type PropGraphSync = {
timeWindow: number | "all";
@@ -13,7 +11,6 @@ export type PropGraphSync = {
xRange?: { min: number; max: number };
historicalFreezeTimestamp?: number | null;
showFromTimestamp?: number | null;
historicalSwitchOrigin: SwitchOrigin;
onTimeWindowChange?: (graphId: string, timeWindow: number | "all") => void;
onViewModeChange?: (
graphId: string,
@@ -30,7 +27,6 @@ export type PropGraphSync = {
export type GraphLine = {
type: "threshold" | "target";
value: number;
targetSeries?: TimeSeries;
color: string;
label?: string;
width?: number;
@@ -87,7 +83,7 @@ export type ControlProps = {
isLiveMode: boolean;
onTimeWindowChange: (timeWindow: number | "all") => void;
onSwitchToLive: () => void;
onSwitchToHistorical: (origin: SwitchOrigin) => void;
onSwitchToHistorical: () => void;
onExport?: () => void;
timeWindowOptions?: TimeWindowOption[];
showFromTimestamp?: number | null;
@@ -104,7 +100,7 @@ export interface HistoricalModeHandlers {
captureHistoricalFreezeTimestamp: () => number;
getHistoricalEndTimestamp: () => number;
handleHistoricalTimeWindow: (timeWindow: number | "all") => void;
switchToHistoricalMode: (origin?: SwitchOrigin) => void;
switchToHistoricalMode: () => void;
switchToLiveMode: () => void;
}
@@ -175,7 +175,7 @@ export function useBigGraphEffects({
const newIsLiveMode = syncGraph.isLiveMode;
if (lastState.isLiveMode && !newIsLiveMode) {
historicalMode.switchToHistoricalMode(syncGraph.historicalSwitchOrigin);
historicalMode.switchToHistoricalMode();
} else if (!lastState.isLiveMode && newIsLiveMode) {
historicalMode.switchToLiveMode();
+1 -10
View File
@@ -1,5 +1,5 @@
import { useState, useCallback, useRef } from "react";
import { PropGraphSync, SwitchOrigin } from "./types";
import { PropGraphSync } from "./types";
import { GraphExportData, exportGraphsToExcel } from "./excelExport";
import { useGraphSettingsStore } from "@/stores/graphSettingsStore";
@@ -19,7 +19,6 @@ export function useGraphSync(exportGroupId?: string) {
null,
);
const historicalSwitchOriginRef = useRef<SwitchOrigin>("gesture");
const historicalFreezeTimestampRef = useRef<number | null>(null);
const graphDataRef = useRef<Map<string, () => GraphExportData | null>>(
new Map(),
@@ -66,7 +65,6 @@ export function useGraphSync(exportGroupId?: string) {
showFromTimestamp?: number | null;
clearHistoricalFreeze?: boolean;
setHistoricalFreeze?: boolean;
historicalSwitchOrigin?: SwitchOrigin;
},
requestId?: number,
) => {
@@ -106,10 +104,6 @@ export function useGraphSync(exportGroupId?: string) {
historicalFreezeTimestampRef.current = Date.now();
}
if (updates.historicalSwitchOrigin !== undefined) {
historicalSwitchOriginRef.current = updates.historicalSwitchOrigin;
}
clearChangeSource();
});
},
@@ -170,7 +164,6 @@ export function useGraphSync(exportGroupId?: string) {
viewMode: "manual",
isLiveMode: false,
setHistoricalFreeze: historicalFreezeTimestampRef.current === null,
historicalSwitchOrigin: "gesture",
});
},
[updateSyncState],
@@ -217,7 +210,6 @@ export function useGraphSync(exportGroupId?: string) {
isLiveMode: false,
viewMode: "manual",
setHistoricalFreeze: true,
historicalSwitchOrigin: "button",
});
}, [updateSyncState]);
@@ -250,7 +242,6 @@ export function useGraphSync(exportGroupId?: string) {
xRange,
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
showFromTimestamp,
historicalSwitchOrigin: historicalSwitchOriginRef.current,
onTimeWindowChange: handleTimeWindowChange,
onViewModeChange: handleViewModeChange,
onZoomChange: handleZoomChangeThrottled,
@@ -1,3 +1,5 @@
export const TROUBLESHOOT_REBOOT_HMI = "troubleshoot:reboot-hmi";
export const TROUBLESHOOT_RESTART_BACKEND = "troubleshoot:restart-backend";
export const TROUBLESHOOT_EXPORT_LOGS = "troubleshoot:export-logs";
export const TROUBLESHOOT_START_LOG_STREAM = "troubleshoot:start-log-stream";
export const TROUBLESHOOT_STOP_LOG_STREAM = "troubleshoot:stop-log-stream";
export const TROUBLESHOOT_LOG_DATA = "troubleshoot:log-data";
@@ -1,7 +1,9 @@
import {
TROUBLESHOOT_REBOOT_HMI,
TROUBLESHOOT_RESTART_BACKEND,
TROUBLESHOOT_EXPORT_LOGS,
TROUBLESHOOT_START_LOG_STREAM,
TROUBLESHOOT_STOP_LOG_STREAM,
TROUBLESHOOT_LOG_DATA,
} from "./troubleshoot-channels";
export function exposeTroubleshootContext() {
@@ -9,6 +11,11 @@ export function exposeTroubleshootContext() {
contextBridge.exposeInMainWorld("troubleshoot", {
rebootHmi: () => ipcRenderer.invoke(TROUBLESHOOT_REBOOT_HMI),
restartBackend: () => ipcRenderer.invoke(TROUBLESHOOT_RESTART_BACKEND),
exportLogs: () => ipcRenderer.invoke(TROUBLESHOOT_EXPORT_LOGS),
startLogStream: () => ipcRenderer.invoke(TROUBLESHOOT_START_LOG_STREAM),
stopLogStream: () => ipcRenderer.invoke(TROUBLESHOOT_STOP_LOG_STREAM),
onLogData: (callback: (log: string) => void) =>
ipcRenderer.on(TROUBLESHOOT_LOG_DATA, (_event, log: string) => {
callback(log);
}),
});
}
@@ -1,13 +1,14 @@
import { ipcMain, dialog } from "electron";
import { spawn, exec, ChildProcess } from "child_process";
import { ipcMain } from "electron";
import { spawn, ChildProcess } from "child_process";
import {
TROUBLESHOOT_REBOOT_HMI,
TROUBLESHOOT_RESTART_BACKEND,
TROUBLESHOOT_EXPORT_LOGS,
TROUBLESHOOT_START_LOG_STREAM,
TROUBLESHOOT_STOP_LOG_STREAM,
TROUBLESHOOT_LOG_DATA,
} from "./troubleshoot-channels";
import fs from "fs";
import path from "path";
let logStreamProcess: ChildProcess | null = null;
export function addTroubleshootEventListeners() {
ipcMain.handle(TROUBLESHOOT_REBOOT_HMI, async () => {
@@ -59,45 +60,62 @@ export function addTroubleshootEventListeners() {
}
});
ipcMain.handle(TROUBLESHOOT_EXPORT_LOGS, async () => {
ipcMain.handle(TROUBLESHOOT_START_LOG_STREAM, async (event) => {
try {
const now = new Date();
const datePart = now.toISOString().split("T")[0]; // YYYY-MM-DD
const timePart = now.toTimeString().split(" ")[0].replace(/:/g, "-"); // HH-mm-ss
const fileName = `journal_${datePart}_${timePart}.log`;
// 1. Open a Save Dialog so the user can choose the location
const { canceled, filePath } = await dialog.showSaveDialog({
title: "Export System Logs",
defaultPath: fileName,
filters: [{ name: "Log Files", extensions: ["log"] }],
});
if (canceled || !filePath) {
return { success: false, error: "Export cancelled by user" };
// Stop any existing log stream
if (logStreamProcess) {
logStreamProcess.kill();
logStreamProcess = null;
}
// 2. Wrap the exec in a typed Promise to match the backend restart pattern
// This resolves the TS2794 error by explicitly defining the return type
return await new Promise<{ success: boolean; error?: string }>(
(resolve) => {
// Note: journalctl -xb usually requires sudo or journal group membership
exec(`journalctl -xb > "${filePath}"`, (error, stdout, stderr) => {
if (error) {
console.error("Exec error:", error);
resolve({
success: false,
error: error instanceof Error ? error.message : String(error),
});
return;
}
resolve({ success: true });
});
logStreamProcess = spawn(
"journalctl",
["-u", "qitech-control-server", "-n", "10000", "-f"],
{
shell: true,
},
);
logStreamProcess.stdout?.on("data", (data) => {
const logLine = data.toString();
event.sender.send(TROUBLESHOOT_LOG_DATA, logLine);
});
logStreamProcess.stderr?.on("data", (data) => {
const logLine = data.toString();
event.sender.send(TROUBLESHOOT_LOG_DATA, logLine);
});
logStreamProcess.on("close", (code) => {
console.log(`Log stream process exited with code ${code}`);
logStreamProcess = null;
});
logStreamProcess.on("error", (error) => {
console.error("Log stream process error:", error);
logStreamProcess = null;
});
return { success: true };
} catch (error) {
console.error("Failed to export logs: ", error);
console.error("Failed to start log stream:", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
});
ipcMain.handle(TROUBLESHOOT_STOP_LOG_STREAM, async () => {
try {
if (logStreamProcess) {
logStreamProcess.kill();
logStreamProcess = null;
console.log("Log stream stopped");
}
return { success: true };
} catch (error) {
console.error("Failed to stop log stream:", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
+32 -3
View File
@@ -39,12 +39,12 @@ export async function restartBackend(): Promise<{
}
}
export async function exportLogs(): Promise<{
export async function startLogStream(): Promise<{
success: boolean;
error?: string;
}> {
try {
return await window.troubleshoot.exportLogs();
return await window.troubleshoot.startLogStream();
} catch (error) {
return {
success: false,
@@ -53,6 +53,35 @@ export async function exportLogs(): Promise<{
}
}
export async function stopLogStream(): Promise<{
success: boolean;
error?: string;
}> {
try {
return await window.troubleshoot.stopLogStream();
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Setup log data streaming with callback
* @param onLogData Callback function to handle incoming log data
*/
export function setupLogDataListener(onLogData: (log: string) => void): void {
window.troubleshoot.onLogData(onLogData);
}
/**
* Cleanup log data listener
*/
export function cleanupLogDataListener(): void {
window.troubleshoot.onLogData(() => {});
}
/**
* Combined function to restart backend and show appropriate toast messages
*/
@@ -70,10 +99,10 @@ export async function restartBackendWithToast(): Promise<boolean> {
/**
* Combined function to reboot HMI and show appropriate toast messages
* ?????? How would you see this?
*/
export async function rebootHmiWithToast(): Promise<boolean> {
const result = await rebootHmi();
if (result.success) {
return true;
} else {
@@ -0,0 +1,78 @@
import { useEffect } from "react";
import { useLogsStore } from "@/stores/logsStore";
import {
startLogStream,
stopLogStream,
setupLogDataListener,
isTroubleshootAvailable,
} from "@/helpers/troubleshoot_helpers";
/**
* Hook to manage global log streaming
* This should be used once at the app level to start log streaming
*/
export function useGlobalLogStreaming() {
const { isStreaming, addLogEntry, setStreaming } = useLogsStore();
useEffect(() => {
// Only proceed if troubleshoot context is available
if (!isTroubleshootAvailable()) {
return;
}
if (!isStreaming) {
startLogStreamHelper();
}
// Setup log data listener
const handleLogData = (logData: string) => {
// Parse systemd journal log format
const lines = logData.split("\n").filter((line) => line.trim());
lines.forEach((line) => {
if (line.trim()) {
// Simple log level detection
let level: "info" | "warn" | "error" | "debug" = "info";
if (line.includes("ERROR") || line.includes("error")) level = "error";
else if (line.includes("WARN") || line.includes("warn"))
level = "warn";
else if (line.includes("DEBUG") || line.includes("debug"))
level = "debug";
addLogEntry({
source: "qitech-control-server",
level,
message: line.trim(),
raw: line,
});
}
});
};
// Setup log data listener using helper
setupLogDataListener(handleLogData);
return () => {
// Cleanup on unmount
if (isStreaming && isTroubleshootAvailable()) {
stopLogStream();
setStreaming(false);
}
};
}, [isStreaming, addLogEntry, setStreaming]);
const startLogStreamHelper = async () => {
try {
const result = await startLogStream();
if (result.success) {
setStreaming(true);
} else {
console.warn("Failed to start log stream:", result.error);
}
} catch (error) {
console.warn("Failed to start log stream:", error);
}
};
return { isStreaming };
}
-43
View File
@@ -304,46 +304,3 @@ export function resetSeries(series: Series): void {
series.lastTimestamp = 0;
series.validCount = 0;
}
/**
* Aligns target series values with a given set of timestamps.
* For each timestamp in dataTimestamps, finds the most recent target value
* that was set before or at that timestamp (step function interpolation).
*/
export function alignTargetSeriesToTimestamps(
targetSeries: TimeSeries,
dataTimestamps: number[],
fallbackValue: number,
): number[] {
if (dataTimestamps.length === 0) {
return [];
}
const [targetTimestamps, targetValues] = extractDataFromSeries(
targetSeries.long,
);
if (targetTimestamps.length === 0) {
return new Array(dataTimestamps.length).fill(fallbackValue);
}
const result: number[] = [];
let targetIndex = 0;
for (const dataTs of dataTimestamps) {
while (
targetIndex < targetTimestamps.length - 1 &&
targetTimestamps[targetIndex + 1] <= dataTs
) {
targetIndex++;
}
if (targetTimestamps[targetIndex] > dataTs) {
result.push(fallbackValue);
} else {
result.push(targetValues[targetIndex]);
}
}
return result;
}
+14 -17
View File
@@ -16,7 +16,6 @@ type Props = {
min: number;
max: number;
onChangeTargetTemp?: (temperature: number) => void;
targetTemperatureEnabled: boolean;
};
export function HeatingZone({
title,
@@ -26,7 +25,6 @@ export function HeatingZone({
heatingTimeSeries,
heatingPower,
onChangeTargetTemp,
targetTemperatureEnabled,
}: Props) {
const targetTemperature = heatingState?.target_temperature ?? 150;
@@ -43,20 +41,19 @@ export function HeatingZone({
timeseries={heatingTimeSeries}
/>
</div>
{targetTemperatureEnabled && (
<Label label="Target Temperature">
<EditValue
value={targetTemperature}
defaultValue={150}
min={min}
max={max}
unit="C"
title="Target Temperature"
renderValue={(value) => roundToDecimals(value, 1)}
onChange={onChangeTargetTemp}
/>
</Label>
)}
<Label label="Target Temperature">
<EditValue
value={targetTemperature}
defaultValue={150}
min={min}
max={max}
unit="C"
title="Target Temperature"
renderValue={(value) => roundToDecimals(value, 1)}
onChange={onChangeTargetTemp}
/>
</Label>
</div>
<TimeSeriesValueNumeric
@@ -68,7 +65,7 @@ export function HeatingZone({
timeseries={heatingPower}
/>
{heatingState?.wiring_error && targetTemperatureEnabled && (
{heatingState?.wiring_error && (
<StatusBadge variant="error">
Cant Read Temperature! Check Temperature Sensor Wiring!
</StatusBadge>
@@ -68,12 +68,10 @@ export function Extruder2ControlPage() {
backTemperature.current?.value ?? 0,
state?.heating_states.back.target_temperature ?? 1,
);
const nozzleReady =
!state?.extruder_settings_state.nozzle_temperature_target_enabled ||
isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
const nozzleReady = isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
return frontReady && middleReady && backReady && nozzleReady;
};
@@ -89,7 +87,6 @@ export function Extruder2ControlPage() {
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Middle"}
@@ -99,7 +96,6 @@ export function Extruder2ControlPage() {
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Back"}
@@ -109,7 +105,6 @@ export function Extruder2ControlPage() {
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Nozzle"}
@@ -119,10 +114,6 @@ export function Extruder2ControlPage() {
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={
state?.extruder_settings_state.nozzle_temperature_target_enabled ??
true
}
/>
<ControlCard className="bg-red" title="Screw Drive">
{state?.inverter_status_state.overload_warning == true ? (
@@ -24,13 +24,6 @@ export function Extruder2GraphsPage() {
motorCurrent,
motorPower,
combinedPower,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder2();
const syncHook = useGraphSync("extruder-graphs");
@@ -56,7 +49,6 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
targetSeries: targetNozzleTemperature,
color: "#ef4444",
show: true,
},
@@ -77,7 +69,6 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
targetSeries: targetFrontTemperature,
color: "#f59e0b",
show: true,
},
@@ -98,7 +89,6 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
targetSeries: targetMiddleTemperature,
color: "#8b5cf6",
show: true,
},
@@ -119,7 +109,6 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
targetSeries: targetBackTemperature,
color: "#3b82f6",
show: true,
},
@@ -264,7 +253,6 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.pressure_state.target_bar,
targetSeries: targetPressure,
color: "#3b82f6",
show: true,
},
@@ -318,7 +306,6 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.screw_state.target_rpm,
targetSeries: targetScrewRpm,
color: "#8b5cf6",
show: true,
},
@@ -20,7 +20,6 @@ export function Extruder2SettingsPage() {
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
setTemperatureTargetEnabled,
} = useExtruder2();
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -69,16 +68,6 @@ export function Extruder2SettingsPage() {
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Nozzle Temperature Target Enabled">
<SelectionGroupBoolean
value={
state?.extruder_settings_state.nozzle_temperature_target_enabled
}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setTemperatureTargetEnabled}
/>
</Label>
<Label label="Show Advanced PID Settings">
<SelectionGroupBoolean
value={showAdvanced}
@@ -109,7 +109,6 @@ export const heatingStatesSchema = z.object({
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
nozzle_temperature_target_enabled: z.boolean(),
});
/**
@@ -222,14 +221,6 @@ export type Extruder2NamespaceStore = {
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
// Target value history (for graph target lines)
targetPressure: TimeSeries;
targetScrewRpm: TimeSeries;
targetNozzleTemperature: TimeSeries;
targetFrontTemperature: TimeSeries;
targetMiddleTemperature: TimeSeries;
targetBackTemperature: TimeSeries;
};
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
@@ -262,28 +253,6 @@ const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries();
// Target value time series (for historical target lines on graphs)
const { initialTimeSeries: targetPressure, insert: addTargetPressure } =
createTimeSeries();
const { initialTimeSeries: targetScrewRpm, insert: addTargetScrewRpm } =
createTimeSeries();
const {
initialTimeSeries: targetNozzleTemperature,
insert: addTargetNozzleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetFrontTemperature,
insert: addTargetFrontTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetMiddleTemperature,
insert: addTargetMiddleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetBackTemperature,
insert: addTargetBackTemperature,
} = createTimeSeries();
export function extruder2MessageHandler(
store: StoreApi<Extruder2NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder2NamespaceStore>,
@@ -302,17 +271,6 @@ export function extruder2MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
const nextTargetPressure = stateEvent.data.pressure_state.target_bar;
const nextTargetScrewRpm = stateEvent.data.screw_state.target_rpm;
const nextTargetNozzleTemperature =
stateEvent.data.heating_states.nozzle.target_temperature;
const nextTargetFrontTemperature =
stateEvent.data.heating_states.front.target_temperature;
const nextTargetMiddleTemperature =
stateEvent.data.heating_states.middle.target_temperature;
const nextTargetBackTemperature =
stateEvent.data.heating_states.back.target_temperature;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -320,53 +278,6 @@ export function extruder2MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure:
state.targetPressure.current?.value === nextTargetPressure
? state.targetPressure
: addTargetPressure(state.targetPressure, {
value: nextTargetPressure,
timestamp,
}),
targetScrewRpm:
state.targetScrewRpm.current?.value === nextTargetScrewRpm
? state.targetScrewRpm
: addTargetScrewRpm(state.targetScrewRpm, {
value: nextTargetScrewRpm,
timestamp,
}),
targetNozzleTemperature:
state.targetNozzleTemperature.current?.value ===
nextTargetNozzleTemperature
? state.targetNozzleTemperature
: addTargetNozzleTemperature(state.targetNozzleTemperature, {
value: nextTargetNozzleTemperature,
timestamp,
}),
targetFrontTemperature:
state.targetFrontTemperature.current?.value ===
nextTargetFrontTemperature
? state.targetFrontTemperature
: addTargetFrontTemperature(state.targetFrontTemperature, {
value: nextTargetFrontTemperature,
timestamp,
}),
targetMiddleTemperature:
state.targetMiddleTemperature.current?.value ===
nextTargetMiddleTemperature
? state.targetMiddleTemperature
: addTargetMiddleTemperature(state.targetMiddleTemperature, {
value: nextTargetMiddleTemperature,
timestamp,
}),
targetBackTemperature:
state.targetBackTemperature.current?.value ===
nextTargetBackTemperature
? state.targetBackTemperature
: addTargetBackTemperature(state.targetBackTemperature, {
value: nextTargetBackTemperature,
timestamp,
}),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -467,14 +378,6 @@ export const createExtruder2NamespaceStore =
middlePower,
combinedPower,
totalEnergyKWh,
// Target value history
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
};
});
@@ -57,14 +57,6 @@ export function useExtruder2() {
backPower,
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder2Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -232,20 +224,6 @@ export function useExtruder2() {
);
};
const setTemperatureTargetEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.nozzle_temperature_target_enabled =
enabled;
},
() =>
requestNozzleTemperatureTargetEnabled({
machine_identification_unique: machineIdentification,
data: { SetNozzleTemperatureTargetEnabled: enabled },
}),
);
};
const setPressurePidKp = (kp: number) => {
updateStateOptimistically(
(current) => {
@@ -395,10 +373,6 @@ export function useExtruder2() {
z.object({ SetExtruderPressureLimitIsEnabled: z.boolean() }),
);
const { request: requestNozzleTemperatureTargetEnabled } = useMachineMutation(
z.object({ SetNozzleTemperatureTargetEnabled: z.boolean() }),
);
const { request: requestPressurePidSettings } = useMachineMutation(
z.object({
SetPressurePidSettings: z.object({
@@ -449,14 +423,6 @@ export function useExtruder2() {
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
@@ -477,7 +443,6 @@ export function useExtruder2() {
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
setTemperatureTargetEnabled,
resetInverter,
};
}
@@ -68,12 +68,10 @@ export function Extruder3ControlPage() {
backTemperature.current?.value ?? 0,
state?.heating_states.back.target_temperature ?? 1,
);
const nozzleReady =
!state?.extruder_settings_state.nozzle_temperature_target_enabled ||
isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
const nozzleReady = isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
return frontReady && middleReady && backReady && nozzleReady;
};
@@ -89,7 +87,6 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Middle"}
@@ -99,7 +96,6 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Back"}
@@ -109,7 +105,6 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
/>
<HeatingZone
title={"Heating Nozzle"}
@@ -119,10 +114,6 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={
state?.extruder_settings_state.nozzle_temperature_target_enabled ??
true
}
/>
<ControlCard className="bg-red" title="Screw Drive">
{state?.inverter_status_state.overload_warning == true ? (
@@ -24,13 +24,6 @@ export function Extruder3GraphsPage() {
motorCurrent,
motorPower,
combinedPower,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder3();
const syncHook = useGraphSync("extruder-graphs");
@@ -56,7 +49,6 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
targetSeries: targetNozzleTemperature,
color: "#ef4444",
show: true,
},
@@ -77,7 +69,6 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
targetSeries: targetFrontTemperature,
color: "#f59e0b",
show: true,
},
@@ -98,7 +89,6 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
targetSeries: targetMiddleTemperature,
color: "#8b5cf6",
show: true,
},
@@ -119,7 +109,6 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
targetSeries: targetBackTemperature,
color: "#3b82f6",
show: true,
},
@@ -264,7 +253,6 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.pressure_state.target_bar,
targetSeries: targetPressure,
color: "#3b82f6",
show: true,
},
@@ -318,7 +306,6 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.screw_state.target_rpm,
targetSeries: targetScrewRpm,
color: "#8b5cf6",
show: true,
},
@@ -20,7 +20,6 @@ export function Extruder3SettingsPage() {
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
setTemperatureTargetEnabled,
} = useExtruder3();
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -69,16 +68,6 @@ export function Extruder3SettingsPage() {
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Nozzle Temperature Target Enabled">
<SelectionGroupBoolean
value={
state?.extruder_settings_state.nozzle_temperature_target_enabled
}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setTemperatureTargetEnabled}
/>
</Label>
<Label label="Show Advanced PID Settings">
<SelectionGroupBoolean
value={showAdvanced}
@@ -109,7 +109,6 @@ export const heatingStatesSchema = z.object({
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
nozzle_temperature_target_enabled: z.boolean(),
});
/**
@@ -222,14 +221,6 @@ export type Extruder3NamespaceStore = {
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
// Target value history (for graph target lines)
targetPressure: TimeSeries;
targetScrewRpm: TimeSeries;
targetNozzleTemperature: TimeSeries;
targetFrontTemperature: TimeSeries;
targetMiddleTemperature: TimeSeries;
targetBackTemperature: TimeSeries;
};
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
@@ -262,28 +253,6 @@ const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries();
// Target value time series (for historical target lines on graphs)
const { initialTimeSeries: targetPressure, insert: addTargetPressure } =
createTimeSeries();
const { initialTimeSeries: targetScrewRpm, insert: addTargetScrewRpm } =
createTimeSeries();
const {
initialTimeSeries: targetNozzleTemperature,
insert: addTargetNozzleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetFrontTemperature,
insert: addTargetFrontTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetMiddleTemperature,
insert: addTargetMiddleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetBackTemperature,
insert: addTargetBackTemperature,
} = createTimeSeries();
export function extruder3MessageHandler(
store: StoreApi<Extruder3NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder3NamespaceStore>,
@@ -302,17 +271,6 @@ export function extruder3MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
const nextTargetPressure = stateEvent.data.pressure_state.target_bar;
const nextTargetScrewRpm = stateEvent.data.screw_state.target_rpm;
const nextTargetNozzleTemperature =
stateEvent.data.heating_states.nozzle.target_temperature;
const nextTargetFrontTemperature =
stateEvent.data.heating_states.front.target_temperature;
const nextTargetMiddleTemperature =
stateEvent.data.heating_states.middle.target_temperature;
const nextTargetBackTemperature =
stateEvent.data.heating_states.back.target_temperature;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -320,53 +278,6 @@ export function extruder3MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure:
state.targetPressure.current?.value === nextTargetPressure
? state.targetPressure
: addTargetPressure(state.targetPressure, {
value: nextTargetPressure,
timestamp,
}),
targetScrewRpm:
state.targetScrewRpm.current?.value === nextTargetScrewRpm
? state.targetScrewRpm
: addTargetScrewRpm(state.targetScrewRpm, {
value: nextTargetScrewRpm,
timestamp,
}),
targetNozzleTemperature:
state.targetNozzleTemperature.current?.value ===
nextTargetNozzleTemperature
? state.targetNozzleTemperature
: addTargetNozzleTemperature(state.targetNozzleTemperature, {
value: nextTargetNozzleTemperature,
timestamp,
}),
targetFrontTemperature:
state.targetFrontTemperature.current?.value ===
nextTargetFrontTemperature
? state.targetFrontTemperature
: addTargetFrontTemperature(state.targetFrontTemperature, {
value: nextTargetFrontTemperature,
timestamp,
}),
targetMiddleTemperature:
state.targetMiddleTemperature.current?.value ===
nextTargetMiddleTemperature
? state.targetMiddleTemperature
: addTargetMiddleTemperature(state.targetMiddleTemperature, {
value: nextTargetMiddleTemperature,
timestamp,
}),
targetBackTemperature:
state.targetBackTemperature.current?.value ===
nextTargetBackTemperature
? state.targetBackTemperature
: addTargetBackTemperature(state.targetBackTemperature, {
value: nextTargetBackTemperature,
timestamp,
}),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -467,14 +378,6 @@ export const createExtruder3NamespaceStore =
middlePower,
combinedPower,
totalEnergyKWh,
// Target value history
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
};
});
@@ -57,14 +57,6 @@ export function useExtruder3() {
backPower,
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder3Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -232,28 +224,6 @@ export function useExtruder3() {
);
};
const setTemperatureTargetEnabled = (enabled: boolean) => {
const res = enabled
? true
: confirm(
"This will disable the ability to set a temperature target for the nozzle heating element and will any wiring error message. Only proceed if you understand the risks.",
);
if (res) {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.nozzle_temperature_target_enabled =
enabled;
},
() =>
requestNozzleTemperatureTargetEnabled({
machine_identification_unique: machineIdentification,
data: { SetNozzleTemperatureTargetEnabled: enabled },
}),
);
}
};
const setPressurePidKp = (kp: number) => {
updateStateOptimistically(
(current) => {
@@ -403,10 +373,6 @@ export function useExtruder3() {
z.object({ SetExtruderPressureLimitIsEnabled: z.boolean() }),
);
const { request: requestNozzleTemperatureTargetEnabled } = useMachineMutation(
z.object({ SetNozzleTemperatureTargetEnabled: z.boolean() }),
);
const { request: requestPressurePidSettings } = useMachineMutation(
z.object({
SetPressurePidSettings: z.object({
@@ -457,14 +423,6 @@ export function useExtruder3() {
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
@@ -481,7 +439,6 @@ export function useExtruder3() {
setMiddleHeatingTemperature,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setTemperatureTargetEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
@@ -10,17 +10,10 @@ import React from "react";
import { useLaser1 } from "./useLaser1";
export function Laser1GraphsPage() {
const {
diameter,
x_diameter,
y_diameter,
roundness,
state,
targetDiameter: targetDiameterSeries,
} = useLaser1();
const { diameter, x_diameter, y_diameter, roundness, state } = useLaser1();
const syncHook = useGraphSync("diameter-roundness-group");
const targetDiameterValue = state?.laser_state?.target_diameter ?? 0;
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
@@ -78,22 +71,21 @@ export function Laser1GraphsPage() {
// Tolerance for diameter
{
type: "threshold" as const,
value: targetDiameterValue + higherTolerance,
value: targetDiameter + higherTolerance,
label: "Upper Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "threshold" as const,
value: targetDiameterValue - lowerTolerance,
value: targetDiameter - lowerTolerance,
label: "Lower Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "target" as const,
value: targetDiameterValue,
targetSeries: targetDiameterSeries,
value: targetDiameter,
label: "Target",
color: diameterColor,
},
@@ -64,7 +64,6 @@ export type Laser1NamespaceStore = {
x_diameter: TimeSeries;
y_diameter: TimeSeries;
roundness: TimeSeries;
targetDiameter: TimeSeries;
};
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries();
@@ -74,8 +73,6 @@ const { initialTimeSeries: y_diameter, insert: addYDiameter } =
createTimeSeries();
const { initialTimeSeries: roundness, insert: addRoundness } =
createTimeSeries();
const { initialTimeSeries: targetDiameter, insert: addTargetDiameter } =
createTimeSeries();
/**
* Factory function to create a new Laser1 namespace store
@@ -90,7 +87,6 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
x_diameter,
y_diameter,
roundness,
targetDiameter,
};
});
@@ -118,17 +114,9 @@ export function laser1MessageHandler(
// Apply appropriate caching strategy based on event type
if (eventName === "StateEvent") {
const stateEvent = stateEventSchema.parse(event);
const nextTargetDiameter = stateEvent.data.laser_state.target_diameter;
updateStore((state) => ({
...state,
state: stateEvent,
targetDiameter:
state.targetDiameter.current?.value === nextTargetDiameter
? state.targetDiameter
: addTargetDiameter(state.targetDiameter, {
value: nextTargetDiameter,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
@@ -11,15 +11,8 @@ import { produce } from "immer";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const {
state,
defaultState,
diameter,
x_diameter,
y_diameter,
roundness,
targetDiameter,
} = useLaser1Namespace(machine_identification_unique);
const { state, defaultState, diameter, x_diameter, y_diameter, roundness } =
useLaser1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
@@ -120,7 +113,6 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
x_diameter,
y_diameter,
roundness,
targetDiameter,
// Loading states
isLoading: stateOptimistic.isOptimistic,
-36
View File
@@ -655,41 +655,6 @@ export const wagoAiTestMachine: MachineProperties = {
],
};
export const testmachinestepper: MachineProperties = {
name: "TestMachineStepper",
version: "V1",
slug: "testmachinestepper",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0037,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 0x00000021,
product_id: 0x07500354,
revision: 0x2,
},
],
},
{
role: 1,
role_label: "Stepper Motor",
allowed_devices: [
{
vendor_id: 0x00000021,
product_id: 0x67214e8,
revision: 0x2,
},
],
},
],
};
export const machineProperties: MachineProperties[] = [
winder2,
extruder3,
@@ -707,7 +672,6 @@ export const machineProperties: MachineProperties[] = [
ip20TestMachine,
wagoDoTestMachine,
TestMotor,
testmachinestepper,
];
export const getMachineProperties = (
@@ -1,130 +0,0 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { useTestMachineStepper } from "./useTestMachineStepper";
import { Label } from "@/control/Label";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { TouchButton } from "@/components/touch/TouchButton";
export function TestMachineStepperControlPage() {
const { state, setTargetSpeed, setEnabled, setFreq, setAccFreq } =
useTestMachineStepper();
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Stepper Speed">
<Label label="Target Speed">
<EditValue
value={state?.target_speed}
unit="rpm"
title="Target Speed"
defaultValue={0}
min={-25000}
max={25000}
step={1}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setTargetSpeed}
/>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setEnabled(true)}
disabled={false}
isLoading={false}
>
Enable
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setEnabled(false)}
disabled={false}
isLoading={false}
>
Disable
</TouchButton>
</Label>
</ControlCard>
<ControlCard height={2} title="Frequency Prescaler">
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(0)}
disabled={false}
isLoading={false}
>
Default (00)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(1)}
disabled={false}
isLoading={false}
>
Low (01)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(2)}
disabled={false}
isLoading={false}
>
Mid (10)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setFreq(3)}
disabled={false}
isLoading={false}
>
High (11)
</TouchButton>
</ControlCard>
<ControlCard height={2} title="Acceleration Factor">
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(0)}
disabled={false}
isLoading={false}
>
Default (00)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(1)}
disabled={false}
isLoading={false}
>
Low (01)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(2)}
disabled={false}
isLoading={false}
>
Mid (10)
</TouchButton>
<TouchButton
variant="outline"
icon="lu:CirclePower"
onClick={() => setAccFreq(3)}
disabled={false}
isLoading={false}
>
High (11)
</TouchButton>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -1,20 +0,0 @@
import { Topbar } from "@/components/Topbar";
import { testMachineStepperSerialRoute } from "@/routes/routes";
import React from "react";
export function TestMachineStepperPage() {
const { serial } = testMachineStepperSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/testmachinestepper/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -1,81 +0,0 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "@/client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
target_speed: z.number(),
enabled: z.boolean(),
freq: z.number(),
acc_freq: z.number(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type TestMachineStepperNamespaceStore = {
state: StateEvent | null;
};
export const createTestMachineStepperNamespaceStore =
(): StoreApi<TestMachineStepperNamespaceStore> =>
create<TestMachineStepperNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function testMachineStepperMessageHandler(
store: StoreApi<TestMachineStepperNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<TestMachineStepperNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (
state: TestMachineStepperNamespaceStore,
) => TestMachineStepperNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useTestMachineStepperNamespaceImplementation =
createNamespaceHookImplementation<TestMachineStepperNamespaceStore>({
createStore: createTestMachineStepperNamespaceStore,
createEventHandler: testMachineStepperMessageHandler,
});
export function useTestMachineStepperNamespace(
machine_identification_unique: MachineIdentificationUnique,
): TestMachineStepperNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useTestMachineStepperNamespaceImplementation(namespaceId);
}
@@ -1,126 +0,0 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMachineStepperSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import {
useTestMachineStepperNamespace,
StateEvent,
} from "./testMachineStepperNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { testmachinestepper } from "@/machines/properties";
import { z } from "zod";
export function useTestMachineStepper() {
const { serial: serialString } = testMachineStepperSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: testmachinestepper.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMachineStepperNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
action: z.string(),
value: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const setTargetSpeed = (target: number) => {
updateStateOptimistically(
(current) => {
current.target_speed = target;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetTargetSpeed", value: { target } },
}),
);
};
const setEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.enabled = enabled;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetEnabled", value: { enabled } },
}),
);
};
const setFreq = (factor: number) => {
updateStateOptimistically(
(current) => {
current.freq = factor;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetFreq", value: { factor } },
}),
);
};
const setAccFreq = (factor: number) => {
updateStateOptimistically(
(current) => {
current.acc_freq = factor;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetAccFreq", value: { factor } },
}),
);
};
return {
state: stateOptimistic.value,
setTargetSpeed,
setEnabled,
setFreq,
setAccFreq,
};
}
@@ -19,7 +19,6 @@ export function Winder2GraphsPage() {
traversePosition,
tensionArmAngle,
pullerSpeed,
targetPullerSpeed,
spoolProgress,
} = useWinder2();
@@ -32,7 +31,6 @@ export function Winder2GraphsPage() {
<PullerSpeedGraph
syncHook={syncHook}
newData={pullerSpeed}
targetSeries={targetPullerSpeed}
targetSpeed={state?.puller_state?.target_speed}
unit="m/min"
renderValue={(value) => roundToDecimals(value, 0)}
@@ -245,14 +243,12 @@ export function SpoolProgressGraph({
export function PullerSpeedGraph({
syncHook,
newData,
targetSeries,
targetSpeed,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
targetSeries?: TimeSeries;
targetSpeed?: number;
unit?: Unit;
renderValue?: (value: number) => string;
@@ -263,7 +259,6 @@ export function PullerSpeedGraph({
lines.push({
type: "target",
value: targetSpeed,
targetSeries,
label: "Target Speed",
color: "#06b6d4", // Match series color
});
@@ -62,7 +62,6 @@ export function useWinder2() {
defaultState,
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -639,7 +638,6 @@ export function useWinder2() {
// Individual live values (TimeSeries)
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -211,7 +211,6 @@ export type Winder2NamespaceStore = {
spoolRpm: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
targetPullerSpeed: TimeSeries;
};
//Store Factory and Message Handler -> no param, so default values
@@ -221,8 +220,6 @@ const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
createTimeSeries();
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
createTimeSeries();
const { initialTimeSeries: targetPullerSpeed, insert: addTargetPullerSpeed } =
createTimeSeries();
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries();
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
createTimeSeries();
@@ -242,7 +239,6 @@ export const createWinder2NamespaceStore =
// Time series data for live values
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -277,18 +273,10 @@ export function winder2MessageHandler(
console.log(event);
// Parse and validate the state event
const stateEvent = stateEventSchema.parse(event);
const nextTargetPullerSpeed = stateEvent.data.puller_state.target_speed;
updateStore((state) => ({
...state,
state: stateEvent,
targetPullerSpeed:
state.targetPullerSpeed.current?.value === nextTargetPullerSpeed
? state.targetPullerSpeed
: addTargetPullerSpeed(state.targetPullerSpeed, {
value: nextTargetPullerSpeed,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
-19
View File
@@ -57,8 +57,6 @@ import { Aquapath1SettingsPage } from "@/machines/aquapath/aquapath1/Aquapath1Se
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { TestMachineStepperPage } from "@/machines/testmachinestepper/TestMachineStepperPage";
import { TestMachineStepperControlPage } from "@/machines/testmachinestepper/TestMachineStepperControlPage";
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage";
@@ -174,19 +172,6 @@ export const testMotorControlRoute = createRoute({
component: () => <TestMotorControlPage />,
});
export const testMachineStepperSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmachinestepper/$serial",
component: () => <TestMachineStepperPage />,
});
export const testMachineStepperControlRoute = createRoute({
getParentRoute: () => testMachineStepperSerialRoute,
path: "control",
component: () => <TestMachineStepperControlPage />,
});
// Leaf route: control page
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
path: "_sidebar",
@@ -530,10 +515,6 @@ export const rootTree = RootRoute.addChildren([
testMachineSerialRoute.addChildren([testMachineControlRoute]),
testMachineStepperSerialRoute.addChildren([
testMachineStepperControlRoute,
]),
analogInputTestMachineSerialRoute.addChildren([
analogInputTestMachineControlRoute,
]),
+25 -38
View File
@@ -1,22 +1,25 @@
import { Alert } from "@/components/Alert";
import { Page } from "@/components/Page";
import { SectionTitle } from "@/components/SectionTitle";
import { Terminal } from "@/components/Terminal";
import { TouchButton } from "@/components/touch/TouchButton";
import {
rebootHmi,
restartBackend,
exportLogs,
} from "@/helpers/troubleshoot_helpers";
import { useLogsStore } from "@/stores/logsStore";
import { rebootHmi, restartBackend } from "@/helpers/troubleshoot_helpers";
import React, { useState } from "react";
import { toast } from "sonner";
export function TroubleshootPage() {
const [isRebootLoading, setIsRebootLoading] = useState(false);
const [isRestartLoading, setIsRestartLoading] = useState(false);
const [isExportLoading, setIsExportLoading] = useState(false);
const { getLogsBySource, clearLogs } = useLogsStore();
// Perhaps we just need to clear the logs ?
// Get backend logs for display
const backendLogs = getLogsBySource("qitech-control-server");
const logLines = backendLogs.map((log) => log.raw);
const handleRebootHmi = async () => {
setIsRebootLoading(true);
try {
const result = await rebootHmi();
if (result.success) {
@@ -47,32 +50,10 @@ export function TroubleshootPage() {
}
};
const handleExportLogs = async () => {
setIsExportLoading(true);
try {
const result = await exportLogs();
if (result.success) {
toast.success("Export Logs initiated");
} else {
toast.error(`Failed to export Logs: ${result.error}`);
}
} catch (error) {
toast.error(`Failed to export Logs: ${error}`);
} finally {
setIsExportLoading(false);
}
};
return (
<Page>
<SectionTitle title="System Troubleshoot" />
<Alert title="Troubleshoot Actions Info" variant="warning">
These actions will temporarily interrupt system operations. The HMI
reboot will restart the entire panel, while the backend restart will
only restart the control service. Use with caution during production.
</Alert>
<div className="flex gap-4">
<TouchButton
variant="destructive"
@@ -93,17 +74,23 @@ export function TroubleshootPage() {
>
Restart Backend Process
</TouchButton>
<TouchButton
variant="outline"
icon="lu:Power"
isLoading={isExportLoading}
onClick={exportLogs}
className="w-max"
>
Export Backend Service Logs
</TouchButton>
</div>
<Alert title="Troubleshoot Actions Info" variant="warning">
These actions will temporarily interrupt system operations. The HMI
reboot will restart the entire panel, while the backend restart will
only restart the control service. Use with caution during production.
</Alert>
<h2 className="text-lg font-semibold">Backend Service Logs</h2>
<Terminal
lines={logLines}
autoScroll={true}
className="h-160"
title="qitech-control-server"
exportPrefix="qitech_control_server_journald"
/>
</Page>
);
}
+3 -11
View File
@@ -4,7 +4,6 @@ import { SectionTitle } from "@/components/SectionTitle";
import { Terminal } from "@/components/Terminal";
import { TouchButton } from "@/components/touch/TouchButton";
import { UpdateProgressBar } from "@/components/UpdateProgressBar";
import { Icon } from "@/components/Icon";
import { updateExecute, updateCancelWithStore } from "@/helpers/update_helpers";
import { useUpdateStore } from "@/stores/updateStore";
import { useSearch } from "@tanstack/react-router";
@@ -89,7 +88,7 @@ export function UpdateExecutePage() {
<Page>
<SectionTitle title="Apply Update" />
<div className="flex flex-row items-center gap-4">
<div className="flex flex-row gap-4">
<TouchButton
className="w-max"
icon="lu:CircleFadingArrowUp"
@@ -109,14 +108,6 @@ export function UpdateExecutePage() {
Cancel Update
</TouchButton>
)}
{isUpdating && (
<div className="ml-auto flex items-center gap-2 rounded-xl border border-blue-400 bg-blue-600 px-4 py-2.5 text-white shadow-xl backdrop-blur-sm transition-all duration-300">
<Icon name="lu:Info" className="h-5 w-5 text-blue-100" />
<span className="text-base leading-snug text-blue-50">
Updates typically take approximately <strong>5 minutes</strong>
</span>
</div>
)}
</div>
{currentUpdateInfo && (
<Alert title="Update Information" variant="info">
@@ -145,7 +136,8 @@ export function UpdateExecutePage() {
</div>
<div className="text-sm">
Please stay connected to the internet and leave the power on. The
machine will reboot after the update completes.
update procedure takes a couple of minutes and reboots the machine
afterwards.
</div>
</div>
</Alert>
+3 -1
View File
@@ -58,7 +58,9 @@ interface UpdateContext {
interface TroubleshootContext {
rebootHmi: () => Promise<{ success: boolean; error?: string }>;
restartBackend: () => Promise<{ success: boolean; error?: string }>;
exportLogs: () => Promise<{ success: boolean; error?: string }>;
startLogStream: () => Promise<{ success: boolean; error?: string }>;
stopLogStream: () => Promise<{ success: boolean; error?: string }>;
onLogData: (callback: (log: string) => void) => void;
}
interface NixOSGeneration {
+8 -23
View File
@@ -15,7 +15,6 @@ use crate::{
wago_750_501::{WAGO_750_501_MODULE_IDENT, WAGO_750_501_PRODUCT_ID},
wago_750_530::{WAGO_750_530_MODULE_IDENT, WAGO_750_530_PRODUCT_ID},
wago_750_652::{WAGO_750_652_MODULE_IDENT, WAGO_750_652_PRODUCT_ID},
wago_750_672::{WAGO_750_672_MODULE_IDENT, WAGO_750_672_PRODUCT_ID},
wago_750_1506::{WAGO_750_1506_MODULE_IDENT, WAGO_750_1506_PRODUCT_ID},
},
},
@@ -63,6 +62,7 @@ impl EthercatDevice for Wago750_354 {
None => break,
}
}
Ok(())
}
@@ -153,22 +153,14 @@ impl std::fmt::Debug for Wago750_354 {
impl Wago750_354 {
pub fn calculate_module_index(pdo_mapping: u32, is_tx: bool) -> u32 {
let start_index = match is_tx {
true => 0x6000,
false => 0x7000,
true => 0x6000 as u32,
false => 0x7000 as u32,
};
let pdo_index = (pdo_mapping & 0xFFFF0000) >> 16;
if pdo_index < start_index {
// Treat as coupler/module 0
return 0;
}
let index_in_hex = pdo_index - start_index;
let index_in_hex = ((pdo_mapping & 0xFFFF0000) >> 16) - start_index;
if index_in_hex < 16 {
0
return 0;
} else {
index_in_hex / 16
return index_in_hex / 16;
}
}
@@ -199,7 +191,7 @@ impl Wago750_354 {
}
let mut mappings_without_coupler: Vec<u32> = vec![];
for i in start_subindex..=count_mappings {
for i in start_subindex..count_mappings {
let pdo_index = device.sdo_read(index.0, i).await?;
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
for j in 0..pdo_map_count {
@@ -311,11 +303,6 @@ impl Wago750_354 {
module.has_rx = false;
module.name = "750-430".to_string();
}
WAGO_750_672_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
module.name = "750-672".to_string();
}
_ => println!(
"Wago-750-354 found Unknown/Unimplemented Module: {}",
ident_iom
@@ -396,9 +383,7 @@ impl Wago750_354 {
WAGO_750_430_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_430::Wago750_430::new()))
}
WAGO_750_672_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_672::Wago750_672::new()))
}
_ => {
println!(
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
@@ -6,4 +6,3 @@ pub mod wago_750_455;
pub mod wago_750_501;
pub mod wago_750_530;
pub mod wago_750_652;
pub mod wago_750_672;
@@ -143,7 +143,7 @@ pub struct Wago750_1506 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
// Should always have one but not when calling constructor
// Should always have on but not when calling constructor
module: Option<Module>,
rx_pdo: Wago750_1506RxPdo,
tx_pdo: Wago750_1506TxPdo,
@@ -1,387 +0,0 @@
/*
* Wago Stepper Controller 750-672
* 70 VDC / 7.5 A
*/
use anyhow::Ok;
use bitvec::field::BitField;
use crate::{
devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
},
io::{
digital_input::{DigitalInputDevice, DigitalInputInput},
stepper_velocity_wago_750_672::{
C1Command, C1Flag, C2Flag, C3Flag, ControlByteC1, ControlByteC2, ControlByteC3, S3Flag,
StatusByteS3,
},
},
};
#[derive(Clone)]
pub struct Wago750_672 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
pub rxpdo: Wago750_672RxPdo,
pub txpdo: Wago750_672TxPdo,
module: Option<Module>,
pub state: InitState,
pub initialized: bool,
}
// unfortunately we need to track and set bits over multiple cycles
// and wait for their acknowledgement so we have to use a state machine
// inside of our loop.
#[derive(Debug, Clone)]
pub enum InitState {
Off,
Enable,
SetMode,
StartPulseStart,
StartPulseEnd,
Running,
ErrorQuit,
ResetQuit,
}
/*
* Wago has two different kind of applications when it comes to
* running the Stepper Controller.
* - Positioning
* - Frequency/Speed Control
* !!!! IMPORTANT !!!!
* It seems like this is only true for the 750 671.
* The 750 672 has differnt commands inside of ControlByteC1
* where w can choose the Speed control mode - just like that
* we always want to use the latter one so the following
* process images are for that usecase.
*/
#[derive(Clone, Debug, Default)]
pub struct Wago750_672RxPdo {
pub control_byte: u8, // C0
pub velocity: i16, // D0/D1
pub acceleration: u16, // D2/D3
pub control_byte3: u8, // C3
pub control_byte2: u8, // C2
pub control_byte1: u8, // C1
}
#[derive(Clone, Debug, Default)]
pub struct Wago750_672TxPdo {
pub status_byte0: u8, // S0
pub actual_velocity: i16, // D0/D1
pub position_l: u8, // D4
pub position_m: u8, // D5
pub position_h: u8, // D6
pub status_byte3: u8, // S3
pub status_byte2: u8, // S2
pub status_byte1: u8, // S1
}
/// Digital input port enumeration for the 6 inputs
#[derive(Debug, Clone, Copy)]
pub enum Wago750_672InputPort {
DI1,
DI2,
DI3,
DI4,
DI5,
DI6,
}
// Get the Digital Inputs from the Status Byte S3
impl DigitalInputDevice<Wago750_672InputPort> for Wago750_672 {
fn get_input(&self, port: Wago750_672InputPort) -> Result<DigitalInputInput, anyhow::Error> {
let s3 = StatusByteS3::from_bits(self.txpdo.status_byte3);
Ok(DigitalInputInput {
value: match port {
Wago750_672InputPort::DI1 => s3.has_flag(S3Flag::Input1),
Wago750_672InputPort::DI2 => s3.has_flag(S3Flag::Input2),
Wago750_672InputPort::DI3 => s3.has_flag(S3Flag::Input3),
Wago750_672InputPort::DI4 => s3.has_flag(S3Flag::Input4),
Wago750_672InputPort::DI5 => s3.has_flag(S3Flag::Input5),
Wago750_672InputPort::DI6 => s3.has_flag(S3Flag::Input6),
},
})
}
}
impl EthercatDeviceUsed for Wago750_672 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_672 {}
impl EthercatDynamicPDO for Wago750_672 {
fn get_tx_offset(&self) -> usize {
self.tx_bit_offset
}
fn get_rx_offset(&self) -> usize {
self.rx_bit_offset
}
fn set_tx_offset(&mut self, offset: usize) {
self.tx_bit_offset = offset
}
fn set_rx_offset(&mut self, offset: usize) {
self.rx_bit_offset = offset
}
}
impl EthercatDeviceProcessing for Wago750_672 {
fn input_post_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_pre_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
}
impl EthercatDevice for Wago750_672 {
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.tx_bit_offset;
let mut b = [0u8; 12];
for i in 0..12 {
b[i] = input[base + i * 8..base + (i + 1) * 8].load_le();
}
self.txpdo = Wago750_672TxPdo {
status_byte0: b[0],
actual_velocity: i16::from_le_bytes([b[2], b[3]]),
position_l: b[6],
position_m: b[7],
position_h: b[8],
status_byte3: b[9],
status_byte2: b[10],
status_byte1: b[11],
};
// Yes this statemachine is unfortunately needed in here to make sure
// bits are correctly set and reset in the correct cycle.
match self.state {
InitState::Off => {
// Do nothing
self.initialized = false;
}
InitState::Enable => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.bits();
self.rxpdo.control_byte1 = c1;
// Switch state if ENABLE and STOP2_N is acknowledged
if self.txpdo.status_byte1 == c1 {
self.state = InitState::SetMode;
}
}
InitState::SetMode => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.control_byte1 = c1;
// Switch state if SPEED MODE is acknowledged
if self.txpdo.status_byte1 == c1 {
self.initialized = true;
self.state = InitState::StartPulseStart;
}
}
InitState::StartPulseStart => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_flag(C1Flag::Start)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.control_byte1 = c1;
// Switch state after StartPulse is acknowledged
if self.txpdo.status_byte1 == c1 {
self.state = InitState::StartPulseEnd;
}
}
InitState::StartPulseEnd => {
let c1 = ControlByteC1::new()
.with_flag(C1Flag::Enable)
.with_flag(C1Flag::Stop2N)
.with_command(C1Command::SpeedControl)
.bits();
self.rxpdo.control_byte1 = c1;
// Switch state after StartPulse is over
if self.txpdo.status_byte1 == c1 {
self.state = InitState::Running;
}
}
InitState::Running => {
let c2 = ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
let c3 = ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
// Check for Error
if self.txpdo.status_byte2 & c2 != 0 {
self.state = InitState::ErrorQuit;
} else {
self.rxpdo.control_byte2 |= c2;
}
// Check for Reset
if self.txpdo.status_byte3 & c3 != 0 {
self.state = InitState::ResetQuit;
} else {
self.rxpdo.control_byte3 |= c3;
}
}
InitState::ErrorQuit => {
self.rxpdo.control_byte2 |=
ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
tracing::error!("Stepper Controller Errored. Trying to reenable...");
self.state = InitState::Enable;
}
InitState::ResetQuit => {
self.rxpdo.control_byte3 |=
ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
tracing::error!("Stepper Controller Reset not Quit. Trying to reenable...");
self.state = InitState::Enable;
}
}
Ok(())
}
fn input_len(&self) -> usize {
12 * 8
}
fn output(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.rx_bit_offset;
let output_bytes = [
self.rxpdo.control_byte,
0,
self.rxpdo.velocity.to_le_bytes()[0],
self.rxpdo.velocity.to_le_bytes()[1],
self.rxpdo.acceleration.to_le_bytes()[0],
self.rxpdo.acceleration.to_le_bytes()[1],
0,
0,
0,
self.rxpdo.control_byte3,
self.rxpdo.control_byte2,
self.rxpdo.control_byte1,
];
for i in 0..12 {
output[base + i * 8..base + (i + 1) * 8].store_le(output_bytes[i]);
}
Ok(())
}
fn output_len(&self) -> usize {
12 * 8
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn is_module(&self) -> bool {
true
}
fn input_checked(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let expected = self.input_len();
let actual = input.len();
if actual != expected {
return Err(anyhow::anyhow!(
"[{}::Device::input_checked] Input length is {} ({} bytes) and must be {} bits ({} bytes)",
module_path!(),
actual,
actual / 8,
expected,
expected / 8
));
}
Ok(())
}
fn output_checked(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let expected = self.output_len();
let actual = output.len();
if actual != expected {
return Err(anyhow::anyhow!(
"[{}::Device::output_checked] Output length is {} ({} bytes) and must be {} bits ({} bytes)",
module_path!(),
actual,
actual / 8,
expected,
expected / 8
));
}
Ok(())
}
fn get_module(&self) -> Option<crate::devices::Module> {
self.module.clone()
}
fn set_module(&mut self, module: crate::devices::Module) {
self.tx_bit_offset = module.tx_offset;
self.rx_bit_offset = module.rx_offset;
self.module = Some(module)
}
}
impl NewEthercatDevice for Wago750_672 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
rxpdo: Wago750_672RxPdo::default(),
txpdo: Wago750_672TxPdo::default(),
state: InitState::Off,
initialized: false,
}
}
}
impl std::fmt::Debug for Wago750_672 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_672")
}
}
pub const WAGO_750_672_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_672_PRODUCT_ID: u32 = 108139752;
pub const WAGO_750_672_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_672_VENDOR_ID, WAGO_750_672_PRODUCT_ID);
@@ -5,7 +5,7 @@ const U16_MAX: i128 = std::u16::MAX as i128;
/// We convert the overflows and underflows to an i128 value for easier calculations.
///
/// When overriding the counter we don't set the valeu direclty but schedula it wiht the [`push_override`] so it can be synced with [`pop_override`] to an EtherCAT device.
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct CounterWrapperU16U128 {
counter: i128,
last_counter: u16,
-1
View File
@@ -7,5 +7,4 @@ pub mod encoder_input;
pub mod pulse_train_output;
pub mod serial_interface;
pub mod stepper_velocity_el70x1;
pub mod stepper_velocity_wago_750_672;
pub mod temperature_input;
@@ -1,376 +0,0 @@
use std::sync::Arc;
use smol::{block_on, lock::RwLock};
use crate::devices::wago_modules::wago_750_672::{InitState, Wago750_672};
/*
* Wago Stepper Velocity Wrapper around Stepper Controller
*
*/
#[derive(Debug)]
pub struct StepperVelocityWago750672 {
pub device: Arc<RwLock<Wago750_672>>,
pub state: InitState,
pub target_velocity: i16,
pub target_acceleration: u16,
pub enabled: bool,
pub freq_range_sel: u8,
pub acc_range_sel: u8,
}
impl StepperVelocityWago750672 {
pub fn new(device: Arc<RwLock<Wago750_672>>) -> Self {
Self {
device,
state: InitState::Off,
target_velocity: 0,
target_acceleration: 10000,
enabled: false,
freq_range_sel: 0,
acc_range_sel: 0,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
if self.enabled && enabled {
return;
}
// this needs to be set before the stepper controller
// is enabled because it will generate an error otherwise
self.set_acceleration(self.target_acceleration);
self.enabled = enabled;
if enabled {
self.change_init_state(InitState::Enable);
} else {
self.change_init_state(InitState::Off);
self.write_control_byte(ControlByte::C1, 0b00000000);
}
}
pub fn set_velocity(&mut self, velocity: i16) {
self.target_velocity = velocity;
let mut dev = block_on(self.device.write());
// clamp velocity to -25000 - 25000 :: because this is
// the min max for the controller
dev.rxpdo.velocity = velocity.clamp(-25000, 25000);
if dev.initialized {
dev.state = InitState::StartPulseStart;
}
}
pub fn set_acceleration(&mut self, acceleration: u16) {
self.target_acceleration = acceleration;
let mut dev = block_on(self.device.write());
dev.rxpdo.acceleration = acceleration;
}
#[allow(dead_code)]
fn get_actual_velocity(&self) -> i16 {
let dev = block_on(self.device.read());
dev.txpdo.actual_velocity
}
#[allow(dead_code)]
fn get_target_acceleration(&self) -> u16 {
let dev = block_on(self.device.read());
dev.rxpdo.acceleration
}
pub fn set_freq_range_sel(&mut self, factor: u8) {
if self.enabled || factor > 3 {
return;
}
self.freq_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.control_byte2)
.with_freq_range(factor)
.bits();
dev.rxpdo.control_byte2 = c2;
}
pub fn set_acc_range_sel(&mut self, factor: u8) {
if self.enabled || factor > 3 {
return;
}
self.acc_range_sel = factor;
let mut dev = block_on(self.device.write());
let c2 = ControlByteC2::from_bits(dev.rxpdo.control_byte2)
.with_acc_range(factor)
.bits();
dev.rxpdo.control_byte2 = c2;
}
fn change_init_state(&mut self, state: InitState) {
self.state = state.clone();
let mut dev = block_on(self.device.write());
dev.state = state;
}
fn write_control_byte(&self, control_byte: ControlByte, value: u8) {
let mut dev = block_on(self.device.write());
match control_byte {
ControlByte::C0 => dev.rxpdo.control_byte = value,
ControlByte::C1 => dev.rxpdo.control_byte1 = value,
ControlByte::C2 => dev.rxpdo.control_byte2 = value,
ControlByte::C3 => dev.rxpdo.control_byte3 = value,
}
}
#[allow(dead_code)]
fn read_status_byte(&self, status_byte: StatusByte) -> u8 {
let dev = block_on(self.device.read());
match status_byte {
StatusByte::S0 => dev.txpdo.status_byte0,
StatusByte::S1 => dev.txpdo.status_byte1,
StatusByte::S2 => dev.txpdo.status_byte2,
StatusByte::S3 => dev.txpdo.status_byte3,
}
}
}
// The Different Control Bytes set control the stepper controller
// by setting and resetting specific bits.
//
// There are some construction functions provided to create the
// control bytes correctly.
/// Control Byte C1
#[derive(Clone, Copy, Default)]
pub struct ControlByteC1(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C1Flag {
Enable = 0b0000_0001,
Stop2N = 0b0000_0010,
Start = 0b0000_0100,
}
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C1Command {
Idle = 0b0000_0000,
SinglePosition = 0b0000_1000,
RunProgram = 0b0001_0000,
SpeedControl = 0b0001_1000,
Reference = 0b0010_0000,
JogMode = 0b0010_1000,
Mailbox = 0b0011_0000,
}
impl ControlByteC1 {
pub const fn new() -> Self {
Self(0)
}
pub const fn with_flag(mut self, flag: C1Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn with_command(mut self, cmd: C1Command) -> Self {
self.0 = (self.0 & 0b0000_0111) | (cmd as u8);
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Control Byte C2
#[derive(Clone, Copy, Default)]
pub struct ControlByteC2(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C2Flag {
FreqRangeSelL = 0b0000_0001,
FreqRangeSelH = 0b0000_0010,
AccRangeSelL = 0b0000_0100,
AccRangeSelH = 0b0000_1000,
PreCalc = 0b0100_0000,
ErrorQuit = 0b1000_0000,
}
impl ControlByteC2 {
pub const fn new() -> Self {
Self(0)
}
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn with_freq_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_1100) | (sel & 0b0000_0011);
self
}
pub const fn with_acc_range(mut self, sel: u8) -> Self {
self.0 = (self.0 & 0b1111_0011) | ((sel & 0b0000_0011) << 2);
self
}
pub const fn with_flag(mut self, flag: C2Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Control Byte C3
#[derive(Clone, Copy, Default)]
pub struct ControlByteC3(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum C3Flag {
SetActualPos = 0b0000_0001,
DirectionPos = 0b0000_0010,
DirectionNeg = 0b0000_0100,
ResetQuit = 0b1000_0000,
}
impl ControlByteC3 {
pub const fn new() -> Self {
Self(0)
}
pub const fn with_flag(mut self, flag: C3Flag) -> Self {
self.0 |= flag as u8;
self
}
pub const fn bits(self) -> u8 {
self.0
}
}
// The status bytes are similar to the control bytes but are only readable
// and most of the time provide corresponding acknoledgements to the diffrent
// Control bytes.
//
// There are also helper functions to construct the diffrent Status bytes for
// compoarison.
/// Status Byte S1
#[derive(Clone, Copy, Default)]
pub struct StatusByteS1(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S1Flag {
Ready = 0b0000_0001,
Stop2NAck = 0b0000_0010,
StartAck = 0b0000_0100,
}
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S1CommandAck {
Idle = 0b0000_0000,
SinglePosition = 0b0000_1000,
RunProgram = 0b0001_0000,
SpeedControl = 0b0001_1000,
Reference = 0b0010_0000,
JogMode = 0b0010_1000,
Mailbox = 0b0011_0000,
}
impl StatusByteS1 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S1Flag) -> bool {
(self.0 & flag as u8) != 0
}
pub const fn bits(self) -> u8 {
self.0
}
}
/// Status Byte S2
#[derive(Clone, Copy, Default)]
pub struct StatusByteS2(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S2Flag {
OnTarget = 0b0000_0001,
Busy = 0b0000_0010,
StandStill = 0b0000_0100,
OnSpeed = 0b0000_1000,
Direction = 0b0001_0000,
ReferenceOk = 0b0010_0000,
PreCalcAck = 0b0100_0000,
Error = 0b1000_0000,
}
impl StatusByteS2 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S2Flag) -> bool {
(self.0 & flag as u8) != 0
}
}
/// Status Byte S3
#[derive(Clone, Copy, Default)]
pub struct StatusByteS3(u8);
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum S3Flag {
Input1 = 0b0000_0001,
Input2 = 0b0000_0010,
Input3 = 0b0000_0100,
Input4 = 0b0000_1000,
Input5 = 0b0001_0000,
Input6 = 0b0010_0000,
Warning = 0b0100_0000,
Reset = 0b1000_0000,
}
impl StatusByteS3 {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn has_flag(self, flag: S3Flag) -> bool {
(self.0 & flag as u8) != 0
}
}
#[derive(Debug, Clone, Copy)]
pub enum ControlByte {
C0,
C1,
C2,
C3,
}
#[derive(Debug, Clone, Copy)]
pub enum StatusByte {
S0,
S1,
S2,
S3,
}
Generated
+12 -111
View File
@@ -1,21 +1,6 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1771796463,
"narHash": "sha256-9bCDuUzpwJXcHMQYMS1yNuzYMmKO/CCwCexpjWOl62I=",
"owner": "ipetkov",
"repo": "crane",
"rev": "3d3de3313e263e04894f284ac18177bd26169bad",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"crane_2": {
"locked": {
"lastModified": 1750266157,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
@@ -30,7 +15,7 @@
"type": "github"
}
},
"crane_3": {
"crane_2": {
"locked": {
"lastModified": 1750266157,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
@@ -117,51 +102,12 @@
"type": "github"
}
},
"flake-utils_5": {
"inputs": {
"systems": "systems_5"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1771851181,
"narHash": "sha256-gFgE6mGUftwseV3DUENMb0k0EiHd739lZexPo5O/sdQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "9a4b494b1aa1b93d8edf167f46dc8e0c0011280c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"nixpkgs"
]
},
"locked": {
"lastModified": 1750973805,
"narHash": "sha256-BZXgag7I0rnL/HMHAsBz3tQrfKAibpY2vovexl2lS+Y=",
@@ -176,10 +122,9 @@
"type": "github"
}
},
"home-manager_3": {
"home-manager_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"nixpkgs"
]
@@ -198,10 +143,9 @@
"type": "github"
}
},
"home-manager_4": {
"home-manager_3": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -223,11 +167,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1767313136,
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"lastModified": 1750969886,
"narHash": "sha256-zW/OFnotiz/ndPFdebpo3X0CrbVNf22n4DjN2vxlb58=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"rev": "a676066377a2fe7457369dd37c31fd2263b662f4",
"type": "github"
},
"original": {
@@ -263,31 +207,6 @@
],
"qitech-control": "qitech-control_2"
},
"locked": {
"lastModified": 1772023797,
"narHash": "sha256-RipiUlH5UKiofTRquREluUFOwEfNjLFEvyDol/VWDRg=",
"owner": "qitechgmbh",
"repo": "control",
"rev": "3e3e3f1c0bc2b26fe0fc1e7d49b65e3fd3d7f368",
"type": "github"
},
"original": {
"owner": "qitechgmbh",
"repo": "control",
"type": "github"
}
},
"qitech-control_2": {
"inputs": {
"crane": "crane_3",
"flake-utils": "flake-utils_3",
"home-manager": "home-manager_3",
"nixpkgs": [
"qitech-control",
"nixpkgs"
],
"qitech-control": "qitech-control_3"
},
"locked": {
"lastModified": 1751047510,
"narHash": "sha256-SZ+DxQvFJKJOro1AVUwWltLQO1L9TIc6AI4tnM9V4tk=",
@@ -302,16 +221,15 @@
"type": "github"
}
},
"qitech-control_3": {
"qitech-control_2": {
"inputs": {
"flake-utils": "flake-utils_4",
"home-manager": "home-manager_4",
"flake-utils": "flake-utils_3",
"home-manager": "home-manager_3",
"nixpkgs": [
"qitech-control",
"qitech-control",
"nixpkgs"
],
"qitech-control": "qitech-control_4",
"qitech-control": "qitech-control_3",
"rust-overlay": "rust-overlay_2"
},
"locked": {
@@ -328,11 +246,10 @@
"type": "github"
}
},
"qitech-control_4": {
"qitech-control_3": {
"inputs": {
"flake-utils": "flake-utils_5",
"flake-utils": "flake-utils_4",
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -383,7 +300,6 @@
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -462,21 +378,6 @@
"repo": "default",
"type": "github"
}
},
"systems_5": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+1 -1
View File
@@ -2,7 +2,7 @@
description = "QiTech Control";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
# Crane for Rust builds with dependency caching
crane = { url = "github:ipetkov/crane"; };
-7
View File
@@ -152,7 +152,6 @@ pub struct HeatingState {
pub struct ExtruderSettingsState {
pub pressure_limit: f64,
pub pressure_limit_enabled: bool,
pub nozzle_temperature_target_enabled: bool,
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
@@ -238,9 +237,6 @@ pub enum Mutation {
// Reset
ResetInverter(bool),
// Toggle nozzle temperature target
SetNozzleTemperatureTargetEnabled(bool),
}
#[derive(Debug)]
@@ -321,9 +317,6 @@ impl MachineApi for ExtruderV2 {
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
}
}
Ok(())
}
-9
View File
@@ -94,9 +94,6 @@ impl ExtruderV2 {
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
nozzle_temperature_target_enabled: self
.temperature_controller_nozzle
.get_temperature_target_enabled(),
},
inverter_status_state: InverterStatusState {
running: self.screw_speed_controller.inverter.status.running,
@@ -299,12 +296,6 @@ impl ExtruderV2 {
self.emit_state();
}
pub fn set_nozzle_temperature_target_is_enabled(&mut self, enabled: bool) {
self.temperature_controller_nozzle
.set_temperature_target_enabled(enabled);
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.screw_speed_controller
.pid
-3
View File
@@ -41,9 +41,6 @@ impl MachineApi for ExtruderV2 {
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
}
}
Ok(())
}
-6
View File
@@ -130,12 +130,6 @@ impl ExtruderV2 {
self.emit_state();
}
pub fn set_nozzle_temperature_target_is_enabled(&mut self, enabled: bool) {
self.extruder_settings_state
.nozzle_temperature_target_enabled = enabled;
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.pid_settings.pressure.ki = settings.ki;
self.pid_settings.pressure.kp = settings.kp;
-1
View File
@@ -81,7 +81,6 @@ impl MachineNewTrait for ExtruderV2 {
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
nozzle_temperature_target_enabled: true,
},
inverter_status_state: InverterStatusState {
running: false,
@@ -20,7 +20,6 @@ pub struct TemperatureController {
temperature_pid_output: f64,
heating_element_wattage: f64,
max_clamp: f64,
target_temp_enabled: bool, // Sets whether the frontend should display a target temperature setter for this temp controller
}
impl TemperatureController {
@@ -56,7 +55,6 @@ impl TemperatureController {
temperature_pid_output: 0.0,
heating_element_wattage,
max_clamp,
target_temp_enabled: true,
}
}
@@ -64,14 +62,6 @@ impl TemperatureController {
self.heating.target_temperature = temp;
}
pub fn set_temperature_target_enabled(&mut self, enabled: bool) {
self.target_temp_enabled = enabled;
}
pub fn get_temperature_target_enabled(&self) -> bool {
self.target_temp_enabled
}
pub const fn disallow_heating(&mut self) {
self.heating_allowed = false;
}
+3 -6
View File
@@ -143,9 +143,6 @@ pub enum Mutation {
SetExtruderPressureLimit(f64),
SetExtruderPressureLimitIsEnabled(bool),
// ToggleTemperatureTarget
SetNozzleTemperatureTargetEnabled(bool),
// Pid Configure
SetPressurePidSettings(PidSettings),
SetTemperaturePidSettings(TemperaturePid),
@@ -206,6 +203,7 @@ impl MachineApi for ExtruderV3 {
Mutation::SetInverterTargetPressure(bar) => self.set_target_pressure(bar),
Mutation::SetInverterTargetRpm(rpm) => self.set_target_rpm(rpm),
Mutation::ResetInverter(_) => self.reset_inverter(),
Mutation::SetFrontHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Front)
}
@@ -224,15 +222,14 @@ impl MachineApi for ExtruderV3 {
Mutation::SetExtruderPressureLimitIsEnabled(enabled) => {
self.set_nozzle_pressure_limit_is_enabled(enabled);
}
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
}
}
Ok(())
}
-9
View File
@@ -102,9 +102,6 @@ impl ExtruderV3 {
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
nozzle_temperature_target_enabled: self
.temperature_controller_nozzle
.get_temperature_target_enabled(),
},
inverter_status_state: InverterStatusState {
running: self.screw_speed_controller.inverter.status.running,
@@ -243,12 +240,6 @@ impl ExtruderV3 {
self.emit_state();
}
pub fn set_nozzle_temperature_target_is_enabled(&mut self, enabled: bool) {
self.temperature_controller_nozzle
.set_temperature_target_enabled(enabled);
self.emit_state();
}
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
self.screw_speed_controller
.set_nozzle_pressure_limit(Pressure::new::<bar>(pressure));
-3
View File
@@ -41,9 +41,6 @@ impl MachineApi for ExtruderV2 {
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
}
}
Ok(())
}
-6
View File
@@ -128,12 +128,6 @@ impl ExtruderV2 {
self.emit_state();
}
pub fn set_nozzle_temperature_target_is_enabled(&mut self, enabled: bool) {
self.extruder_settings_state
.nozzle_temperature_target_enabled = enabled;
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.pid_settings.pressure.ki = settings.ki;
self.pid_settings.pressure.kp = settings.kp;
-1
View File
@@ -81,7 +81,6 @@ impl MachineNewTrait for ExtruderV2 {
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
nozzle_temperature_target_enabled: true,
},
inverter_status_state: InverterStatusState {
running: false,
-2
View File
@@ -32,7 +32,6 @@ pub mod motor_test_machine;
pub mod registry;
pub mod serial;
pub mod test_machine;
pub mod test_machine_stepper;
pub mod wago_8ch_dio_test_machine;
pub mod wago_ai_test_machine;
pub mod wago_do_test_machine;
@@ -54,7 +53,6 @@ pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036;
pub const DIGITAL_INPUT_TEST_MACHINE: u16 = 0x0040;
pub const WAGO_8CH_IO_TEST_MACHINE: u16 = 0x0041;
pub const TEST_MACHINE_STEPPER: u16 = 0x0037;
pub const MOTOR_TEST_MACHINE: u16 = 0x0011;
pub const WAGO_DO_TEST_MACHINE: u16 = 0x000E;
+1 -2
View File
@@ -22,7 +22,7 @@ use crate::{
winder2::Winder2,
};
use crate::{test_machine::TestMachine, test_machine_stepper::TestMachineStepper};
use crate::test_machine::TestMachine;
use lazy_static::lazy_static;
@@ -144,7 +144,6 @@ lazy_static! {
Wago8chDigitalIOTestMachine::MACHINE_IDENTIFICATION,
);
mc.register::<TestMachineStepper>(TestMachineStepper::MACHINE_IDENTIFICATION);
mc
};
}
-33
View File
@@ -1,33 +0,0 @@
use super::TestMachineStepper;
use crate::{MachineAct, MachineMessage};
use std::time::{Duration, Instant};
impl MachineAct for TestMachineStepper {
fn act(&mut self, now: Instant) {
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_state();
self.last_state_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => {}
MachineMessage::DisconnectMachine(_machine_connection) => {}
MachineMessage::RequestValues(_sender) => {}
}
}
}
-87
View File
@@ -1,87 +0,0 @@
use super::TestMachineStepper;
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub target_speed: i16,
pub enabled: bool,
pub freq: u8,
pub acc_freq: u8,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum TestMachineStepperEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetTargetSpeed { target: i16 },
SetEnabled { enabled: bool },
SetFreq { factor: u8 },
SetAccFreq { factor: u8 },
}
#[derive(Debug, Clone)]
pub struct TestMachineStepperNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<TestMachineStepperEvents> for TestMachineStepperNamespace {
fn emit(&mut self, events: TestMachineStepperEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<TestMachineStepperEvents> for TestMachineStepperEvents {
fn event_value(&self) -> GenericEvent {
match self {
TestMachineStepperEvents::State(event) => event.clone().into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for TestMachineStepper {
fn api_get_sender(&self) -> smol::channel::Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetTargetSpeed { target } => self.set_target_speed(target),
Mutation::SetEnabled { enabled } => self.set_enabled(enabled),
Mutation::SetFreq { factor } => self.set_freq(factor),
Mutation::SetAccFreq { factor } => self.set_acc_freq(factor),
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
-76
View File
@@ -1,76 +0,0 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::test_machine_stepper::api::{StateEvent, TestMachineStepperEvents};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::stepper_velocity_wago_750_672::StepperVelocityWago750672;
use smol::channel::{Receiver, Sender};
use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::test_machine_stepper::api::TestMachineStepperNamespace;
use crate::{TEST_MACHINE_STEPPER, VENDOR_QITECH};
#[derive(Debug)]
pub struct TestMachineStepper {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub namespace: TestMachineStepperNamespace,
pub last_state_emit: Instant,
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub stepper: StepperVelocityWago750672,
}
impl Machine for TestMachineStepper {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl TestMachineStepper {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: TEST_MACHINE_STEPPER,
};
}
impl TestMachineStepper {
pub fn get_state(&self) -> StateEvent {
StateEvent {
target_speed: self.stepper.target_velocity,
enabled: self.stepper.enabled,
freq: self.stepper.freq_range_sel,
acc_freq: self.stepper.acc_range_sel,
}
}
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(TestMachineStepperEvents::State(event));
}
pub fn set_target_speed(&mut self, speed: i16) {
self.stepper.set_velocity(speed);
self.emit_state();
}
pub fn set_enabled(&mut self, enabled: bool) {
self.stepper.set_enabled(enabled);
self.emit_state();
}
pub fn set_freq(&mut self, factor: u8) {
self.stepper.set_freq_range_sel(factor);
self.emit_state();
}
pub fn set_acc_freq(&mut self, factor: u8) {
self.stepper.set_acc_range_sel(factor);
self.emit_state();
}
}
-86
View File
@@ -1,86 +0,0 @@
use crate::test_machine_stepper::{TestMachineStepper, api::TestMachineStepperNamespace};
use ethercat_hal::{
devices::{
EthercatDevice, downcast_device,
wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354},
wago_modules::wago_750_672::Wago750_672,
},
io::stepper_velocity_wago_750_672::StepperVelocityWago750672,
};
use smol::{block_on, lock::RwLock};
use std::{sync::Arc, time::Instant};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use anyhow::Error;
impl MachineNewTrait for TestMachineStepper {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
let device_identification = params
.device_group
.iter()
.map(|device_identification| device_identification.clone())
.collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/TestMachineStepper::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
let _wago_750_354 = get_ethercat_device::<Wago750_354>(
hardware,
params,
0,
[WAGO_750_354_IDENTITY_A].to_vec(),
)
.await?;
let modules = Wago750_354::initialize_modules(_wago_750_354.1).await?;
let mut coupler = _wago_750_354.0.write().await;
for module in modules {
coupler.set_module(module);
}
coupler.init_slot_modules(_wago_750_354.1);
let dev = coupler.slot_devices.first().unwrap().clone().unwrap();
// change uncomment this to change to different stepper driver
//
// let wago_750_671: Arc<RwLock<Wago750_671>> =
// downcast_device::<Wago750_671>(dev).await?;
let wago_750_672: Arc<RwLock<Wago750_672>> =
downcast_device::<Wago750_672>(dev).await?;
drop(coupler);
let stepper = StepperVelocityWago750672::new(wago_750_672);
let (sender, receiver) = smol::channel::unbounded();
let mut my_test = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: TestMachineStepperNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
main_sender: params.main_thread_channel.clone(),
stepper,
};
my_test.emit_state();
Ok(my_test)
})
}
}
-10
View File
@@ -7,16 +7,6 @@ in {
options.services.qitech = {
enable = mkEnableOption "QiTech Control";
openFirewall = mkOption {
type = types.bool;
default = false;
description =
"Whether to open ports in the firewall for the QiTech server";
};
networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 443 ]; };
user = mkOption {
type = types.str;
default = "qitech-service";
+70 -52
View File
@@ -15,7 +15,7 @@ in {
consoleMode = "max"; # Use the highest available resolution
};
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages_6_18;
boot.kernelPackages = pkgs.linuxPackages_6_13;
boot.kernelModules = [ "i915" ];
boot.kernelParams = [
@@ -64,6 +64,7 @@ in {
];
# Add these system settings for a more comprehensive kiosk setup
boot.kernel.sysctl = {
"kernel.panic_on_oops" = 1; # Reboot on kernel oops
"kernel.panic" = 10; # Reboot after 10 seconds on panic
@@ -104,7 +105,15 @@ in {
}
];
networking.hostName = "nixos";
networking.hostName = "nixos"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
networking.interfaces.enp1s0.ipv4.addresses = [
{
address = "10.10.10.1";
prefixLength = 24;
}
];
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
@@ -134,43 +143,15 @@ in {
# Enable the X11 windowing system.
services.xserver.enable = true;
#services.xserver.videoDrivers = [ "intel" ];
services.xserver.displayManager.gdm = {
enable = true;
autoSuspend = false;
wayland = true;
};
services.xserver.desktopManager.gnome.enable = true;
services.caddy = {
enable = true;
# This puts the import at the TOP of the Caddyfile (Global Scope)
extraConfig = ''
import /var/lib/caddy/auth_snippet.conf
'';
virtualHosts.":443" = {
extraConfig = ''
import machine_basic_auth
reverse_proxy localhost:3001
tls internal {
on_demand
}
'';
};
};
systemd.services.caddy.serviceConfig.ReadOnlyPaths = [ "/var/lib/caddy/auth_snippet.conf" ];
#services.caddy = {
# enable = true;
# virtualHosts."localhost".extraConfig = ''
# respond "Hello, world!"
# '';
#};
# Disable sleep/suspend
systemd.targets.sleep.enable = false;
systemd.targets.suspend.enable = false;
@@ -185,25 +166,18 @@ in {
powertop.enable = false;
};
# Ensure all power management is disabled
services.logind = {
# This remains for backward compatibility/high-level override
lidSwitch = "ignore";
# Structured settings for logind.conf
settings = {
Login = {
HandlePowerKey = "ignore";
HandleSuspendKey = "ignore";
HandleHibernateKey = "ignore";
HandleLidSwitch = "ignore";
IdleAction = "ignore";
};
};
extraConfig = ''
HandlePowerKey=ignore
HandleSuspendKey=ignore
HandleHibernateKey=ignore
HandleLidSwitch=ignore
IdleAction=ignore
'';
};
# Configure keymap in X11
services.xserver.xkb = {
layout = "de";
@@ -224,6 +198,12 @@ in {
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
#jack.enable = true;
# use the example session manager (no others are packaged yet so this is enabled by default,
# no need to redefine it in your config for now)
#media-session.enable = true;
};
# Enable graphics acceleration
@@ -284,7 +264,6 @@ in {
wireshark
pciutils
neofetch
caddy
];
xdg.portal.enable = true;
@@ -325,12 +304,51 @@ in {
QITECH_OS_GIT_URL = gitInfo.gitUrl;
};
# Set revision label
# Set revision labe;
system.nixos.label = "${gitInfo.gitAbbreviationEscaped}_${gitInfo.gitCommit}";
networking.firewall.allowedUDPPorts = [ 53 67 69 ];
networking.firewall.allowedTCPPorts = [ 443 ];
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
# programs.gnupg.agent = {
# enable = true;
# enableSSHSupport = true;
# };
# List services that you want to enable:
# Enable the OpenSSH daemon.
# services.openssh.enable = true;
services.dnsmasq = {
enable = true;
resolveLocalQueries = false;
settings = {
interface = "enp1s0"; # only this interface
bind-interfaces = true;
server = [ "1.1.1.1" "8.8.8.8" ];
# DHCP subnet + pool
dhcp-range = "10.10.10.50,10.10.10.250,255.255.255.0,12h";
# Default gateway (router)
dhcp-option = [
"3,10.10.10.1" # option 3 = router
"6,1.1.1.1,8.8.8.8" # option 6 = DNS servers
];
};
};
networking.firewall.enable = true;
networking.firewall.trustedInterfaces = [ "enp1s0" ];
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It‘s perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "24.11"; # Did you read the comment?
}
+2 -1
View File
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true;
npmDepsHash = "sha256-GKKCnTp5QM5zqayl/qZnmzT2uD01UJ9dgamC7cTYif4=";
npmDepsHash = "sha256-rnaAsSkoTRoBgLCTF8ZFpi7uUQ+8W+u3qoMxsptX6NU=";
npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = ''
@@ -44,6 +44,7 @@ buildNpmPackage rec {
--enable-gpu \
--ignore-gpu-blocklist \
--enable-gpu-rasterization \
--disable-software-rasterizer \
"$@"
EOF
chmod +x $out/bin/qitech-control-electron
-3
View File
@@ -30,9 +30,6 @@ docker run --rm \
--workdir /workspace \
nixos/nix:latest \
sh -c "
whoami
ls -lh
cd /workspace
echo 'Enabling flakes and nix-command...'
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' > ~/.config/nix/nix.conf
-868
View File
@@ -1,868 +0,0 @@
{
"name": "control",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"electron": "^38.8.3"
}
},
"node_modules/@electron/get": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^2.2.0",
"fs-extra": "^8.1.0",
"got": "^11.8.5",
"progress": "^2.0.3",
"semver": "^6.2.0",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"global-agent": "^3.0.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.19.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/boolean": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.6.0"
}
},
"node_modules/cacheable-request": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"dev": true,
"license": "MIT",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/electron": {
"version": "38.8.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-38.8.4.tgz",
"integrity": "sha512-vYZ2SH2OhzkCmkbRaijtZLIEJRMnLUL2KQmcAM/7kamf4++3AKSNCBjE1epmQnYFY1M4oKjOUcS3G02ayxR5Bw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
"electron": "cli.js"
},
"engines": {
"node": ">= 12.20.55"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/global-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"es6-error": "^4.1.1",
"matcher": "^3.0.0",
"roarr": "^2.15.3",
"semver": "^7.3.2",
"serialize-error": "^7.0.1"
},
"engines": {
"node": ">=10.0"
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/globalthis": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"define-properties": "^1.2.1",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=10.19.0"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"escape-string-regexp": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true,
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true,
"license": "MIT"
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"detect-node": "^2.0.4",
"globalthis": "^1.0.1",
"json-stringify-safe": "^5.0.1",
"semver-compare": "^1.0.0",
"sprintf-js": "^1.1.2"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"type-fest": "^0.13.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"devDependencies": {
"electron": "^38.8.4"
}
}
+28
View File
@@ -0,0 +1,28 @@
dist
.wrangler
.output
.vercel
.netlify
.vinxi
app.config.timestamp_*.js
# Environment
.env
.env*.local
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# Temp
gitignore
# System Files
.DS_Store
Thumbs.db
+298
View File
@@ -0,0 +1,298 @@
# Electron → SolidJS Migration Analysis
## Why SolidJS is a Good Fit
The electron frontend has two fundamental issues that SolidJS directly solves:
1. **React's reconciler triggers re-renders on any state change** — the codebase added an entire `ThrottledStoreUpdater` class (100+ lines) to cap re-renders at 30 FPS and prevent UI jank from high-frequency socket events.
In SolidJS, reactive updates are surgical: only the DOM nodes that actually depend on a changed signal update. No reconciliation pass, no virtual DOM diff. The `batch()` primitive coalesces multiple signal writes into one reactive pass — replaces `ThrottledStoreUpdater` entirely with zero boilerplate.
2. **React's hook rules and closure gotchas** force state management up into libraries (Zustand) and create a deep abstraction stack. SolidJS primitives (`createSignal`, `createStore`, `onCleanup`) live exactly where the components live and have no closure staleness issues.
---
## Abstraction Comparison: WebSocket Layer
### Electron (React)
Adding a machine namespace requires touching **5+ files**:
| File | Lines | Purpose |
|------|-------|---------|
| `socketioStore.ts` | 515 | Base infrastructure: ThrottledStoreUpdater, createNamespaceHookImplementation, namespace lifecycle |
| `{machine}Namespace.ts` | ~77 | Zod schemas, Zustand store, message handler, hook wrapper |
| `use{Machine}.ts` | ~95 | Route param parsing, optimistic state, mutation helpers |
| `routes.tsx` | +~15 | Route definitions |
| `properties.ts` | +~20 | Machine properties entry |
**Total per machine: ~220+ lines of mandatory boilerplate before writing any UI.**
### SolidJS (this PoC)
Adding a machine namespace requires:
```ts
// namespaces/myMachine.ts — ~25 lines total
export function createMyMachineNamespace(vendor, machine, serial) {
return createNamespace(
machineNamespacePath(vendor, machine, serial),
(event, set) => {
if (event.name === "StateEvent") set("data", event.data);
},
{ data: null },
);
}
```
Then in the page component:
```tsx
const [state] = createMyMachineNamespace(VENDOR, MACHINE, serial());
// state().data is now reactive — that's it
```
**The socket connects on component mount, disconnects on unmount, updates are reactive.
No store, no hook factory, no ThrottledStoreUpdater, no ref-counting.**
---
## The Core Primitive (`src/lib/socketio.ts`)
```
createNamespace(path, handler, initialState) → [signal, socket]
```
- `batch()` replaces `ThrottledStoreUpdater` — SolidJS batches reactive updates automatically
- `onCleanup()` replaces manual `useEffect` teardown — socket disconnects when scope disposed
- The signal IS the store — no Zustand, no Immer needed for simple machine state
This single 80-line file replaces `socketioStore.ts` (515 lines) for new machines.
---
## State Management
### Electron
- Zustand (global singletons + per-namespace stores)
- Immer (immutable updates)
- `useSyncExternalStore` (bridge Zustand → React)
- `ThrottledStoreUpdater` (performance workaround)
- `useStateOptimistic` (dual optimistic/real state)
### SolidJS
- `createSignal` / `createStore` — SolidJS built-ins
- `batch()` — built-in, no library
- Optimistic state: just another `createSignal` in the component
The optimistic pattern from `useStateOptimistic.tsx` (~80 lines) collapses to two lines in a SolidJS component:
```ts
const [optimistic, setOptimistic] = createSignal<State | null>(null);
const effective = () => optimistic() ?? serverState().data;
```
---
## Things That Don't Exist in Electron That SolidJS Gets for Free
| Feature | Electron workaround | SolidJS |
|---------|-------------------|---------|
| Fine-grained reactivity | `ThrottledStoreUpdater` (100 LOC) | `batch()` built-in |
| Cleanup on unmount | `useEffect` return + ref counting | `onCleanup()` |
| Derived state | `useMemo` | `createMemo()` or just `() => expr` |
| Conditional render | `&&` / ternary (JSX pitfalls) | `<Show>` (handles null/undefined safely) |
| List render | `.map()` | `<For>` (keyed, no full re-render on list change) |
---
## What the Full Migration Looks Like
### Things to keep / port directly
- The Zod event schemas (optional — validation is still useful)
- The REST API mutation pattern (`mutateMachine()`)
- The uplot chart components (framework-agnostic, just need a SolidJS wrapper)
- The unit/value display logic
### Things to discard
- `ThrottledStoreUpdater` — not needed
- `createNamespaceHookImplementation` — replaced by `createNamespace()`
- Zustand + Immer — replaced by SolidJS primitives
- `useStateOptimistic` — two lines in a component
- `useSyncExternalStore` bridge — not needed
- All the `useEffect` lifecycle boilerplate for socket management
### Things to re-evaluate
- **Electron itself** — this PoC runs as a plain browser app (`npm run dev`), served from the Rust backend. No Electron needed if the app runs in the browser. Consider just serving the built SolidJS app as static files from the Axum server.
- **i18next** — if the app is only used in one language, skip it
- **TanStack Router** → `@solidjs/router` handles the use case with less ceremony
- **react-hook-form + Zod** → SolidJS has no equivalent but the forms here are simple enough for bare `createSignal` + validation on submit
---
## Migration Scope Estimate
| Area | Effort | Notes |
|------|--------|-------|
| WebSocket infrastructure | Done (80 lines) | `src/lib/socketio.ts` |
| Main namespace | Done (~55 lines) | `src/namespaces/main.ts` |
| Per-machine namespace | ~25 lines each | See testMachine.ts as template |
| Machine pages (control/settings) | Medium | Mostly UI translation, logic simplifies |
| Graph components (uplot) | Medium | uplot is framework-agnostic; wrap in `onMount`/`onCleanup` |
| Sidebar + routing | Done | See App.tsx + Sidebar.tsx |
| Setup/EtherCAT pages | Small | Just display logic, no complex state |
| Presets | Medium | Local storage + fetch, straightforward |
| Electron IPC (update, theme, NixOS) | Low if dropped | Not needed if switching to browser app |
**Rough estimate: 2–3 weeks for a complete working port, 1 week for a fully functional PoC covering all machine types.**
The biggest time investment is translating the 15+ machine-specific UI pages, not the infrastructure — the infrastructure is already done and is dramatically simpler.
---
## Proof of Concept Structure
```
src/
├── lib/
│ ├── socketio.ts # Core: createNamespace() primitive (80 lines)
│ └── api.ts # REST mutations
├── namespaces/
│ ├── main.ts # Main namespace (~55 lines, vs ~170 in electron)
│ ├── testMachine.ts # Test machine namespace (~25 lines, vs ~77 in electron)
│ └── mock1.ts # Mock machine namespace (~60 lines incl. types)
├── pages/
│ ├── MachinesPage.tsx # Machine list
│ ├── TestMachineControlPage.tsx # LED control with optimistic updates
│ └── Mock1ControlPage.tsx # Mock machine: live values + freq/mode controls
├── components/
│ └── Sidebar.tsx # Navigation sidebar
└── App.tsx # Router + context provider
```
Run with: `npm run dev` (vite dev server, proxies to backend at localhost:3001)
---
## How Other Machines Would Be Implemented
Each machine follows the same two-step pattern:
### Step 1: Namespace file (`src/namespaces/{machine}.ts`)
Define a state type that mirrors the Rust `StateEvent` and `LiveValuesEvent` structs, then call `createNamespace()`:
```ts
export type MyMachineState = {
// fields from StateEvent
some_field: number | null;
mode: "Standby" | "Running" | null;
// fields from LiveValuesEvent (high-frequency, set separately)
live_measurement: number | null;
};
export function createMyMachineNamespace(serial: number) {
return createNamespace<MyMachineState>(
machineNamespacePath(VENDOR, MACHINE_ID, serial),
(event, set) => {
if (event.name === "StateEvent") {
set("some_field", event.data.some_field);
set("mode", event.data.mode);
} else if (event.name === "LiveValuesEvent") {
set("live_measurement", event.data.measurement);
}
},
{ some_field: null, mode: null, live_measurement: null },
);
}
```
Machine IDs (from `machines/src/lib.rs`):
| Machine | Vendor | Machine ID |
|---------|--------|-----------|
| TestMachine | 1 | 0x0033 (51) |
| Winder v2 | 1 | 0x0002 (2) |
| Extruder v2 | 1 | 0x0004 (4) |
| Laser v1 | 1 | 0x0006 (6) |
| Mock | 1 | 0x0007 (7) |
| Buffer v1 | 1 | 0x0008 (8) |
| Aquapath v1 | 1 | 0x0009 (9) |
| Wago Power v1 | 1 | 0x000A (10) |
| Extruder v3 | 1 | 0x0016 (22) |
### Step 2: Page component (`src/pages/{Machine}ControlPage.tsx`)
```tsx
export default function MyMachineControlPage() {
const params = useParams<{ serial: string }>();
const serial = () => parseInt(params.serial);
const [state] = createMyMachineNamespace(serial());
// Optimistic state per mutable field
const [optimisticMode, setOptimisticMode] = createSignal<Mode | null>(null);
const mode = () => optimisticMode() ?? state().mode ?? "Standby";
async function setMode(newMode: Mode) {
setOptimisticMode(newMode);
try {
await mutateMachine(
{ machine_identification: { vendor: VENDOR, machine: MACHINE_ID }, serial: serial() },
{ SetMode: newMode },
);
} catch { setOptimisticMode(null); }
}
return (/* JSX */);
}
```
### Step 3: Register route + sidebar entry
In `App.tsx`:
```tsx
<Route path="/machines/mymachine/:serial/control" component={MyMachineControlPage} />
<Route path="/machines/mymachine/:serial" component={...} />
```
In `Sidebar.tsx` and `MachinesPage.tsx`, add to the slug/label maps:
```ts
"1_X": "mymachine" // vendor_machine → slug
```
### Pages needed per machine (from electron reference)
| Machine | Pages in electron | Notes |
|---------|------------------|-------|
| TestMachine | Control | LED toggle (done) |
| Mock | Control | Frequencies + mode + live values (done) |
| Winder v2 | Control, Settings, Graphs, Presets, Manual | Complex: spool/traverse/puller motors |
| Extruder v2/v3 | Control, Settings, Graphs, Presets, Manual | Heating zones, inverter speed |
| Laser v1 | Control, Graphs, Presets, Manual | Diameter measurement, tolerance bands |
| Buffer v1 | Control, Settings | Motor control |
| Aquapath v1 | Control, Settings, Graphs | Temperature + analog output |
| Wago Power v1 | Control | Power channel display |
### Shared UI components still to build
The electron frontend has these reusable control components that would benefit from SolidJS ports:
- **`ControlCard`** — titled card wrapper (partially done with `.control-card` CSS)
- **`EditValue`** — numeric input with min/max/step/unit/default display
- **`SelectionGroup`** — segmented button toggle (done with `.mode-selector` pattern)
- **`TimeSeriesValueNumeric`** — displays latest value from a live stream
- **Graph wrapper** — uplot integration in SolidJS using `onMount`/`onCleanup`
- **`Topbar`** — tab navigation within a machine (Control / Graph / Manual / Presets)
- **`Page`** — page container with consistent padding/width
- **Preset system** — load/save named parameter sets via REST API
---
## Machine-Specific Complexity Notes
### Winder v2
Most complex machine. Has three motors (spool, traverse, puller), a tension arm, and a diameter visualisation component. The electron page (`Winder2ControlPage.tsx`) uses custom SVG components (`Spool.tsx`, `TraverseBar.tsx`, `TensionArm.tsx`). These are pure rendering components that can be ported as-is to SolidJS.
### Extruder v2 / v3
Has heating zones (up to 8 zones, each with temperature setpoint + PID state). The `HeatingZone.tsx` component in electron is shared between extruder2 and extruder3. The graph page plots temperature history via uplot.
### Laser v1
Has `DiameterVisualisation.tsx` — a custom SVG component showing the wire cross-section with tolerance bands. The control page shows live diameter + tolerance settings.
### Mock Machine
Simplest "real" machine with meaningful state: three sine wave generators with frequency controls and a running/standby mode. Ideal for testing the frontend with the mock backend (`cargo run --features mock-machine`).
+36
View File
@@ -0,0 +1,36 @@
## Usage
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
```bash
$ npm install # or pnpm install or yarn install
```
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
## Available Scripts
In the project directory, you can run:
### `npm run dev` or `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
### `npm run build`
Builds the app for production to the `dist` folder.<br>
It correctly bundles Solid in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
## Deployment
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
+156
View File
@@ -0,0 +1,156 @@
# SolidJS Frontend — Feature Parity Roadmap
This document tracks what still needs to be implemented to reach feature parity with the
existing Electron/React frontend. Checked items are complete in this PoC.
---
## Infrastructure
- [x] Socket.IO connection primitive (`createNamespace`)
- [x] Main namespace (machine discovery, EtherCAT status)
- [x] REST mutation helper (`mutateMachine`)
- [x] Sidebar with live machine list
- [x] Basic routing (`@solidjs/router`)
- [x] Dark theme CSS variables
- [ ] Topbar / tab navigation per machine (Control / Graph / Manual / Presets)
- [ ] Toast notifications (success/error feedback on mutations)
- [ ] Global error boundary
- [ ] Vite proxy config for production build (serve from Axum)
---
## Shared UI Components
- [x] Control card wrapper (`.control-card`)
- [x] Live value display (numeric, tabular-nums)
- [x] Frequency / numeric input with reset-to-default button
- [x] Mode selector (segmented button)
- [ ] `EditValue` — generalised numeric input with unit label, min/max/step, inline validation
- [ ] `SelectionGroup` — generalised segmented button for arbitrary enum values
- [ ] `TimeSeriesValueNumeric` — last-value display from a streaming signal
- [ ] uplot graph wrapper — `onMount` creates chart, reactive signals push data, `onCleanup` destroys
- [ ] Preset system UI — list presets, apply preset, save as preset (uses REST API)
- [ ] Manual/PDF viewer page
- [ ] EtherCAT setup page (device list, address assignments)
- [ ] Setup / configuration page (EtherCAT interface picker)
---
## Machine Pages
### Test Machine (vendor=1, machine=0x33/51)
- [x] Control page — LED toggle grid
### Mock Machine (vendor=1, machine=0x07/7)
- [x] Control page — live sine wave values, frequency inputs, mode selector
- [ ] Graph page — uplot with amplitude_sum + individual waves over time
### Winder v2 (vendor=1, machine=0x02/2)
- [ ] Namespace (`winder2Namespace.ts`)
- [ ] Control page — spool speed, traverse, puller, tension arm state
- [ ] Settings page — traverse limits, spool diameter config
- [ ] Graph page — speed/tension over time
- [ ] Presets page
- [ ] Manual page
### Extruder v2 (vendor=1, machine=0x04/4) / Extruder v3 (machine=0x16/22)
- [ ] Namespace
- [ ] Control page — heating zones (setpoints + actual temps), inverter speed
- [ ] Settings page — PID parameters per zone
- [ ] Graph page — temperature history
- [ ] Presets page
- [ ] Manual page
### Laser v1 (vendor=1, machine=0x06/6)
- [ ] Namespace
- [ ] Control page — live diameter, target diameter, tolerance band, in-tolerance indicator
- [ ] Graph page — diameter over time with tolerance lines
- [ ] Presets page
- [ ] Manual page
### Buffer v1 (vendor=1, machine=0x08/8)
- [ ] Namespace
- [ ] Control page — motor speed setpoints, actual speeds
- [ ] Settings page
### Aquapath v1 (vendor=1, machine=0x09/9)
- [ ] Namespace
- [ ] Control page — temperature setpoints, analog outputs, encoder position
- [ ] Settings page
- [ ] Graph page
### Wago Power v1 (vendor=1, machine=0x0A/10)
- [ ] Namespace
- [ ] Control page — power channel on/off display
### Digital Input Test Machine (vendor=1, machine=0x40/64)
- [ ] Namespace + Control page — digital input state display
### Wago 8ch DIO Test (vendor=1, machine=0x41/65)
- [ ] Namespace + Control page
### Wago DO Test (vendor=1, machine=0x0E/14)
- [ ] Namespace + Control page
### Wago AI Test (vendor=1, machine=0x36/54)
- [ ] Namespace + Control page
### IP20 Test Machine (vendor=1, machine=0x34/52)
- [ ] Namespace + Control page
### Motor Test Machine (vendor=1, machine=0x11/17)
- [ ] Namespace + Control page
### Analog Input Test Machine (vendor=1, machine=0x35/53)
- [ ] Namespace + Control page
---
## Pages Outside Machine Control
- [ ] Machines overview — currently functional, but uses `unknown-*` slug for unregistered machines
- [ ] EtherCAT status page — show `ethercatDevices` and `ethercatInterface` from main namespace
- [ ] Setup page — EtherCAT interface picker (currently hardcoded in backend)
- [ ] About / version page
---
## Nice-to-Have / Polish
- [ ] Loading skeleton states (instead of plain "Waiting…" text)
- [ ] Error state display when machine.error is set
- [ ] Reconnect indicator (socket disconnect/reconnect visual feedback)
- [ ] Responsive layout for smaller screens
- [ ] Keyboard shortcuts (already exists in electron via keybindings)
---
## What to Drop from Electron
These exist in the Electron app but are **not needed** in the SolidJS version:
- Electron IPC (auto-update, NixOS config, system tray) — not applicable in browser
- `ThrottledStoreUpdater` — replaced by SolidJS `batch()`
- Zustand + Immer — replaced by `createSignal`/`createStore`
- `useStateOptimistic` — two lines inline in each component
- `createNamespaceHookImplementation` — replaced by `createNamespace()`
- `react-hook-form` — bare signals are enough for the simple forms here
- i18next — skip unless multi-language is required
---
## Testing the PoC with Mock Mode
Start the backend in mock mode:
```sh
cargo run --features mock-machine
```
Start the frontend dev server:
```sh
cd qitech-control-gui && npm run dev
```
Open http://localhost:3000 — the mock machine should appear in the sidebar.
Navigate to `/machines/mock1/{serial}/control` to see the control page.
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
<title>Solid App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>
File diff suppressed because it is too large. Load diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "vite-template-solid",
"version": "0.0.0",
"description": "",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"license": "MIT",
"devDependencies": {
"solid-devtools": "^0.34.3",
"typescript": "^5.7.2",
"vite": "^7.1.4",
"vite-plugin-solid": "^2.11.8"
},
"dependencies": {
"@solidjs/router": "^0.15.4",
"socket.io-client": "^4.8.3",
"socket.io-msgpack-parser": "^3.0.2",
"solid-js": "^1.9.5",
"uplot": "^1.6.32",
"zod": "^4.3.6"
}
}
+1346
View File
File diff suppressed because it is too large. Load diff
+33
View File
@@ -0,0 +1,33 @@
.App {
text-align: center;
}
.logo {
animation: logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.link {
color: #b318f0;
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+81
View File
@@ -0,0 +1,81 @@
/**
* App root — sets up routing and creates the main namespace socket once.
*
* The main namespace signal is created here (at app scope) and provided via
* context so all pages can access it without prop drilling.
*
* Machine-specific namespaces are created inside their pages so they
* connect/disconnect with the page lifecycle automatically.
*
* No global store. No Zustand. No Immer. No ThrottledStoreUpdater.
* State lives in SolidJS signals, reactive updates are handled by batch().
*/
import { Router, Route } from "@solidjs/router";
import { createContext, useContext } from "solid-js";
import type { JSX } from "solid-js";
import Sidebar from "./components/Sidebar";
import MachinesPage from "./pages/MachinesPage";
import TestMachineControlPage from "./pages/TestMachineControlPage";
import Mock1ControlPage from "./pages/Mock1ControlPage";
import Mock1GraphPage from "./pages/Mock1GraphPage";
import { createMainNamespace, type MainState } from "./namespaces/main";
import "./styles.css";
// Context so nested pages can read main state without prop drilling
export const MainStateContext = createContext<() => MainState>(() => ({
machines: null,
ethercatDevices: null,
ethercatInterface: null,
}));
export function useMainState() {
return useContext(MainStateContext);
}
// Shared layout wrapper passed to Router as `root`
function AppLayout(props: { children?: JSX.Element }) {
const mainState = useMainState();
return (
<div class="app-layout">
<Sidebar mainState={mainState} />
<main class="app-content">{props.children}</main>
</div>
);
}
export default function App() {
// One socket for the main namespace, alive for the entire app lifetime.
// createMainNamespace() calls createNamespace() which calls onCleanup()
// — so the socket disconnects if App ever unmounts.
const [mainState] = createMainNamespace();
return (
<MainStateContext.Provider value={mainState}>
<Router root={AppLayout}>
<Route path="/" component={() => <MachinesPage mainState={mainState} />} />
<Route path="/machines/testmachine/:serial/control" component={TestMachineControlPage} />
<Route
path="/machines/testmachine/:serial"
component={() => (
<div class="page">
<h1>Test Machine</h1>
<p class="muted">Select Control from the sidebar.</p>
</div>
)}
/>
<Route path="/machines/mock1/:serial/control" component={Mock1ControlPage} />
<Route path="/machines/mock1/:serial/graph" component={Mock1GraphPage} />
<Route
path="/machines/mock1/:serial"
component={() => (
<div class="page">
<h1>Mock Machine</h1>
<p class="muted">Select Control from the sidebar.</p>
</div>
)}
/>
</Router>
</MainStateContext.Provider>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,143 @@
/**
* MiniGraph — compact inline sparkline for control page cards.
*
* 64 px tall, no cursor, no legend, no x-axis.
* Y-axis grid shows on the right side.
* Driven by a rAF loop reading directly from a SeriesBuffer — no reactive
* subscriptions, no intermediate array copies on every event.
*
* Props:
* - getBuffer: () => SeriesBuffer called each frame to read the ring buffer
* - timeWindow: number seconds of history to display (default: 10)
* - stroke: CSS colour string (default: var(--accent) / #6c8cff)
*/
import uPlot from "uplot";
import { onMount, onCleanup } from "solid-js";
import type { SeriesBuffer } from "../namespaces/mock1";
const HEIGHT = 64;
type Props = {
getBuffer: () => SeriesBuffer;
timeWindow?: number;
stroke?: string;
};
export default function MiniGraph(props: Props) {
let container!: HTMLDivElement;
let uplot: uPlot | null = null;
let rafId = 0;
let pendingWidth = 0;
function buildOpts(width: number): uPlot.Options {
return {
width,
height: HEIGHT,
padding: [4, 0, 4, 0],
cursor: { show: false },
legend: { show: false },
scales: {
x: { time: true },
y: { auto: false },
},
axes: [
{ show: false },
{
side: 1, // right side
size: 40,
stroke: "#717898",
grid: { stroke: "#2e3148", width: 0.5 },
ticks: { stroke: "#2e3148", width: 0.5 },
values: (_u, ticks) => ticks.map((v) => v.toFixed(2)),
},
],
series: [
{},
{
stroke: props.stroke ?? "#6c8cff",
width: 1.5,
spanGaps: true,
points: { show: false },
},
],
};
}
function frame() {
if (!uplot) { rafId = requestAnimationFrame(frame); return; }
if (pendingWidth > 0) {
uplot.setSize({ width: pendingWidth, height: HEIGHT });
pendingWidth = 0;
}
const buf = props.getBuffer();
const { times, values, length, head } = buf;
if (length === 0) { rafId = requestAnimationFrame(frame); return; }
const bufSize = times.length;
const isFull = length === bufSize;
const startIdx = isFull ? head : 0;
const tMax = times[(head - 1 + bufSize) % bufSize];
const tMin = tMax - (props.timeWindow ?? 10);
// Count entries in window
let count = 0;
for (let i = 0; i < length; i++) {
if (times[(startIdx + i) % bufSize] >= tMin) count++;
}
const outT = new Float64Array(count);
const outV = new Float64Array(count);
let out = 0;
let minY = Infinity, maxY = -Infinity;
for (let i = 0; i < length; i++) {
const idx = (startIdx + i) % bufSize;
const t = times[idx];
if (t >= tMin) {
outT[out] = t;
outV[out] = values[idx];
if (values[idx] < minY) minY = values[idx];
if (values[idx] > maxY) maxY = values[idx];
out++;
}
}
if (count === 0) { rafId = requestAnimationFrame(frame); return; }
const range = maxY - minY || 1;
const pad = range * 0.15;
uplot.batch(() => {
uplot!.setData([outT, outV], false);
uplot!.setScale("x", { min: tMin, max: tMax });
uplot!.setScale("y", { min: minY - pad, max: maxY + pad });
});
rafId = requestAnimationFrame(frame);
}
onMount(() => {
const ro = new ResizeObserver(([entry]) => {
const w = Math.floor(entry.contentRect.width);
if (w === 0) return;
if (!uplot) {
uplot = new uPlot(buildOpts(w), [new Float64Array(0), new Float64Array(0)], container);
rafId = requestAnimationFrame(frame);
} else {
pendingWidth = w;
}
});
ro.observe(container);
onCleanup(() => {
cancelAnimationFrame(rafId);
ro.disconnect();
uplot?.destroy();
uplot = null;
});
});
return <div ref={container} class="minigraph-wrap" />;
}
@@ -0,0 +1,92 @@
import { A } from "@solidjs/router";
import { For, Show, createMemo } from "solid-js";
import type { MainState, MachineObj } from "../namespaces/main";
type Props = {
mainState: () => MainState;
};
function machineLabel(m: MachineObj): string {
const { vendor, machine } = m.machine_identification_unique.machine_identification;
const labels: Record<string, string> = {
"1_51": "Test Machine", // 0x0033
"1_2": "Winder v2", // 0x0002
"1_4": "Extruder v2", // 0x0004
"1_6": "Laser v1", // 0x0006
"1_7": "Mock", // 0x0007
"1_8": "Buffer v1", // 0x0008
"1_9": "Aquapath v1", // 0x0009
"1_10": "Wago Power", // 0x000A
"1_22": "Extruder v3", // 0x0016
};
return labels[`${vendor}_${machine}`] ?? `Machine ${vendor}/${machine}`;
}
function machineSlug(m: MachineObj): string {
const { vendor, machine } = m.machine_identification_unique.machine_identification;
const slugs: Record<string, string> = {
"1_51": "testmachine", // 0x0033
"1_2": "winder2", // 0x0002
"1_4": "extruder2", // 0x0004
"1_6": "laser1", // 0x0006
"1_7": "mock1", // 0x0007
"1_8": "buffer1", // 0x0008
"1_9": "aquapath1", // 0x0009
"1_10": "wago_power1", // 0x000A
"1_22": "extruder3", // 0x0016
};
return slugs[`${vendor}_${machine}`] ?? `unknown-${vendor}-${machine}`;
}
// Slugs that have a graph page implemented
const SLUGS_WITH_GRAPH = new Set(["mock1"]);
export default function Sidebar(props: Props) {
const machines = createMemo(() => props.mainState().machines ?? []);
return (
<nav class="sidebar">
<div class="sidebar-brand">
<span>QiTech Control</span>
</div>
<div class="sidebar-section">
<A href="/" class="sidebar-link" end>
Machines
</A>
</div>
<Show when={machines().length > 0}>
<div class="sidebar-section">
<div class="sidebar-section-label">Active Machines</div>
<For each={machines()}>
{(m) => {
const slug = machineSlug(m);
const serial = m.machine_identification_unique.serial;
return (
<div class="sidebar-machine">
<A href={`/machines/${slug}/${serial}`} class="sidebar-link" end>
{machineLabel(m)} <span class="sidebar-serial">#{serial}</span>
</A>
<A href={`/machines/${slug}/${serial}/control`} class="sidebar-sublink">
Control
</A>
<Show when={SLUGS_WITH_GRAPH.has(slug)}>
<A href={`/machines/${slug}/${serial}/graph`} class="sidebar-sublink">
Graph
</A>
</Show>
</div>
);
}}
</For>
</div>
</Show>
<div class="sidebar-footer">
<div class={`connection-dot ${props.mainState().machines !== null ? "connected" : "disconnected"}`} />
<span>{props.mainState().machines !== null ? "Connected" : "Connecting…"}</span>
</div>
</nav>
);
}
@@ -0,0 +1,147 @@
/**
* UPlotGraph — SolidJS wrapper around uPlot with a rolling time window.
*
* Design principles:
* - The uPlot instance is driven by a rAF loop, NOT by reactive effects.
* This caps redraws at the display refresh rate (~60 fps) regardless of
* how fast incoming data arrives (e.g. 10 Hz socket events).
* - The caller provides a `getSeriesData` callback that returns the current
* ring-buffer slices. The callback is called once per animation frame so
* it must be cheap (just a typed-array slice of the visible window).
* - `timeWindow` (seconds) controls how much history is visible. The x-axis
* scale is set to [latestTimestamp - timeWindow, latestTimestamp] every
* frame, giving the rolling-window effect.
* - Resize is handled by ResizeObserver → setSize on next rAF frame.
*/
import uPlot from "uplot";
import "uplot/dist/uPlot.min.css";
import { onMount, onCleanup } from "solid-js";
export type HorizLine = {
value: number;
color: string;
dash?: boolean;
label?: string;
};
type Props = {
/** Called each animation frame. Return aligned data arrays (timestamps in seconds). */
getSeriesData: () => uPlot.AlignedData;
series: uPlot.Series[];
unit?: string;
height?: number;
/** Visible time window in seconds (default: 30 s). */
timeWindow?: number;
lines?: HorizLine[];
};
export default function UPlotGraph(props: Props) {
let container!: HTMLDivElement;
let uplot: uPlot | null = null;
let rafId = 0;
let pendingWidth = 0;
function buildOpts(width: number): uPlot.Options {
const height = props.height ?? 240;
const lines = props.lines ?? [];
return {
width,
height,
cursor: { sync: { key: "graphs" } },
scales: {
x: { time: true },
y: { auto: true },
},
axes: [
{
stroke: "#717898",
grid: { stroke: "#2e3148", width: 1 },
ticks: { stroke: "#2e3148" },
},
{
label: props.unit,
stroke: "#717898",
grid: { stroke: "#2e3148", width: 1 },
ticks: { stroke: "#2e3148" },
},
],
series: [
{},
...props.series,
],
hooks: lines.length > 0 ? {
drawSeries: [
(u) => {
const ctx = u.ctx;
ctx.save();
for (const line of lines) {
const yPos = Math.round(u.valToPos(line.value, "y", true));
ctx.beginPath();
ctx.strokeStyle = line.color;
ctx.lineWidth = 1.5;
if (line.dash) ctx.setLineDash([6, 3]);
ctx.moveTo(u.bbox.left, yPos);
ctx.lineTo(u.bbox.left + u.bbox.width, yPos);
ctx.stroke();
ctx.setLineDash([]);
if (line.label) {
ctx.fillStyle = line.color;
ctx.font = "11px Inter, system-ui, sans-serif";
ctx.fillText(line.label, u.bbox.left + 4, yPos - 4);
}
}
ctx.restore();
},
],
} : {},
};
}
function frame() {
if (!uplot) { rafId = requestAnimationFrame(frame); return; }
// Apply any pending resize first
if (pendingWidth > 0) {
uplot.setSize({ width: pendingWidth, height: props.height ?? 240 });
pendingWidth = 0;
}
const data = props.getSeriesData();
const times = data[0] as Float64Array | number[];
const n = times.length;
if (n > 0) {
const tMax = times[n - 1];
const tMin = tMax - (props.timeWindow ?? 30);
uplot.setData(data, false); // false = don't auto-scale x
uplot.setScale("x", { min: tMin, max: tMax });
}
rafId = requestAnimationFrame(frame);
}
onMount(() => {
const ro = new ResizeObserver(([entry]) => {
const w = Math.floor(entry.contentRect.width);
if (w === 0) return;
if (!uplot) {
uplot = new uPlot(buildOpts(w), props.getSeriesData(), container);
rafId = requestAnimationFrame(frame);
} else {
// Defer resize to the rAF loop to avoid mid-paint artifacts
pendingWidth = w;
}
});
ro.observe(container);
onCleanup(() => {
cancelAnimationFrame(rafId);
ro.disconnect();
uplot?.destroy();
uplot = null;
});
});
return <div ref={container} class="uplot-wrap" />;
}
+13
View File
@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more