Skip to content

Commit b5a93e0

Browse files
authored
Merge develop into main for 1.5.0 release (#138)
* #115 updated installer project to Wix 5.0.2 * #116 updated license copyright year to 2025 * #117 updated to Wix Toolset 5.0.2 * #117 removed unnecessary instruction * #93 finished removing console mode * #117 use newer Wix feature for detecting .NET 4.8 * #117 restored missing file in project * #92 added basic manual update check * #92 updated to use Newtonsoft.Json and added to installer * #124 intial GenAI implementation and tests. Not reviewed. * #96 measure the rate at which resistance packets are collected and show in the test controller * #96 update resistance packet rate display to once per second * #55 prevent exception on disconnection * #125 adjusted buttons list control * #96 thread safety and UI adjustments * #126 assembly metadata update * #127 crank diagnostics tab and chart * #128 added installer bitmaps and made it support per-use/per-machine installation scope * #92 added button to go to the releases page when an update is detected * #129 URL updates * #129 URL update * #91 removed midpoint option * #130 fixed trip meter tests * #118 only use the power table for power readings * Update README.md * #132 connect crank to flywheel in test controller. * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * #43 put crank calculations behind an interface and extension methods. * #126 updated copyright year to 2026 * #126 package updates * Update README.md * #126 hide telemetry saving in release * #129 readme update * #78 some robustness regarding deltas and period length, and other alterations * #93 removed reference to console app icon from installer code
1 parent 183b867 commit b5a93e0

59 files changed

Lines changed: 1593 additions & 810 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2021-2025 David Mason
3+
Copyright (c) 2021-2026 David Mason
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

LICENSE.rtf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{\*\generator Riched20 10.0.19041}\viewkind4\uc1
33
\pard\f0\fs22\lang2057 MIT License\par
44
\par
5-
Copyright (c) 2021-2025 David Mason\par
5+
Copyright (c) 2021-2026 David Mason\par
66
\par
77
Permission is hereby granted, free of charge, to any person obtaining a copy\par
88
of this software and associated documentation files (the "Software"), to deal\par

README.md

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ It is not necessary to use the diagnostic utility from this repository to utilis
1010

1111
---
1212

13-
The "Trixter" company or companies that made this bike and the X-Dream mountain biking simulator are now dissolved.
13+
The "Trixter" company or companies that made this bike and the X-Dream mountain biking simulator provided with the V1 bike are now dissolved.
1414
This repository and the associated discussions / wikis are not associated with these companies or any that acquired them.
1515

1616
---
@@ -23,29 +23,45 @@ Help with the X-Dream mountain biking simulation software itself can be found in
2323

2424
A .NET Standard 2.0 client API which provides classes to:
2525
- read status from and send resistance to a Trixter X-Dream Bike.
26-
- emulate a Trixter V1 X-Dream Bike.
26+
- emulate a Trixter X-Dream Bike.
2727

2828
# Trixter.XDream.Diagnostics
2929

3030
A replacement for the console and UI test applications that were supplied with the X-Dream software.
3131

3232
So far the improvements over the originals are:
33-
- console and GUI options in a single application
33+
- graphic user interface
3434
- detects the COM port the bike is on
3535
- detects backpedalling and shows the crank RPM in the backwards direction
3636
- calculates flywheel and crank cumulative revolutions.
3737
- estimate of the power being applied to the flywheel from a power table of crank speed and resistance to power.
38-
- estimate of expended energy
38+
- estimate of energy expended by rider
39+
- shows an animated graph of crank positions covered to indicate sensor problems
3940
- is maintainable
4041

41-
The default mode of the utility has a graphic user interface that shows the state of the various controls and a slider bar to set flywheel resistance.
42+
## Details Tab
43+
44+
The utility has a graphic user interface that shows the state of the various controls and a slider bar to set flywheel resistance.
4245
Each brake applies 50% flywheel resistance.
4346

44-
<img src="https://github.com/user-attachments/assets/d33ca02c-bf63-4199-a4d3-a1334d5d861a" align="center" width="550px" ></a>
47+
![image](https://github.com/user-attachments/assets/ef9ad901-cbdc-49e5-85c2-0d8c1befb8f7)
48+
49+
## Crank Tab
50+
51+
The Crank tab shows information about activation from the crank position sensor, to show graphically if there is a problem with the crank sensor.
52+
This could also be done by turning the crank very slowly and watching the crank position number on the Diagnostics tab or the test utility supplied with the X-Dream software.
53+
54+
https://github.com/user-attachments/assets/91aed2b2-9df8-48e2-b423-215dfc9cc127
55+
56+
## Driver Tab
57+
58+
This tab provides an opinion on the state of the bike's driver.
59+
60+
![image](https://github.com/user-attachments/assets/ebefb94b-fa13-4ba3-b6ae-a7533c6b3fac)
61+
62+
63+
4564

4665

47-
The utility also has a console mode to provide a similar experience to the original.
48-
If the utility is invoked with the --console command line option, or through the program menu short cut for the console mode, it shows a console application.
4966

50-
<img src="https://github.com/user-attachments/assets/3b6dfc6f-fbf2-4cc4-956c-261a198fb4f8" align="center" width="750px" ></a>
5167

Scripts/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.4.1
1+
1.5.0
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Trixter.XDream.API.Filters;
4+
5+
namespace Trixter.XDream.API.Testing.Experimental
6+
{
7+
public class ActivationMonitor
8+
{
9+
private readonly Queue<Sample> samples = new Queue<Sample>();
10+
private double activationThreshold = 0.7;
11+
private double deactivationThreshold = 0.3;
12+
private int samplingPeriod = 1000;
13+
private bool isCurrentlyActive = false;
14+
15+
private int activeSampleCount = 0; // Running sum of active samples
16+
17+
18+
/// <summary>
19+
/// Represents a sample with a timestamp and activation state.
20+
/// </summary>
21+
private class Sample
22+
{
23+
public long Timestamp { get; }
24+
public bool IsActive { get; }
25+
public Sample(long timestamp, bool isActive)
26+
{
27+
this.Timestamp = timestamp;
28+
this.IsActive = isActive;
29+
}
30+
}
31+
32+
/// <summary>
33+
/// Function used to get the current time in ticks.
34+
/// Defaults to DateTime.UtcNow.Ticks.
35+
/// </summary>
36+
private readonly Func<long> getTime;
37+
38+
protected virtual void OnActivationChanged(bool e) => ActivationChanged?.Invoke(this, e);
39+
40+
/// <summary>
41+
/// Event triggered on activation or deactivation.
42+
/// The bool parameter is true for activation and false for deactivation.
43+
/// </summary>
44+
public event EventHandler<bool> ActivationChanged;
45+
46+
/// <summary>
47+
/// Gets the current activation state.
48+
/// </summary>
49+
public bool IsActive => this.isCurrentlyActive;
50+
51+
/// <summary>
52+
/// Gets or sets the activation threshold.
53+
/// </summary>
54+
public double ActivationThreshold
55+
{
56+
get => this.activationThreshold;
57+
set => this.activationThreshold = Clamp(value, this.deactivationThreshold, 1.0);
58+
}
59+
60+
/// <summary>
61+
/// Gets or sets the deactivation threshold.
62+
/// </summary>
63+
public double DeactivationThreshold
64+
{
65+
get => this.deactivationThreshold;
66+
set => this.deactivationThreshold = Clamp(value, 0.0, this.activationThreshold);
67+
}
68+
69+
/// <summary>
70+
/// Gets or sets the sampling period in milliseconds.
71+
/// </summary>
72+
public int SamplingPeriod
73+
{
74+
get => (int)this.samplingPeriod;
75+
private set => this.samplingPeriod = Math.Max(value, 1);
76+
}
77+
78+
/// <summary>
79+
/// Constructor with optional time provider for testing.
80+
/// </summary>
81+
/// <param name="samplingPeriod">The sample period in the unit of the <see cref="getTime"/> function.</param>
82+
/// <param name="getTime">A function providing the current time in ticks (for testing purposes).</param>
83+
public ActivationMonitor(int samplingPeriod, Func<long> getTime = null)
84+
{
85+
this.getTime = getTime ?? (() => DateTime.UtcNow.Ticks);
86+
this.SamplingPeriod = samplingPeriod;
87+
}
88+
89+
/// <summary>
90+
/// Updates the activation state and triggers the event if thresholds are crossed.
91+
/// </summary>
92+
/// <param name="isActive">Indicates whether the flag is active.</param>
93+
public void Update(bool isActive)
94+
{
95+
this.samples.Enqueue(new Sample(this.getTime(), isActive));
96+
97+
if (isActive)
98+
{
99+
this.activeSampleCount++;
100+
}
101+
102+
this.CleanUpOldSamples();
103+
double meanActivation = this.CalculateMeanActivation();
104+
105+
if (!this.isCurrentlyActive && meanActivation >= this.activationThreshold)
106+
{
107+
this.isCurrentlyActive = true;
108+
this.OnActivationChanged(true);
109+
}
110+
else if (this.isCurrentlyActive && meanActivation < this.deactivationThreshold)
111+
{
112+
this.isCurrentlyActive = false;
113+
this.OnActivationChanged(false);
114+
}
115+
}
116+
117+
/// <summary>
118+
/// Removes outdated samples and adjusts the active sample count.
119+
/// </summary>
120+
private void CleanUpOldSamples()
121+
{
122+
long cutoffTicks = this.getTime() - this.samplingPeriod;
123+
124+
while (this.samples.Count > 0 && this.samples.Peek().Timestamp < cutoffTicks)
125+
{
126+
var expiredSample = this.samples.Dequeue();
127+
128+
if (expiredSample.IsActive)
129+
{
130+
this.activeSampleCount--;
131+
}
132+
}
133+
}
134+
135+
/// <summary>
136+
/// Calculates the mean activation over the current sample window.
137+
/// </summary>
138+
private double CalculateMeanActivation()
139+
{
140+
if (this.samples.Count == 0) return 0.0;
141+
142+
return (double)this.activeSampleCount / this.samples.Count;
143+
}
144+
145+
/// <summary>
146+
/// Clamps the value between the specified minimum and maximum.
147+
/// </summary>
148+
private static double Clamp(double value, double min, double max)
149+
{
150+
if (value < min) return min;
151+
if (value > max) return max;
152+
return value;
153+
}
154+
}
155+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using NUnit.Framework;
2+
using System;
3+
using System.Threading;
4+
5+
namespace Trixter.XDream.API.Testing.Experimental
6+
{
7+
[TestFixture]
8+
public class ActivationMonitorTests
9+
{
10+
private ActivationMonitor monitor;
11+
private bool eventTriggered;
12+
private bool eventState;
13+
14+
private long currentTime;
15+
16+
[SetUp]
17+
public void Setup()
18+
{
19+
currentTime = 9834982; // Some arbitrary time
20+
21+
monitor = new ActivationMonitor(1000, () => currentTime)
22+
{
23+
ActivationThreshold = 0.6,
24+
DeactivationThreshold = 0.4,
25+
};
26+
27+
eventTriggered = false;
28+
eventState = false;
29+
30+
monitor.ActivationChanged += (s, e) =>
31+
{
32+
eventTriggered = true;
33+
eventState = e;
34+
};
35+
}
36+
37+
private void AdvanceTimeByMilliseconds(int milliseconds)
38+
{
39+
currentTime += TimeSpan.FromMilliseconds(milliseconds).Ticks;
40+
}
41+
42+
[Test]
43+
public void Update_TriggersActivation_WhenThresholdIsExceeded()
44+
{
45+
monitor.Update(true);
46+
monitor.Update(true);
47+
monitor.Update(true);
48+
49+
AdvanceTimeByMilliseconds(10);
50+
51+
Assert.That(eventTriggered, Is.True);
52+
Assert.That(eventState, Is.True);
53+
Assert.That(monitor.IsActive, Is.True);
54+
}
55+
56+
[Test]
57+
public void Update_TriggersDeactivation_WhenThresholdFallsBelow()
58+
{
59+
monitor.Update(true);
60+
monitor.Update(true);
61+
monitor.Update(true);
62+
63+
AdvanceTimeByMilliseconds(10);
64+
Assert.That(monitor.IsActive, Is.True);
65+
66+
// Simulate deactivation by adding false samples
67+
eventTriggered = false;
68+
monitor.Update(false);
69+
monitor.Update(false);
70+
monitor.Update(false);
71+
72+
AdvanceTimeByMilliseconds(10);
73+
74+
Assert.That(eventTriggered, Is.True);
75+
Assert.That(eventState, Is.False);
76+
Assert.That(monitor.IsActive, Is.False);
77+
}
78+
79+
[Test]
80+
public void Update_DoesNotTriggerEvent_IfStateRemainsSame()
81+
{
82+
monitor.Update(true);
83+
monitor.Update(true);
84+
85+
AdvanceTimeByMilliseconds(10);
86+
87+
Assert.That(eventTriggered, Is.True);
88+
Assert.That(monitor.IsActive, Is.True);
89+
90+
eventTriggered = false;
91+
92+
monitor.Update(true);
93+
monitor.Update(true);
94+
95+
AdvanceTimeByMilliseconds(10);
96+
97+
Assert.That(eventTriggered, Is.False);
98+
Assert.That(monitor.IsActive, Is.True);
99+
}
100+
101+
[Test]
102+
public void IsActive_ReturnsFalse_Initially()
103+
{
104+
Assert.That(monitor.IsActive, Is.False);
105+
}
106+
107+
[Test]
108+
public void Thresholds_CanBeModified()
109+
{
110+
monitor.ActivationThreshold = 0.8;
111+
monitor.DeactivationThreshold = 0.2;
112+
113+
Assert.That(monitor.ActivationThreshold, Is.EqualTo(0.8));
114+
Assert.That(monitor.DeactivationThreshold, Is.EqualTo(0.2));
115+
}
116+
}
117+
118+
}

Trixter.XDream.API.Testing/Experimental/CrankMeterExperiments.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
using System.Collections.Generic;
44
using System.Linq;
55
using Trixter.XDream.API.Communications;
6+
using Trixter.XDream.API.Crank;
67
using Trixter.XDream.API.Meters;
78

89
namespace Trixter.XDream.API.Testing.Experimental
910
{
1011
[TestFixture]
1112
public class CrankMeterExperiments
1213
{
14+
private static readonly ICrankSpecification crankSpec = XDreamCrankSpecification.Default;
15+
1316
[Explicit]
1417
[Test]
1518
public void BackAndForwardWithRealData()
@@ -44,7 +47,7 @@ public void CorrelateCrankSpeed1()
4447

4548
HashSet<int>[] rpms = Enumerable.Range(0, 1000).Select(i => new HashSet<int>()).ToArray(); // If we get a calculated 1000RPM, something is unnatural
4649

47-
ICrankMeter cs = new PositionalCrankMeter();
50+
ICrankMeter cs = new PositionalCrankMeter(crankSpec);
4851
XDreamState last = messages[0];
4952

5053
cs.AddData(last);
@@ -74,7 +77,7 @@ public void CorrelateCrankSpeed2()
7477
{
7578
XDreamMessage[] messages = XDreamMessageIO.Read(Resources.flywheel_crank_messages);
7679

77-
StatisticsGatheringCrankMeter cm = new StatisticsGatheringCrankMeter(new PositionalCrankMeter(), 500);
80+
StatisticsGatheringCrankMeter cm = new StatisticsGatheringCrankMeter(new PositionalCrankMeter(crankSpec), 500);
7881

7982
Array.ForEach(messages, cm.AddData);
8083

0 commit comments

Comments
 (0)