Added Servo mode
Refactored client to allow more simulators
11
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
AssettoCorsa/source/__history
|
Client/source/__history
|
||||||
AssettoCorsa/*.dproj.local
|
Client/*.dproj.local
|
||||||
AssettoCorsa/*.identcache
|
Client/*.identcache
|
||||||
AssettoCorsa/bin
|
Client/bin
|
||||||
AssettoCorsa/lib
|
Client/lib
|
||||||
|
Client/__history
|
@ -1,19 +0,0 @@
|
|||||||
program AssettoCorsaSF;
|
|
||||||
|
|
||||||
uses
|
|
||||||
Vcl.Forms,
|
|
||||||
MainFrm in 'source\MainFrm.pas' {MainForm},
|
|
||||||
AssettoCorsa.SharedMemory in 'source\AssettoCorsa.SharedMemory.pas',
|
|
||||||
CPort in 'source\CPort.pas';
|
|
||||||
|
|
||||||
{$R *.res}
|
|
||||||
|
|
||||||
var
|
|
||||||
MainForm: TMainForm;
|
|
||||||
|
|
||||||
begin
|
|
||||||
Application.Initialize;
|
|
||||||
Application.MainFormOnTaskbar := True;
|
|
||||||
Application.CreateForm(TMainForm, MainForm);
|
|
||||||
Application.Run;
|
|
||||||
end.
|
|
@ -1,115 +0,0 @@
|
|||||||
object MainForm: TMainForm
|
|
||||||
Left = 0
|
|
||||||
Top = 0
|
|
||||||
BorderIcons = [biSystemMenu, biMinimize]
|
|
||||||
BorderStyle = bsSingle
|
|
||||||
Caption = 'SimulatorFans - Assetto Corsa'
|
|
||||||
ClientHeight = 390
|
|
||||||
ClientWidth = 645
|
|
||||||
Color = clBtnFace
|
|
||||||
Font.Charset = DEFAULT_CHARSET
|
|
||||||
Font.Color = clWindowText
|
|
||||||
Font.Height = -11
|
|
||||||
Font.Name = 'Tahoma'
|
|
||||||
Font.Style = []
|
|
||||||
OldCreateOrder = False
|
|
||||||
OnCreate = FormCreate
|
|
||||||
PixelsPerInch = 96
|
|
||||||
TextHeight = 13
|
|
||||||
object PortComboBox: TComboBox
|
|
||||||
Left = 24
|
|
||||||
Top = 24
|
|
||||||
Width = 201
|
|
||||||
Height = 21
|
|
||||||
Style = csDropDownList
|
|
||||||
TabOrder = 0
|
|
||||||
end
|
|
||||||
object RefreshPortsButton: TButton
|
|
||||||
Left = 231
|
|
||||||
Top = 23
|
|
||||||
Width = 75
|
|
||||||
Height = 23
|
|
||||||
Caption = 'Refresh'
|
|
||||||
TabOrder = 1
|
|
||||||
OnClick = RefreshPortsButtonClick
|
|
||||||
end
|
|
||||||
object Button1: TButton
|
|
||||||
Left = 192
|
|
||||||
Top = 176
|
|
||||||
Width = 75
|
|
||||||
Height = 25
|
|
||||||
Caption = 'Connect'
|
|
||||||
TabOrder = 2
|
|
||||||
OnClick = Button1Click
|
|
||||||
end
|
|
||||||
object Button2: TButton
|
|
||||||
Left = 288
|
|
||||||
Top = 176
|
|
||||||
Width = 75
|
|
||||||
Height = 25
|
|
||||||
Caption = 'Fan 1 full'
|
|
||||||
TabOrder = 3
|
|
||||||
OnClick = Button2Click
|
|
||||||
end
|
|
||||||
object Button3: TButton
|
|
||||||
Left = 384
|
|
||||||
Top = 176
|
|
||||||
Width = 75
|
|
||||||
Height = 25
|
|
||||||
Caption = 'Fan 2 full'
|
|
||||||
TabOrder = 4
|
|
||||||
OnClick = Button3Click
|
|
||||||
end
|
|
||||||
object Button4: TButton
|
|
||||||
Left = 480
|
|
||||||
Top = 176
|
|
||||||
Width = 75
|
|
||||||
Height = 25
|
|
||||||
Caption = 'Off'
|
|
||||||
TabOrder = 5
|
|
||||||
OnClick = Button4Click
|
|
||||||
end
|
|
||||||
object Button5: TButton
|
|
||||||
Left = 288
|
|
||||||
Top = 207
|
|
||||||
Width = 75
|
|
||||||
Height = 25
|
|
||||||
Caption = 'Fan 1 half'
|
|
||||||
TabOrder = 6
|
|
||||||
OnClick = Button5Click
|
|
||||||
end
|
|
||||||
object Button6: TButton
|
|
||||||
Left = 384
|
|
||||||
Top = 207
|
|
||||||
Width = 75
|
|
||||||
Height = 25
|
|
||||||
Caption = 'Fan 2 half'
|
|
||||||
TabOrder = 7
|
|
||||||
OnClick = Button6Click
|
|
||||||
end
|
|
||||||
object Edit1: TEdit
|
|
||||||
Left = 288
|
|
||||||
Top = 253
|
|
||||||
Width = 75
|
|
||||||
Height = 21
|
|
||||||
TabOrder = 8
|
|
||||||
Text = '0'
|
|
||||||
end
|
|
||||||
object Button7: TButton
|
|
||||||
Left = 288
|
|
||||||
Top = 280
|
|
||||||
Width = 171
|
|
||||||
Height = 25
|
|
||||||
Caption = 'Set'
|
|
||||||
TabOrder = 9
|
|
||||||
OnClick = Button7Click
|
|
||||||
end
|
|
||||||
object Edit2: TEdit
|
|
||||||
Left = 384
|
|
||||||
Top = 253
|
|
||||||
Width = 75
|
|
||||||
Height = 21
|
|
||||||
TabOrder = 10
|
|
||||||
Text = '0'
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,208 +0,0 @@
|
|||||||
unit MainFrm;
|
|
||||||
|
|
||||||
interface
|
|
||||||
uses
|
|
||||||
System.Classes,
|
|
||||||
System.SysUtils,
|
|
||||||
Vcl.Controls,
|
|
||||||
Vcl.ExtCtrls,
|
|
||||||
Vcl.Forms,
|
|
||||||
Vcl.StdCtrls,
|
|
||||||
|
|
||||||
CPort;
|
|
||||||
|
|
||||||
|
|
||||||
type
|
|
||||||
TMainForm = class(TForm)
|
|
||||||
PortComboBox: TComboBox;
|
|
||||||
RefreshPortsButton: TButton;
|
|
||||||
Button1: TButton;
|
|
||||||
Button2: TButton;
|
|
||||||
Button3: TButton;
|
|
||||||
Button4: TButton;
|
|
||||||
Button5: TButton;
|
|
||||||
Button6: TButton;
|
|
||||||
Edit1: TEdit;
|
|
||||||
Button7: TButton;
|
|
||||||
Edit2: TEdit;
|
|
||||||
|
|
||||||
procedure FormCreate(Sender: TObject);
|
|
||||||
procedure RefreshPortsButtonClick(Sender: TObject);
|
|
||||||
procedure Button1Click(Sender: TObject);
|
|
||||||
procedure Button2Click(Sender: TObject);
|
|
||||||
procedure Button3Click(Sender: TObject);
|
|
||||||
procedure Button4Click(Sender: TObject);
|
|
||||||
procedure Button5Click(Sender: TObject);
|
|
||||||
procedure Button6Click(Sender: TObject);
|
|
||||||
procedure Button7Click(Sender: TObject);
|
|
||||||
private
|
|
||||||
FComPort: TComPort;
|
|
||||||
FReceived: string;
|
|
||||||
FOnResponse: TProc<string>;
|
|
||||||
|
|
||||||
procedure OnReceiveChar(Sender: TObject; Count: Integer);
|
|
||||||
|
|
||||||
procedure RefreshPorts;
|
|
||||||
procedure SendCommand(const ACommand: string; AOnResponse: TProc<string>);
|
|
||||||
|
|
||||||
property ComPort: TComPort read FComPort;
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
implementation
|
|
||||||
|
|
||||||
uses
|
|
||||||
System.RegularExpressions, Vcl.Dialogs;
|
|
||||||
|
|
||||||
{$R *.dfm}
|
|
||||||
|
|
||||||
procedure TMainForm.Button1Click(Sender: TObject);
|
|
||||||
begin
|
|
||||||
SendCommand('>Info',
|
|
||||||
procedure(Response: string)
|
|
||||||
var
|
|
||||||
match: TMatch;
|
|
||||||
|
|
||||||
begin
|
|
||||||
match := TRegEx.Match(Response, '<Info:Fans=(\d+)', [roSingleLine]);
|
|
||||||
if match.Success then
|
|
||||||
ShowMessage('Connected, got ' + match.Groups[1].Value + ' fan(s)')
|
|
||||||
else
|
|
||||||
ShowMessage('Invalid response: ' + Response);
|
|
||||||
end);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.Button2Click(Sender: TObject);
|
|
||||||
begin
|
|
||||||
Edit1.Text := '255';
|
|
||||||
SendCommand('>SetFans:255,0',
|
|
||||||
procedure(Response: string)
|
|
||||||
begin
|
|
||||||
if Response <> '<SetFans' then
|
|
||||||
ShowMessage('Invalid response: ' + Response);
|
|
||||||
end);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.Button3Click(Sender: TObject);
|
|
||||||
begin
|
|
||||||
Edit2.Text := '255';
|
|
||||||
SendCommand('>SetFans:0,255',
|
|
||||||
procedure(Response: string)
|
|
||||||
begin
|
|
||||||
if Response <> '<SetFans' then
|
|
||||||
ShowMessage('Invalid response: ' + Response);
|
|
||||||
end);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.Button4Click(Sender: TObject);
|
|
||||||
begin
|
|
||||||
Edit1.Text := '0';
|
|
||||||
Edit2.Text := '0';
|
|
||||||
SendCommand('>SetFans:0,0',
|
|
||||||
procedure(Response: string)
|
|
||||||
begin
|
|
||||||
if Response <> '<SetFans' then
|
|
||||||
ShowMessage('Invalid response: ' + Response);
|
|
||||||
end);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.Button5Click(Sender: TObject);
|
|
||||||
begin
|
|
||||||
Edit1.Text := '128';
|
|
||||||
SendCommand('>SetFans:128,0',
|
|
||||||
procedure(Response: string)
|
|
||||||
begin
|
|
||||||
if Response <> '<SetFans' then
|
|
||||||
ShowMessage('Invalid response: ' + Response);
|
|
||||||
end);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.Button6Click(Sender: TObject);
|
|
||||||
begin
|
|
||||||
Edit2.Text := '128';
|
|
||||||
SendCommand('>SetFans:0,128',
|
|
||||||
procedure(Response: string)
|
|
||||||
begin
|
|
||||||
if Response <> '<SetFans' then
|
|
||||||
ShowMessage('Invalid response: ' + Response);
|
|
||||||
end);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.Button7Click(Sender: TObject);
|
|
||||||
begin
|
|
||||||
SendCommand('>SetFans:' + Edit1.Text + ',' + Edit2.Text,
|
|
||||||
procedure(Response: string)
|
|
||||||
begin
|
|
||||||
if Response <> '<SetFans' then
|
|
||||||
ShowMessage('Invalid response: ' + Response);
|
|
||||||
end);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.FormCreate(Sender: TObject);
|
|
||||||
begin
|
|
||||||
RefreshPorts;
|
|
||||||
PortComboBox.ItemIndex := Pred(PortComboBox.Items.Count);
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
procedure TMainForm.RefreshPortsButtonClick(Sender: TObject);
|
|
||||||
begin
|
|
||||||
RefreshPorts;
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
procedure TMainForm.RefreshPorts;
|
|
||||||
var
|
|
||||||
currentPort: string;
|
|
||||||
|
|
||||||
begin
|
|
||||||
currentPort := '';
|
|
||||||
if PortComboBox.ItemIndex > -1 then
|
|
||||||
currentPort := PortComboBox.Items[PortComboBox.ItemIndex];
|
|
||||||
|
|
||||||
EnumComPorts(PortComboBox.Items);
|
|
||||||
|
|
||||||
if Length(currentPort) > 0 then
|
|
||||||
PortComboBox.ItemIndex := PortComboBox.Items.IndexOf(currentPort);
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
procedure TMainForm.OnReceiveChar(Sender: TObject; Count: Integer);
|
|
||||||
var
|
|
||||||
data: string;
|
|
||||||
terminatorPos: Integer;
|
|
||||||
|
|
||||||
begin
|
|
||||||
(Sender as TComPort).ReadStr(data, Count);
|
|
||||||
FReceived := FReceived + data;
|
|
||||||
|
|
||||||
terminatorPos := AnsiPos(#10, FReceived);
|
|
||||||
if terminatorPos > 0 then
|
|
||||||
begin
|
|
||||||
// Since the protocol is quite synchronous, this is good enough for now
|
|
||||||
SetLength(FReceived, terminatorPos - 1);
|
|
||||||
if Assigned(FOnResponse) then
|
|
||||||
FOnResponse(FReceived);
|
|
||||||
|
|
||||||
FReceived := '';
|
|
||||||
FOnResponse := nil;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
procedure TMainForm.SendCommand(const ACommand: string; AOnResponse: TProc<string>);
|
|
||||||
begin
|
|
||||||
if not Assigned(FComPort) then
|
|
||||||
begin
|
|
||||||
FComPort := TComPort.Create(Self);
|
|
||||||
FComPort.Port := PortComboBox.Items[PortComboBox.ItemIndex];
|
|
||||||
FComPort.BaudRate := br19200;
|
|
||||||
FComPort.OnRxChar := OnReceiveChar;
|
|
||||||
FComPort.Open;
|
|
||||||
end;
|
|
||||||
|
|
||||||
FOnResponse := AOnResponse;
|
|
||||||
ComPort.WriteStr(ACommand + #10);
|
|
||||||
end;
|
|
||||||
|
|
||||||
end.
|
|
24
Client/SimulatorFans.dpr
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
program SimulatorFans;
|
||||||
|
|
||||||
|
uses
|
||||||
|
Vcl.Forms,
|
||||||
|
MainFrm in 'source\MainFrm.pas' {MainForm},
|
||||||
|
AssettoCorsa.SharedMemory in 'source\AssettoCorsa.SharedMemory.pas',
|
||||||
|
CPort in 'source\CPort.pas',
|
||||||
|
Simulator.Registry in 'source\Simulator.Registry.pas',
|
||||||
|
Simulator.Base in 'source\Simulator.Base.pas' {BaseSimulatorFrame: TFrame},
|
||||||
|
Simulator.AssettoCorsa in 'source\Simulator.AssettoCorsa.pas' {AssettoCorsaFrame: TFrame},
|
||||||
|
ESCCalibrationFrm in 'source\ESCCalibrationFrm.pas' {ESCCalibrationForm},
|
||||||
|
Simulator.Manual in 'source\Simulator.Manual.pas' {ManualFrame: TFrame};
|
||||||
|
|
||||||
|
{$R *.res}
|
||||||
|
|
||||||
|
var
|
||||||
|
MainForm: TMainForm;
|
||||||
|
|
||||||
|
begin
|
||||||
|
Application.Initialize;
|
||||||
|
Application.MainFormOnTaskbar := True;
|
||||||
|
Application.CreateForm(TMainForm, MainForm);
|
||||||
|
Application.Run;
|
||||||
|
end.
|
@ -3,7 +3,7 @@
|
|||||||
<ProjectGuid>{3CBA51E1-33C8-4599-8659-3A1F26AA5E70}</ProjectGuid>
|
<ProjectGuid>{3CBA51E1-33C8-4599-8659-3A1F26AA5E70}</ProjectGuid>
|
||||||
<ProjectVersion>13.4</ProjectVersion>
|
<ProjectVersion>13.4</ProjectVersion>
|
||||||
<FrameworkType>VCL</FrameworkType>
|
<FrameworkType>VCL</FrameworkType>
|
||||||
<MainSource>AssettoCorsaSF.dpr</MainSource>
|
<MainSource>SimulatorFans.dpr</MainSource>
|
||||||
<Base>True</Base>
|
<Base>True</Base>
|
||||||
<Config Condition="'$(Config)'==''">Debug</Config>
|
<Config Condition="'$(Config)'==''">Debug</Config>
|
||||||
<Platform Condition="'$(Platform)'==''">Win32</Platform>
|
<Platform Condition="'$(Platform)'==''">Win32</Platform>
|
||||||
@ -41,10 +41,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Base)'!=''">
|
<PropertyGroup Condition="'$(Base)'!=''">
|
||||||
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
|
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
|
||||||
<Manifest_File>None</Manifest_File>
|
<Manifest_File>$(BDS)\bin\default_app.manifest</Manifest_File>
|
||||||
<VerInfo_Locale>1043</VerInfo_Locale>
|
<VerInfo_Locale>1043</VerInfo_Locale>
|
||||||
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace)</DCC_Namespace>
|
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace)</DCC_Namespace>
|
||||||
<Icon_MainIcon>$(BDS)\bin\delphi_PROJECTICON.ico</Icon_MainIcon>
|
<Icon_MainIcon>..\Logo\SimulatorFans.ico</Icon_MainIcon>
|
||||||
<DCC_UsePackage>bindcompfmx;fmx;rtl;dbrtl;IndySystem;DbxClientDriver;bindcomp;inetdb;DBXInterBaseDriver;DataSnapCommon;DataSnapClient;DataSnapServer;DataSnapProviderClient;xmlrtl;DbxCommonDriver;IndyProtocols;DBXMySQLDriver;dbxcds;soaprtl;FMXTee;bindengine;DBXOracleDriver;CustomIPTransport;dsnap;DBXInformixDriver;IndyCore;FmxTeeUI;DBXFirebirdDriver;inet;inetdbxpress;DBXSybaseASADriver;IPIndyImpl;dbexpress;DataSnapIndy10ServerTransport;$(DCC_UsePackage)</DCC_UsePackage>
|
<DCC_UsePackage>bindcompfmx;fmx;rtl;dbrtl;IndySystem;DbxClientDriver;bindcomp;inetdb;DBXInterBaseDriver;DataSnapCommon;DataSnapClient;DataSnapServer;DataSnapProviderClient;xmlrtl;DbxCommonDriver;IndyProtocols;DBXMySQLDriver;dbxcds;soaprtl;FMXTee;bindengine;DBXOracleDriver;CustomIPTransport;dsnap;DBXInformixDriver;IndyCore;FmxTeeUI;DBXFirebirdDriver;inet;inetdbxpress;DBXSybaseASADriver;IPIndyImpl;dbexpress;DataSnapIndy10ServerTransport;$(DCC_UsePackage)</DCC_UsePackage>
|
||||||
<DCC_DcuOutput>.\$(Platform)\$(Config)</DCC_DcuOutput>
|
<DCC_DcuOutput>.\$(Platform)\$(Config)</DCC_DcuOutput>
|
||||||
<DCC_ExeOutput>.\$(Platform)\$(Config)</DCC_ExeOutput>
|
<DCC_ExeOutput>.\$(Platform)\$(Config)</DCC_ExeOutput>
|
||||||
@ -74,6 +74,7 @@
|
|||||||
<DCC_RemoteDebug>true</DCC_RemoteDebug>
|
<DCC_RemoteDebug>true</DCC_RemoteDebug>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
|
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
|
||||||
|
<Manifest_File>$(BDS)\bin\default_app.manifest</Manifest_File>
|
||||||
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
|
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
|
||||||
<VerInfo_Locale>1033</VerInfo_Locale>
|
<VerInfo_Locale>1033</VerInfo_Locale>
|
||||||
<DCC_RemoteDebug>false</DCC_RemoteDebug>
|
<DCC_RemoteDebug>false</DCC_RemoteDebug>
|
||||||
@ -94,6 +95,23 @@
|
|||||||
</DCCReference>
|
</DCCReference>
|
||||||
<DCCReference Include="source\AssettoCorsa.SharedMemory.pas"/>
|
<DCCReference Include="source\AssettoCorsa.SharedMemory.pas"/>
|
||||||
<DCCReference Include="source\CPort.pas"/>
|
<DCCReference Include="source\CPort.pas"/>
|
||||||
|
<DCCReference Include="source\Simulator.Registry.pas"/>
|
||||||
|
<DCCReference Include="source\Simulator.Base.pas">
|
||||||
|
<Form>BaseSimulatorFrame</Form>
|
||||||
|
<DesignClass>TFrame</DesignClass>
|
||||||
|
</DCCReference>
|
||||||
|
<DCCReference Include="source\Simulator.AssettoCorsa.pas">
|
||||||
|
<Form>AssettoCorsaFrame</Form>
|
||||||
|
<DesignClass>TFrame</DesignClass>
|
||||||
|
</DCCReference>
|
||||||
|
<DCCReference Include="source\ESCCalibrationFrm.pas">
|
||||||
|
<Form>ESCCalibrationForm</Form>
|
||||||
|
<FormType>dfm</FormType>
|
||||||
|
</DCCReference>
|
||||||
|
<DCCReference Include="source\Simulator.Manual.pas">
|
||||||
|
<Form>ManualFrame</Form>
|
||||||
|
<DesignClass>TFrame</DesignClass>
|
||||||
|
</DCCReference>
|
||||||
<BuildConfiguration Include="Release">
|
<BuildConfiguration Include="Release">
|
||||||
<Key>Cfg_2</Key>
|
<Key>Cfg_2</Key>
|
||||||
<CfgParent>Base</CfgParent>
|
<CfgParent>Base</CfgParent>
|
||||||
@ -112,7 +130,7 @@
|
|||||||
<BorlandProject>
|
<BorlandProject>
|
||||||
<Delphi.Personality>
|
<Delphi.Personality>
|
||||||
<Source>
|
<Source>
|
||||||
<Source Name="MainSource">AssettoCorsaSF.dpr</Source>
|
<Source Name="MainSource">SimulatorFans.dpr</Source>
|
||||||
</Source>
|
</Source>
|
||||||
<VersionInfo>
|
<VersionInfo>
|
||||||
<VersionInfo Name="IncludeVerInfo">False</VersionInfo>
|
<VersionInfo Name="IncludeVerInfo">False</VersionInfo>
|
BIN
Client/SimulatorFans.res
Normal file
BIN
Client/resources/lock.bmp
Normal file
After Width: | Height: | Size: 488 B |
92
Client/source/ESCCalibrationFrm.dfm
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
object ESCCalibrationForm: TESCCalibrationForm
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
ActiveControl = CloseButton
|
||||||
|
BorderStyle = bsDialog
|
||||||
|
Caption = 'ESC calibration'
|
||||||
|
ClientHeight = 213
|
||||||
|
ClientWidth = 565
|
||||||
|
Color = clBtnFace
|
||||||
|
Font.Charset = DEFAULT_CHARSET
|
||||||
|
Font.Color = clWindowText
|
||||||
|
Font.Height = -11
|
||||||
|
Font.Name = 'Tahoma'
|
||||||
|
Font.Style = []
|
||||||
|
OldCreateOrder = False
|
||||||
|
Position = poMainFormCenter
|
||||||
|
OnClose = FormClose
|
||||||
|
DesignSize = (
|
||||||
|
565
|
||||||
|
213)
|
||||||
|
PixelsPerInch = 96
|
||||||
|
TextHeight = 13
|
||||||
|
object CloseButton: TButton
|
||||||
|
Left = 245
|
||||||
|
Top = 179
|
||||||
|
Width = 75
|
||||||
|
Height = 25
|
||||||
|
Anchors = [akLeft, akBottom]
|
||||||
|
Cancel = True
|
||||||
|
Caption = 'Close'
|
||||||
|
ModalResult = 8
|
||||||
|
TabOrder = 0
|
||||||
|
end
|
||||||
|
object CalibrationGroupBox: TGroupBox
|
||||||
|
Left = 8
|
||||||
|
Top = 8
|
||||||
|
Width = 549
|
||||||
|
Height = 161
|
||||||
|
Anchors = [akLeft, akTop, akRight, akBottom]
|
||||||
|
TabOrder = 1
|
||||||
|
ExplicitWidth = 525
|
||||||
|
DesignSize = (
|
||||||
|
549
|
||||||
|
161)
|
||||||
|
object HelpLabel1: TLabel
|
||||||
|
Left = 16
|
||||||
|
Top = 16
|
||||||
|
Width = 521
|
||||||
|
Height = 33
|
||||||
|
Anchors = [akLeft, akTop, akRight]
|
||||||
|
AutoSize = False
|
||||||
|
Caption =
|
||||||
|
'You can use the Full button below to set the PWM output to it'#39's ' +
|
||||||
|
'maximum value, disregarding the regular maximum value as program' +
|
||||||
|
'med into the SimulatorFans device.'
|
||||||
|
WordWrap = True
|
||||||
|
ExplicitWidth = 497
|
||||||
|
end
|
||||||
|
object HelpLabel2: TLabel
|
||||||
|
Left = 16
|
||||||
|
Top = 55
|
||||||
|
Width = 521
|
||||||
|
Height = 34
|
||||||
|
Anchors = [akLeft, akTop, akRight]
|
||||||
|
AutoSize = False
|
||||||
|
Caption =
|
||||||
|
'Disconnect your Electronic Speed Controller before continuing, o' +
|
||||||
|
'r it can spin up the motor to it'#39's full power. USE WITH CARE!'
|
||||||
|
WordWrap = True
|
||||||
|
ExplicitWidth = 497
|
||||||
|
end
|
||||||
|
object FullButton: TButton
|
||||||
|
Left = 200
|
||||||
|
Top = 104
|
||||||
|
Width = 75
|
||||||
|
Height = 25
|
||||||
|
Caption = 'Full'
|
||||||
|
TabOrder = 0
|
||||||
|
OnClick = FullButtonClick
|
||||||
|
end
|
||||||
|
object OffButton: TButton
|
||||||
|
Left = 281
|
||||||
|
Top = 104
|
||||||
|
Width = 75
|
||||||
|
Height = 25
|
||||||
|
Caption = 'Off'
|
||||||
|
Enabled = False
|
||||||
|
TabOrder = 1
|
||||||
|
OnClick = OffButtonClick
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
78
Client/source/ESCCalibrationFrm.pas
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
unit ESCCalibrationFrm;
|
||||||
|
|
||||||
|
interface
|
||||||
|
uses
|
||||||
|
System.Classes,
|
||||||
|
Vcl.Controls,
|
||||||
|
Vcl.Forms,
|
||||||
|
Vcl.StdCtrls,
|
||||||
|
|
||||||
|
Simulator.Registry;
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
TESCCalibrationForm = class(TForm)
|
||||||
|
CloseButton: TButton;
|
||||||
|
CalibrationGroupBox: TGroupBox;
|
||||||
|
HelpLabel1: TLabel;
|
||||||
|
HelpLabel2: TLabel;
|
||||||
|
FullButton: TButton;
|
||||||
|
OffButton: TButton;
|
||||||
|
procedure FormClose(Sender: TObject; var Action: TCloseAction);
|
||||||
|
procedure FullButtonClick(Sender: TObject);
|
||||||
|
procedure OffButtonClick(Sender: TObject);
|
||||||
|
private
|
||||||
|
FFans: ISimulatorFans;
|
||||||
|
FFull: Boolean;
|
||||||
|
protected
|
||||||
|
property Fans: ISimulatorFans read FFans;
|
||||||
|
public
|
||||||
|
class procedure Execute(AFans: ISimulatorFans);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
|
||||||
|
{$R *.dfm}
|
||||||
|
|
||||||
|
{ TESCCalibrationForm }
|
||||||
|
class procedure TESCCalibrationForm.Execute(AFans: ISimulatorFans);
|
||||||
|
begin
|
||||||
|
with Self.Create(nil) do
|
||||||
|
try
|
||||||
|
FFans := AFans;
|
||||||
|
ShowModal;
|
||||||
|
finally
|
||||||
|
Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TESCCalibrationForm.FormClose(Sender: TObject; var Action: TCloseAction);
|
||||||
|
begin
|
||||||
|
if FFull then
|
||||||
|
Fans.SetAll(0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TESCCalibrationForm.FullButtonClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FullButton.Enabled := False;
|
||||||
|
OffButton.Enabled := True;
|
||||||
|
|
||||||
|
Fans.SetFull;
|
||||||
|
FFull := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TESCCalibrationForm.OffButtonClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FullButton.Enabled := True;
|
||||||
|
OffButton.Enabled := False;
|
||||||
|
|
||||||
|
Fans.SetAll(0);
|
||||||
|
FFull := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
184
Client/source/MainFrm.dfm
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
object MainForm: TMainForm
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
BorderIcons = [biSystemMenu, biMinimize]
|
||||||
|
BorderStyle = bsSingle
|
||||||
|
Caption = 'SimulatorFans'
|
||||||
|
ClientHeight = 352
|
||||||
|
ClientWidth = 580
|
||||||
|
Color = clBtnFace
|
||||||
|
Font.Charset = DEFAULT_CHARSET
|
||||||
|
Font.Color = clWindowText
|
||||||
|
Font.Height = -11
|
||||||
|
Font.Name = 'Tahoma'
|
||||||
|
Font.Style = []
|
||||||
|
OldCreateOrder = False
|
||||||
|
Position = poScreenCenter
|
||||||
|
ShowHint = True
|
||||||
|
OnClose = FormClose
|
||||||
|
OnCreate = FormCreate
|
||||||
|
PixelsPerInch = 96
|
||||||
|
TextHeight = 13
|
||||||
|
object HardwareGroupBox: TGroupBox
|
||||||
|
AlignWithMargins = True
|
||||||
|
Left = 8
|
||||||
|
Top = 8
|
||||||
|
Width = 564
|
||||||
|
Height = 137
|
||||||
|
Margins.Left = 8
|
||||||
|
Margins.Top = 8
|
||||||
|
Margins.Right = 8
|
||||||
|
Margins.Bottom = 8
|
||||||
|
Align = alTop
|
||||||
|
Caption = ' Hardware configuration '
|
||||||
|
TabOrder = 0
|
||||||
|
object PortLabel: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 27
|
||||||
|
Width = 50
|
||||||
|
Height = 13
|
||||||
|
Caption = 'COM port:'
|
||||||
|
end
|
||||||
|
object PortStatusLabel: TLabel
|
||||||
|
Left = 288
|
||||||
|
Top = 27
|
||||||
|
Width = 121
|
||||||
|
Height = 13
|
||||||
|
AutoSize = False
|
||||||
|
Caption = 'Not connected'
|
||||||
|
end
|
||||||
|
object FansLabel: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 54
|
||||||
|
Width = 27
|
||||||
|
Height = 13
|
||||||
|
Caption = 'Fans:'
|
||||||
|
end
|
||||||
|
object FanCountLabel: TLabel
|
||||||
|
Left = 88
|
||||||
|
Top = 54
|
||||||
|
Width = 185
|
||||||
|
Height = 13
|
||||||
|
AutoSize = False
|
||||||
|
Caption = '?'
|
||||||
|
end
|
||||||
|
object PortComboBox: TComboBox
|
||||||
|
Left = 88
|
||||||
|
Top = 24
|
||||||
|
Width = 185
|
||||||
|
Height = 21
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 0
|
||||||
|
OnClick = PortComboBoxClick
|
||||||
|
end
|
||||||
|
object CalibrationButton: TButton
|
||||||
|
Left = 88
|
||||||
|
Top = 96
|
||||||
|
Width = 113
|
||||||
|
Height = 25
|
||||||
|
Caption = 'ESC calibration...'
|
||||||
|
Enabled = False
|
||||||
|
TabOrder = 1
|
||||||
|
OnClick = CalibrationButtonClick
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object SimulatorGroupBox: TGroupBox
|
||||||
|
AlignWithMargins = True
|
||||||
|
Left = 8
|
||||||
|
Top = 153
|
||||||
|
Width = 564
|
||||||
|
Height = 191
|
||||||
|
Margins.Left = 8
|
||||||
|
Margins.Top = 0
|
||||||
|
Margins.Right = 8
|
||||||
|
Margins.Bottom = 8
|
||||||
|
Align = alClient
|
||||||
|
Caption = ' Simulator '
|
||||||
|
TabOrder = 1
|
||||||
|
object SimulatorPanel: TPanel
|
||||||
|
AlignWithMargins = True
|
||||||
|
Left = 14
|
||||||
|
Top = 68
|
||||||
|
Width = 536
|
||||||
|
Height = 109
|
||||||
|
Margins.Left = 12
|
||||||
|
Margins.Top = 12
|
||||||
|
Margins.Right = 12
|
||||||
|
Margins.Bottom = 12
|
||||||
|
Align = alClient
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 0
|
||||||
|
ExplicitTop = 56
|
||||||
|
ExplicitHeight = 121
|
||||||
|
end
|
||||||
|
object SimulatorSelectionPanel: TPanel
|
||||||
|
Left = 2
|
||||||
|
Top = 15
|
||||||
|
Width = 560
|
||||||
|
Height = 41
|
||||||
|
Align = alTop
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 1
|
||||||
|
ExplicitLeft = 192
|
||||||
|
ExplicitTop = 72
|
||||||
|
ExplicitWidth = 185
|
||||||
|
DesignSize = (
|
||||||
|
560
|
||||||
|
41)
|
||||||
|
object SimulatorLockButton: TSpeedButton
|
||||||
|
Left = 527
|
||||||
|
Top = 7
|
||||||
|
Width = 23
|
||||||
|
Height = 23
|
||||||
|
Hint =
|
||||||
|
'Lock the simulator selection dropdown to prevent accidental chan' +
|
||||||
|
'ges'
|
||||||
|
AllowAllUp = True
|
||||||
|
GroupIndex = 1
|
||||||
|
Glyph.Data = {
|
||||||
|
E6010000424DE60100000000000036000000280000000C0000000C0000000100
|
||||||
|
180000000000B0010000120B0000120B00000000000000000000FF00FF000000
|
||||||
|
000000000000000000000000000000000000000000000000000000FF00FFFF00
|
||||||
|
FF000000000000000000000000000000000000000000000000000000000000FF
|
||||||
|
00FFFF00FF000000000000000000000000000000000000000000000000000000
|
||||||
|
000000FF00FFFF00FF0000000000000000000000000000000000000000000000
|
||||||
|
00000000000000FF00FFFF00FF00000000000000000000000000000000000000
|
||||||
|
0000000000000000000000FF00FFFF00FF000000000000000000000000000000
|
||||||
|
000000000000000000000000000000FF00FFFF00FF0000000000000000000000
|
||||||
|
00000000000000000000000000000000000000FF00FFFF00FFFF00FF00000000
|
||||||
|
0000FF00FFFF00FFFF00FFFF00FF000000000000FF00FFFF00FFFF00FFFF00FF
|
||||||
|
000000000000FF00FFFF00FFFF00FFFF00FF000000000000FF00FFFF00FFFF00
|
||||||
|
FFFF00FF000000000000FF00FFFF00FFFF00FFFF00FF000000000000FF00FFFF
|
||||||
|
00FFFF00FFFF00FFFF00FF000000000000000000000000000000000000FF00FF
|
||||||
|
FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF000000000000000000000000FF00
|
||||||
|
FFFF00FFFF00FFFF00FF}
|
||||||
|
OnClick = SimulatorLockButtonClick
|
||||||
|
end
|
||||||
|
object SimulatorComboBox: TComboBox
|
||||||
|
Left = 12
|
||||||
|
Top = 8
|
||||||
|
Width = 509
|
||||||
|
Height = 21
|
||||||
|
Style = csDropDownList
|
||||||
|
Anchors = [akLeft, akTop, akRight]
|
||||||
|
Sorted = True
|
||||||
|
TabOrder = 0
|
||||||
|
OnClick = SimulatorComboBoxClick
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object ConnectTimer: TTimer
|
||||||
|
Enabled = False
|
||||||
|
Interval = 5000
|
||||||
|
OnTimer = ConnectTimerTimer
|
||||||
|
Left = 424
|
||||||
|
Top = 24
|
||||||
|
end
|
||||||
|
object ResponseTimer: TTimer
|
||||||
|
Enabled = False
|
||||||
|
Interval = 2000
|
||||||
|
OnTimer = ResponseTimerTimer
|
||||||
|
Left = 424
|
||||||
|
Top = 80
|
||||||
|
end
|
||||||
|
end
|
466
Client/source/MainFrm.pas
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
unit MainFrm;
|
||||||
|
|
||||||
|
// #ToDo1 -oMvR: 2-9-2017: device change notification -> refresh COM port list
|
||||||
|
// #ToDo1 -oMvR: 2-9-2017: visualize fan power
|
||||||
|
// #ToDo1 -oMvR: 10-9-2017: save settings
|
||||||
|
|
||||||
|
interface
|
||||||
|
uses
|
||||||
|
System.Classes,
|
||||||
|
System.SysUtils,
|
||||||
|
Vcl.Controls,
|
||||||
|
Vcl.ExtCtrls,
|
||||||
|
Vcl.Forms,
|
||||||
|
Vcl.StdCtrls,
|
||||||
|
|
||||||
|
CPort,
|
||||||
|
|
||||||
|
Simulator.Registry, Vcl.Buttons;
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
TMainForm = class(TForm, ISimulatorFans)
|
||||||
|
PortComboBox: TComboBox;
|
||||||
|
HardwareGroupBox: TGroupBox;
|
||||||
|
PortLabel: TLabel;
|
||||||
|
PortStatusLabel: TLabel;
|
||||||
|
ConnectTimer: TTimer;
|
||||||
|
FansLabel: TLabel;
|
||||||
|
SimulatorGroupBox: TGroupBox;
|
||||||
|
ResponseTimer: TTimer;
|
||||||
|
FanCountLabel: TLabel;
|
||||||
|
CalibrationButton: TButton;
|
||||||
|
SimulatorComboBox: TComboBox;
|
||||||
|
SimulatorPanel: TPanel;
|
||||||
|
SimulatorSelectionPanel: TPanel;
|
||||||
|
SimulatorLockButton: TSpeedButton;
|
||||||
|
|
||||||
|
procedure FormCreate(Sender: TObject);
|
||||||
|
procedure PortComboBoxClick(Sender: TObject);
|
||||||
|
procedure ConnectTimerTimer(Sender: TObject);
|
||||||
|
procedure ResponseTimerTimer(Sender: TObject);
|
||||||
|
procedure FormClose(Sender: TObject; var Action: TCloseAction);
|
||||||
|
procedure CalibrationButtonClick(Sender: TObject);
|
||||||
|
procedure SimulatorComboBoxClick(Sender: TObject);
|
||||||
|
procedure SimulatorLockButtonClick(Sender: TObject);
|
||||||
|
private
|
||||||
|
FComPort: TComPort;
|
||||||
|
FReceived: string;
|
||||||
|
FOnResponse: TProc<string>;
|
||||||
|
FOnTimeout: TProc;
|
||||||
|
FReady: Boolean;
|
||||||
|
FFanValues: array of Byte;
|
||||||
|
FSimulator: ISimulator;
|
||||||
|
|
||||||
|
procedure LoadSimulators;
|
||||||
|
|
||||||
|
procedure RefreshPorts;
|
||||||
|
procedure InitDevice(const AInfo: string);
|
||||||
|
|
||||||
|
procedure TryConnect;
|
||||||
|
|
||||||
|
procedure SendCommand(const ACommand: string; AOnResponse: TProc<string>; AOnTimeout: TProc = nil);
|
||||||
|
procedure StartCommand;
|
||||||
|
procedure EndCommand;
|
||||||
|
procedure OnReceiveChar(Sender: TObject; Count: Integer);
|
||||||
|
|
||||||
|
function ValidFanIndex(AFan: Byte): Boolean; inline;
|
||||||
|
|
||||||
|
procedure SelectSimulator;
|
||||||
|
|
||||||
|
property ComPort: TComPort read FComPort;
|
||||||
|
property Ready: Boolean read FReady;
|
||||||
|
property Simulator: ISimulator read FSimulator;
|
||||||
|
public
|
||||||
|
{ ISimulatorFans }
|
||||||
|
function GetFanCount: Integer;
|
||||||
|
function GetReady: Boolean;
|
||||||
|
function GetIsRunning: Boolean;
|
||||||
|
|
||||||
|
function GetValue(const AFan: Byte): Byte;
|
||||||
|
function GetMaxValue: Byte;
|
||||||
|
|
||||||
|
procedure SetValue(const AFan, AValue: Byte);
|
||||||
|
procedure SetAll(const AValue: Byte);
|
||||||
|
|
||||||
|
procedure SetFull;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
implementation
|
||||||
|
uses
|
||||||
|
System.Math,
|
||||||
|
System.StrUtils,
|
||||||
|
Vcl.Dialogs,
|
||||||
|
|
||||||
|
ESCCalibrationFrm;
|
||||||
|
|
||||||
|
|
||||||
|
{$R *.dfm}
|
||||||
|
|
||||||
|
|
||||||
|
{ TMainForm }
|
||||||
|
procedure TMainForm.FormCreate(Sender: TObject);
|
||||||
|
begin
|
||||||
|
LoadSimulators;
|
||||||
|
RefreshPorts;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
|
||||||
|
begin
|
||||||
|
if Ready then
|
||||||
|
begin
|
||||||
|
SendCommand('>SetFans:0',
|
||||||
|
procedure(Response: string)
|
||||||
|
begin
|
||||||
|
FReady := False;
|
||||||
|
Close;
|
||||||
|
end);
|
||||||
|
Action := caNone;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.PortComboBoxClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
TryConnect;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.SimulatorComboBoxClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
SelectSimulator;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.SimulatorLockButtonClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
SimulatorComboBox.Enabled := not SimulatorLockButton.Down;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.CalibrationButtonClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if Assigned(Simulator) then
|
||||||
|
Simulator.Stop;
|
||||||
|
|
||||||
|
TESCCalibrationForm.Execute(Self);
|
||||||
|
|
||||||
|
if Assigned(Simulator) then
|
||||||
|
Simulator.Start;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.LoadSimulators;
|
||||||
|
var
|
||||||
|
simulator: TRegisteredSimulator;
|
||||||
|
|
||||||
|
begin
|
||||||
|
SimulatorComboBox.Items.BeginUpdate;
|
||||||
|
try
|
||||||
|
SimulatorComboBox.Items.Clear;
|
||||||
|
|
||||||
|
for simulator in GetRegisteredSimulators do
|
||||||
|
SimulatorComboBox.Items.AddObject(simulator.Name, simulator);
|
||||||
|
|
||||||
|
SimulatorComboBox.ItemIndex := 0;
|
||||||
|
finally
|
||||||
|
SimulatorComboBox.Items.EndUpdate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
SelectSimulator;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.RefreshPorts;
|
||||||
|
var
|
||||||
|
currentPort: string;
|
||||||
|
|
||||||
|
begin
|
||||||
|
currentPort := '';
|
||||||
|
if PortComboBox.ItemIndex > -1 then
|
||||||
|
currentPort := PortComboBox.Items[PortComboBox.ItemIndex];
|
||||||
|
|
||||||
|
EnumComPorts(PortComboBox.Items);
|
||||||
|
|
||||||
|
if Length(currentPort) > 0 then
|
||||||
|
PortComboBox.ItemIndex := PortComboBox.Items.IndexOf(currentPort);
|
||||||
|
|
||||||
|
TryConnect;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.InitDevice(const AInfo: string);
|
||||||
|
var
|
||||||
|
info: TStringList;
|
||||||
|
value: string;
|
||||||
|
fanCount: Integer;
|
||||||
|
|
||||||
|
begin
|
||||||
|
info := TStringList.Create;
|
||||||
|
try
|
||||||
|
for value in SplitString(AInfo, ',') do
|
||||||
|
info.Add(value);
|
||||||
|
|
||||||
|
if TryStrToInt(info.Values['Fans'], fanCount) then
|
||||||
|
begin
|
||||||
|
SetLength(FFanValues, fanCount);
|
||||||
|
FanCountLabel.Caption := IntToStr(fanCount);
|
||||||
|
end else
|
||||||
|
SetLength(FFanValues, 0);
|
||||||
|
|
||||||
|
CalibrationButton.Enabled := info.Values['Mode'] = 'Servo';
|
||||||
|
finally
|
||||||
|
FreeAndNil(info)
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.SendCommand(const ACommand: string; AOnResponse: TProc<string>; AOnTimeout: TProc);
|
||||||
|
begin
|
||||||
|
if (not Assigned(ComPort)) or (not ComPort.Connected) then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
StartCommand;
|
||||||
|
FOnResponse := AOnResponse;
|
||||||
|
FOnTimeout := AOnTimeout;
|
||||||
|
ComPort.WriteStr(ACommand + #10);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
function TMainForm.GetFanCount: Integer;
|
||||||
|
begin
|
||||||
|
Result := Length(FFanValues);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
function TMainForm.GetReady: Boolean;
|
||||||
|
begin
|
||||||
|
Result := FReady;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
function TMainForm.GetIsRunning: Boolean;
|
||||||
|
begin
|
||||||
|
Result := Ready and (GetMaxValue > 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
function TMainForm.GetValue(const AFan: Byte): Byte;
|
||||||
|
begin
|
||||||
|
if ValidFanIndex(AFan) then
|
||||||
|
Result := FFanValues[AFan]
|
||||||
|
else
|
||||||
|
Result := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
function TMainForm.GetMaxValue: Byte;
|
||||||
|
var
|
||||||
|
value: Byte;
|
||||||
|
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
|
||||||
|
for value in FFanValues do
|
||||||
|
if value > Result then
|
||||||
|
Result := value;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.SetValue(const AFan, AValue: Byte);
|
||||||
|
var
|
||||||
|
values: TStringBuilder;
|
||||||
|
value: Byte;
|
||||||
|
|
||||||
|
begin
|
||||||
|
if ValidFanIndex(AFan) and (FFanValues[AFan] <> AValue) then
|
||||||
|
begin
|
||||||
|
FFanValues[AFan] := AValue;
|
||||||
|
|
||||||
|
values := TStringBuilder.Create;
|
||||||
|
try
|
||||||
|
for value in FFanValues do
|
||||||
|
begin
|
||||||
|
if values.Length > 0 then
|
||||||
|
values.Append(',');
|
||||||
|
|
||||||
|
values.Append(value);
|
||||||
|
end;
|
||||||
|
|
||||||
|
SendCommand('>SetFans:' + values.ToString, nil);
|
||||||
|
finally
|
||||||
|
FreeAndNil(values);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.SetAll(const AValue: Byte);
|
||||||
|
var
|
||||||
|
fanIndex: Integer;
|
||||||
|
|
||||||
|
begin
|
||||||
|
for fanIndex := 0 to High(FFanValues) do
|
||||||
|
FFanValues[fanIndex] := AValue;
|
||||||
|
|
||||||
|
SendCommand('>SetFans:A,' + IntToStr(AValue), nil);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.SetFull;
|
||||||
|
var
|
||||||
|
fanIndex: Integer;
|
||||||
|
|
||||||
|
begin
|
||||||
|
for fanIndex := 0 to High(FFanValues) do
|
||||||
|
FFanValues[fanIndex] := 255;
|
||||||
|
|
||||||
|
SendCommand('>SetFans:M', nil);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.TryConnect;
|
||||||
|
var
|
||||||
|
newPort: string;
|
||||||
|
|
||||||
|
begin
|
||||||
|
if PortComboBox.ItemIndex = -1 then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
ConnectTimer.Enabled := False;
|
||||||
|
FReady := False;
|
||||||
|
CalibrationButton.Enabled := False;
|
||||||
|
|
||||||
|
PortStatusLabel.Caption := 'Attempting to connect';
|
||||||
|
PortStatusLabel.Update;
|
||||||
|
|
||||||
|
if not Assigned(FComPort) then
|
||||||
|
begin
|
||||||
|
FComPort := TComPort.Create(Self);
|
||||||
|
FComPort.BaudRate := br19200;
|
||||||
|
FComPort.OnRxChar := OnReceiveChar;
|
||||||
|
end;
|
||||||
|
|
||||||
|
newPort := PortComboBox.Items[PortComboBox.ItemIndex];
|
||||||
|
if ComPort.Connected and (newPort <> ComPort.Port) then
|
||||||
|
ComPort.Close;
|
||||||
|
|
||||||
|
ComPort.Port := newPort;
|
||||||
|
try
|
||||||
|
ComPort.Open;
|
||||||
|
|
||||||
|
SendCommand('>Info',
|
||||||
|
procedure(Response: string)
|
||||||
|
begin
|
||||||
|
if AnsiStartsText('<Info:', Response) then
|
||||||
|
begin
|
||||||
|
PortStatusLabel.Caption := 'Connected';
|
||||||
|
InitDevice(Copy(Response, 7, MaxInt));
|
||||||
|
FReady := True;
|
||||||
|
end else
|
||||||
|
begin
|
||||||
|
PortStatusLabel.Caption := 'Invalid response';
|
||||||
|
ConnectTimer.Enabled := True;
|
||||||
|
end;
|
||||||
|
end,
|
||||||
|
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
PortStatusLabel.Caption := 'No response';
|
||||||
|
ConnectTimer.Enabled := True;
|
||||||
|
end);
|
||||||
|
except
|
||||||
|
on E:Exception do
|
||||||
|
begin
|
||||||
|
PortStatusLabel.Caption := 'Failed to connect';
|
||||||
|
ConnectTimer.Enabled := True;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.OnReceiveChar(Sender: TObject; Count: Integer);
|
||||||
|
var
|
||||||
|
data: string;
|
||||||
|
terminatorPos: Integer;
|
||||||
|
|
||||||
|
begin
|
||||||
|
(Sender as TComPort).ReadStr(data, Count);
|
||||||
|
FReceived := FReceived + data;
|
||||||
|
|
||||||
|
terminatorPos := AnsiPos(#10, FReceived);
|
||||||
|
if terminatorPos > 0 then
|
||||||
|
begin
|
||||||
|
// Since the protocol is quite synchronous, this is good enough for now
|
||||||
|
SetLength(FReceived, terminatorPos - 1);
|
||||||
|
if Assigned(FOnResponse) then
|
||||||
|
FOnResponse(FReceived);
|
||||||
|
|
||||||
|
EndCommand;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
function TMainForm.ValidFanIndex(AFan: Byte): Boolean;
|
||||||
|
begin
|
||||||
|
Result := AFan < Length(FFanValues);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.SelectSimulator;
|
||||||
|
var
|
||||||
|
registeredSimulator: TRegisteredSimulator;
|
||||||
|
|
||||||
|
begin
|
||||||
|
if Assigned(FSimulator) then
|
||||||
|
begin
|
||||||
|
FSimulator.Stop;
|
||||||
|
FSimulator := nil;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if SimulatorComboBox.ItemIndex = -1 then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
registeredSimulator := (SimulatorComboBox.Items.Objects[SimulatorComboBox.ItemIndex] as TRegisteredSimulator);
|
||||||
|
FSimulator := registeredSimulator.ConstructorFunc(Self);
|
||||||
|
|
||||||
|
Simulator.SetUIParent(SimulatorPanel);
|
||||||
|
Simulator.Start;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.ConnectTimerTimer(Sender: TObject);
|
||||||
|
begin
|
||||||
|
TryConnect;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.ResponseTimerTimer(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if Assigned(FOnTimeout) then
|
||||||
|
FOnTimeout;
|
||||||
|
|
||||||
|
EndCommand;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.StartCommand;
|
||||||
|
begin
|
||||||
|
FReceived := '';
|
||||||
|
ResponseTimer.Enabled := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TMainForm.EndCommand;
|
||||||
|
begin
|
||||||
|
FReceived := '';
|
||||||
|
FOnResponse := nil;
|
||||||
|
FOnTimeout := nil;
|
||||||
|
ResponseTimer.Enabled := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end.
|
72
Client/source/Simulator.AssettoCorsa.dfm
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
inherited AssettoCorsaFrame: TAssettoCorsaFrame
|
||||||
|
Width = 544
|
||||||
|
Height = 78
|
||||||
|
ParentFont = False
|
||||||
|
ExplicitWidth = 544
|
||||||
|
ExplicitHeight = 78
|
||||||
|
DesignSize = (
|
||||||
|
544
|
||||||
|
78)
|
||||||
|
object StatusCaptionLabel: TLabel
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 35
|
||||||
|
Height = 13
|
||||||
|
Caption = 'Status:'
|
||||||
|
end
|
||||||
|
object StatusLabel: TLabel
|
||||||
|
Left = 92
|
||||||
|
Top = 0
|
||||||
|
Width = 452
|
||||||
|
Height = 13
|
||||||
|
Anchors = [akLeft, akTop, akRight]
|
||||||
|
AutoSize = False
|
||||||
|
Caption = 'Checking...'
|
||||||
|
end
|
||||||
|
object CurrentSpeedCaptionLabel: TLabel
|
||||||
|
Left = 0
|
||||||
|
Top = 28
|
||||||
|
Width = 73
|
||||||
|
Height = 13
|
||||||
|
Caption = 'Current speed:'
|
||||||
|
end
|
||||||
|
object FanFullSpeedCaptionLabel: TLabel
|
||||||
|
Left = 0
|
||||||
|
Top = 56
|
||||||
|
Width = 84
|
||||||
|
Height = 13
|
||||||
|
Caption = 'Fan full speed at:'
|
||||||
|
end
|
||||||
|
object FanFullSpeedUnitsLabel: TLabel
|
||||||
|
Left = 163
|
||||||
|
Top = 56
|
||||||
|
Width = 23
|
||||||
|
Height = 13
|
||||||
|
Caption = 'km/h'
|
||||||
|
end
|
||||||
|
object CurrentSpeedLabel: TLabel
|
||||||
|
Left = 92
|
||||||
|
Top = 28
|
||||||
|
Width = 94
|
||||||
|
Height = 13
|
||||||
|
AutoSize = False
|
||||||
|
Caption = '0 km/h'
|
||||||
|
end
|
||||||
|
object FanFullSpeedEdit: TSpinEdit
|
||||||
|
Left = 92
|
||||||
|
Top = 53
|
||||||
|
Width = 65
|
||||||
|
Height = 22
|
||||||
|
MaxValue = 2147483647
|
||||||
|
MinValue = 1
|
||||||
|
TabOrder = 0
|
||||||
|
Value = 300
|
||||||
|
end
|
||||||
|
object SimTimer: TTimer
|
||||||
|
Enabled = False
|
||||||
|
Interval = 250
|
||||||
|
OnTimer = SimTimerTimer
|
||||||
|
Left = 480
|
||||||
|
Top = 24
|
||||||
|
end
|
||||||
|
end
|
114
Client/source/Simulator.AssettoCorsa.pas
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
unit Simulator.AssettoCorsa;
|
||||||
|
|
||||||
|
// #ToDo1 -oMvR: 2-9-2017: configurable speed for full fan power
|
||||||
|
|
||||||
|
interface
|
||||||
|
uses
|
||||||
|
System.Classes,
|
||||||
|
Vcl.Controls,
|
||||||
|
Vcl.ExtCtrls,
|
||||||
|
Vcl.Forms,
|
||||||
|
Vcl.StdCtrls,
|
||||||
|
|
||||||
|
Simulator.Base,
|
||||||
|
Simulator.Registry, Vcl.Samples.Spin;
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
TAssettoCorsaFrame = class(TSimulatorBaseFrame)
|
||||||
|
SimTimer: TTimer;
|
||||||
|
StatusCaptionLabel: TLabel;
|
||||||
|
StatusLabel: TLabel;
|
||||||
|
CurrentSpeedCaptionLabel: TLabel;
|
||||||
|
FanFullSpeedCaptionLabel: TLabel;
|
||||||
|
FanFullSpeedUnitsLabel: TLabel;
|
||||||
|
FanFullSpeedEdit: TSpinEdit;
|
||||||
|
CurrentSpeedLabel: TLabel;
|
||||||
|
|
||||||
|
procedure SimTimerTimer(Sender: TObject);
|
||||||
|
public
|
||||||
|
procedure Initialize; override;
|
||||||
|
|
||||||
|
procedure Start; override;
|
||||||
|
procedure Stop; override;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
implementation
|
||||||
|
uses
|
||||||
|
System.SysUtils,
|
||||||
|
|
||||||
|
AssettoCorsa.SharedMemory;
|
||||||
|
|
||||||
|
|
||||||
|
{$R *.dfm}
|
||||||
|
|
||||||
|
|
||||||
|
{ TAssettoCorsaFrame }
|
||||||
|
procedure TAssettoCorsaFrame.Initialize;
|
||||||
|
begin
|
||||||
|
inherited Initialize;
|
||||||
|
|
||||||
|
CurrentSpeedLabel.Caption := '';
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TAssettoCorsaFrame.SimTimerTimer(Sender: TObject);
|
||||||
|
var
|
||||||
|
gameState: IAssettoCorsaSharedMemory;
|
||||||
|
physics: TACSMPhysics;
|
||||||
|
fanValue: Integer;
|
||||||
|
|
||||||
|
begin
|
||||||
|
gameState := GetAssettoCorsaSharedMemory;
|
||||||
|
if gameState.GetPhysics(physics) then
|
||||||
|
begin
|
||||||
|
StatusLabel.Caption := 'Connected';
|
||||||
|
CurrentSpeedLabel.Caption := Format('%.0f km/h', [physics.speedKmh]);
|
||||||
|
|
||||||
|
if Fans.Ready then
|
||||||
|
begin
|
||||||
|
fanValue := Trunc((physics.speedKmh / FanFullSpeedEdit.Value) * 255);
|
||||||
|
if fanValue > 255 then
|
||||||
|
fanValue := 255;
|
||||||
|
|
||||||
|
Fans.SetAll(fanValue);
|
||||||
|
end;
|
||||||
|
|
||||||
|
SimTimer.Interval := 250;
|
||||||
|
end else
|
||||||
|
begin
|
||||||
|
StatusLabel.Caption := 'Assetto Corsa not running';
|
||||||
|
CurrentSpeedLabel.Caption := '';
|
||||||
|
SimTimer.Interval := 3000;
|
||||||
|
|
||||||
|
if Fans.IsRunning then
|
||||||
|
Fans.SetAll(0);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TAssettoCorsaFrame.Start;
|
||||||
|
begin
|
||||||
|
inherited Start;
|
||||||
|
|
||||||
|
SimTimer.Enabled := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TAssettoCorsaFrame.Stop;
|
||||||
|
begin
|
||||||
|
SimTimer.Enabled := False;
|
||||||
|
|
||||||
|
inherited Stop;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
initialization
|
||||||
|
RegisterSimulator('AssettoCorsa', 'Assetto Corsa',
|
||||||
|
function(AFans: ISimulatorFans): ISimulator
|
||||||
|
begin
|
||||||
|
Result := TSimulatorFrame<TAssettoCorsaFrame>.Create(AFans);
|
||||||
|
end);
|
||||||
|
|
||||||
|
end.
|
7
Client/source/Simulator.Base.dfm
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
object SimulatorBaseFrame: TSimulatorBaseFrame
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 467
|
||||||
|
Height = 165
|
||||||
|
TabOrder = 0
|
||||||
|
end
|
109
Client/source/Simulator.Base.pas
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
unit Simulator.Base;
|
||||||
|
|
||||||
|
interface
|
||||||
|
uses
|
||||||
|
Vcl.Controls,
|
||||||
|
Vcl.Forms,
|
||||||
|
|
||||||
|
Simulator.Registry;
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
TSimulatorBaseFrameClass = class of TSimulatorBaseFrame;
|
||||||
|
|
||||||
|
TSimulatorBaseFrame = class(TFrame)
|
||||||
|
private
|
||||||
|
FFans: ISimulatorFans;
|
||||||
|
public
|
||||||
|
constructor Create(AFans: ISimulatorFans); reintroduce; virtual;
|
||||||
|
|
||||||
|
procedure Initialize; virtual;
|
||||||
|
|
||||||
|
procedure Start; virtual;
|
||||||
|
procedure Stop; virtual;
|
||||||
|
|
||||||
|
procedure SetUIParent(AParent: TWinControl); virtual;
|
||||||
|
|
||||||
|
property Fans: ISimulatorFans read FFans;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
TSimulatorFrame<T: TSimulatorBaseFrame> = class(TInterfacedObject, ISimulator)
|
||||||
|
private
|
||||||
|
FSimulatorFrame: TSimulatorBaseFrame;
|
||||||
|
protected
|
||||||
|
property SimulatorFrame: TSimulatorBaseFrame read FSimulatorFrame;
|
||||||
|
public
|
||||||
|
constructor Create(AFans: ISimulatorFans);
|
||||||
|
|
||||||
|
{ ISimulator }
|
||||||
|
procedure Start;
|
||||||
|
procedure Stop;
|
||||||
|
|
||||||
|
procedure SetUIParent(AParent: TWinControl);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
{$R *.dfm}
|
||||||
|
|
||||||
|
{ TSimulatorBaseFrame }
|
||||||
|
constructor TSimulatorBaseFrame.Create(AFans: ISimulatorFans);
|
||||||
|
begin
|
||||||
|
inherited Create(nil);
|
||||||
|
|
||||||
|
FFans := AFans;
|
||||||
|
Initialize;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TSimulatorBaseFrame.Initialize;
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TSimulatorBaseFrame.Start;
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TSimulatorBaseFrame.Stop;
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TSimulatorBaseFrame.SetUIParent(AParent: TWinControl);
|
||||||
|
begin
|
||||||
|
Align := alClient;
|
||||||
|
Parent := AParent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
{ TSimulatorFrame<T> }
|
||||||
|
constructor TSimulatorFrame<T>.Create(AFans: ISimulatorFans);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
|
||||||
|
FSimulatorFrame := TSimulatorBaseFrameClass(T).Create(AFans);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TSimulatorFrame<T>.Start;
|
||||||
|
begin
|
||||||
|
FSimulatorFrame.Start;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TSimulatorFrame<T>.Stop;
|
||||||
|
begin
|
||||||
|
FSimulatorFrame.Stop;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TSimulatorFrame<T>.SetUIParent(AParent: TWinControl);
|
||||||
|
begin
|
||||||
|
FSimulatorFrame.SetUIParent(AParent);
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
27
Client/source/Simulator.Manual.dfm
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
inherited ManualFrame: TManualFrame
|
||||||
|
Width = 544
|
||||||
|
Height = 49
|
||||||
|
ParentFont = False
|
||||||
|
ExplicitWidth = 544
|
||||||
|
ExplicitHeight = 49
|
||||||
|
object ValueTrackBar: TTrackBar
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 544
|
||||||
|
Height = 41
|
||||||
|
Align = alTop
|
||||||
|
Max = 255
|
||||||
|
Frequency = 5
|
||||||
|
PositionToolTip = ptBottom
|
||||||
|
ShowSelRange = False
|
||||||
|
TabOrder = 0
|
||||||
|
TickMarks = tmBoth
|
||||||
|
end
|
||||||
|
object UpdateTimer: TTimer
|
||||||
|
Enabled = False
|
||||||
|
Interval = 100
|
||||||
|
OnTimer = UpdateTimerTimer
|
||||||
|
Left = 488
|
||||||
|
Top = 8
|
||||||
|
end
|
||||||
|
end
|
67
Client/source/Simulator.Manual.pas
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
unit Simulator.Manual;
|
||||||
|
|
||||||
|
// #ToDo1 -oMvR: 2-9-2017: configurable speed for full fan power
|
||||||
|
|
||||||
|
interface
|
||||||
|
uses
|
||||||
|
System.Classes,
|
||||||
|
Vcl.Controls,
|
||||||
|
Vcl.ExtCtrls,
|
||||||
|
Vcl.Forms,
|
||||||
|
Vcl.StdCtrls,
|
||||||
|
|
||||||
|
Simulator.Base,
|
||||||
|
Simulator.Registry, Vcl.Samples.Spin, Vcl.ComCtrls;
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
TManualFrame = class(TSimulatorBaseFrame)
|
||||||
|
UpdateTimer: TTimer;
|
||||||
|
ValueTrackBar: TTrackBar;
|
||||||
|
|
||||||
|
procedure UpdateTimerTimer(Sender: TObject);
|
||||||
|
public
|
||||||
|
procedure Start; override;
|
||||||
|
procedure Stop; override;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
implementation
|
||||||
|
uses
|
||||||
|
System.SysUtils;
|
||||||
|
|
||||||
|
|
||||||
|
{$R *.dfm}
|
||||||
|
|
||||||
|
|
||||||
|
{ TManualFrame }
|
||||||
|
procedure TManualFrame.UpdateTimerTimer(Sender: TObject);
|
||||||
|
begin
|
||||||
|
Fans.SetAll(ValueTrackBar.Position);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TManualFrame.Start;
|
||||||
|
begin
|
||||||
|
inherited Start;
|
||||||
|
|
||||||
|
UpdateTimer.Enabled := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TManualFrame.Stop;
|
||||||
|
begin
|
||||||
|
UpdateTimer.Enabled := False;
|
||||||
|
|
||||||
|
inherited Stop;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
initialization
|
||||||
|
RegisterSimulator('Manual', 'Manual fan control',
|
||||||
|
function(AFans: ISimulatorFans): ISimulator
|
||||||
|
begin
|
||||||
|
Result := TSimulatorFrame<TManualFrame>.Create(AFans);
|
||||||
|
end);
|
||||||
|
|
||||||
|
end.
|
96
Client/source/Simulator.Registry.pas
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
unit Simulator.Registry;
|
||||||
|
|
||||||
|
interface
|
||||||
|
uses
|
||||||
|
Vcl.Controls,
|
||||||
|
|
||||||
|
System.Generics.Collections;
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
ISimulator = interface
|
||||||
|
['{732D49D6-2E54-4257-887A-B09D8F2771B1}']
|
||||||
|
procedure Start;
|
||||||
|
procedure Stop;
|
||||||
|
|
||||||
|
procedure SetUIParent(AParent: TWinControl);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
ISimulatorFans = interface
|
||||||
|
['{23A005ED-907A-4776-A9FB-70D213C7381C}']
|
||||||
|
function GetFanCount: Integer;
|
||||||
|
function GetReady: Boolean;
|
||||||
|
function GetIsRunning: Boolean;
|
||||||
|
|
||||||
|
function GetValue(const AFan: Byte): Byte;
|
||||||
|
function GetMaxValue: Byte;
|
||||||
|
|
||||||
|
procedure SetValue(const AFan, AValue: Byte);
|
||||||
|
procedure SetAll(const AValue: Byte);
|
||||||
|
|
||||||
|
procedure SetFull;
|
||||||
|
|
||||||
|
property Ready: Boolean read GetReady;
|
||||||
|
property IsRunning: Boolean read GetIsRunning;
|
||||||
|
property FanCount: Integer read GetFanCount;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
TSimulatorConstructor = reference to function(AFans: ISimulatorFans): ISimulator;
|
||||||
|
|
||||||
|
TRegisteredSimulator = class(TObject)
|
||||||
|
private
|
||||||
|
FKey: string;
|
||||||
|
FName: string;
|
||||||
|
FConstructorFunc: TSimulatorConstructor;
|
||||||
|
public
|
||||||
|
constructor Create(const AKey, AName: string; AConstructor: TSimulatorConstructor);
|
||||||
|
|
||||||
|
property Key: string read FKey;
|
||||||
|
property Name: string read FName;
|
||||||
|
property ConstructorFunc: TSimulatorConstructor read FConstructorFunc;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure RegisterSimulator(const AKey, AName: string; AConstructor: TSimulatorConstructor);
|
||||||
|
function GetRegisteredSimulators: TEnumerable<TRegisteredSimulator>;
|
||||||
|
|
||||||
|
|
||||||
|
implementation
|
||||||
|
uses
|
||||||
|
System.SysUtils;
|
||||||
|
|
||||||
|
var
|
||||||
|
SimulatorsList: TObjectList<TRegisteredSimulator>;
|
||||||
|
|
||||||
|
|
||||||
|
procedure RegisterSimulator(const AKey, AName: string; AConstructor: TSimulatorConstructor);
|
||||||
|
begin
|
||||||
|
SimulatorsList.Add(TRegisteredSimulator.Create(AKey, AName, AConstructor));
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
function GetRegisteredSimulators: TEnumerable<TRegisteredSimulator>;
|
||||||
|
begin
|
||||||
|
Result := SimulatorsList;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
{ TRegisteredSimulator }
|
||||||
|
constructor TRegisteredSimulator.Create(const AKey, AName: string; AConstructor: TSimulatorConstructor);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
|
||||||
|
FKey := AKey;
|
||||||
|
FName := AName;
|
||||||
|
FConstructorFunc := AConstructor;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
initialization
|
||||||
|
SimulatorsList := TObjectList<TRegisteredSimulator>.Create;
|
||||||
|
|
||||||
|
finalization
|
||||||
|
FreeAndNil(SimulatorsList);
|
||||||
|
|
||||||
|
end.
|
BIN
Logo/SimulatorFans-16.png
Normal file
After Width: | Height: | Size: 515 B |
BIN
Logo/SimulatorFans-32.png
Normal file
After Width: | Height: | Size: 1008 B |
BIN
Logo/SimulatorFans-48.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Logo/SimulatorFans-64.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
4950
Logo/SimulatorFans.ai
Normal file
BIN
Logo/SimulatorFans.ico
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
Logo/SimulatorFans.png
Normal file
After Width: | Height: | Size: 15 KiB |
@ -4,7 +4,7 @@
|
|||||||
* https://git.x2software.net/pub/SimulatorFans
|
* https://git.x2software.net/pub/SimulatorFans
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* Accepts serial commands to control one or more fans using PWM.
|
* Accepts serial commands to control one or more fans.
|
||||||
*
|
*
|
||||||
* All commands are terminated with a #10 character and are case sensitive.
|
* All commands are terminated with a #10 character and are case sensitive.
|
||||||
* Values are passed as ASCII text. This makes it easy to test it using the
|
* Values are passed as ASCII text. This makes it easy to test it using the
|
||||||
@ -29,6 +29,11 @@
|
|||||||
*
|
*
|
||||||
* >SetFans:v1,v2,v...
|
* >SetFans:v1,v2,v...
|
||||||
* Updates the fan values. Each value ranges from 0 to 255.
|
* Updates the fan values. Each value ranges from 0 to 255.
|
||||||
|
* If the first value is 'A', the second value is applied to each fan.
|
||||||
|
*
|
||||||
|
* When in Servo mode, if the first value is 'M', the Servo is
|
||||||
|
* set to it's maximum pulse width, disregarding the MaxValue.
|
||||||
|
* Used for ESC calibration. USE WITH CARE!
|
||||||
*
|
*
|
||||||
* Example response:
|
* Example response:
|
||||||
* <SetFans
|
* <SetFans
|
||||||
@ -39,45 +44,98 @@
|
|||||||
* Configuration
|
* Configuration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Defines the method used for controlling the fans.
|
||||||
|
#define ModePWM 0
|
||||||
|
#define ModeServo 1
|
||||||
|
|
||||||
|
// ModePWM:
|
||||||
|
// Fans are connected through a transistor (or preferably MOSFET) directly
|
||||||
|
// from the pins. The PWM frequency is set low to reduce noise.
|
||||||
|
//
|
||||||
|
// ModeServo:
|
||||||
|
// Intended for use with an ESC (Electronic Speed Controller). The PWM signal
|
||||||
|
// on each pin will be compatible with standard ESCs or servos.
|
||||||
|
#define FanMode ModeServo
|
||||||
|
|
||||||
|
|
||||||
// The number of fans connected. Each fan must connect to it's own
|
// The number of fans connected. Each fan must connect to it's own
|
||||||
// PWM-enabled pin through a transistor (or preferably MOSFET).
|
// PWM-enabled or Servo-supported pin.
|
||||||
#define FanCount 2
|
#define FanCount 1
|
||||||
|
|
||||||
// The pin on which each of the fans is connected. The number of
|
// The pin on which each of the fans is connected. The number of
|
||||||
// values must be equal to FanCount (not sure why the compiler
|
// values must be equal to FanCount (not sure why the compiler
|
||||||
// doesn't enforce this if FanCount is bigger). The order is
|
// doesn't enforce this if FanCount is bigger). The order is
|
||||||
// assumed to be left to right. In a setup of more than 180 degrees,
|
// assumed to be left to right.
|
||||||
// start counting behind the point of view and in a clockwise manner.
|
|
||||||
//
|
|
||||||
// Possibly the PC software could support remapping at some point in the
|
|
||||||
// future as that's easier than recompiling for the Arduino, but I
|
|
||||||
// wanted to keep it simple until then.
|
|
||||||
const byte FanPin[FanCount] =
|
const byte FanPin[FanCount] =
|
||||||
{
|
{
|
||||||
5,
|
9
|
||||||
6
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PWM mode configuration
|
||||||
|
*/
|
||||||
// When a fan goes from completely off to a value below full, this
|
// When a fan goes from completely off to a value below full, this
|
||||||
// determines how long the fan will run on full power before changing
|
// determines how long the fan will run on full power before changing
|
||||||
// to the actual value, to give it a chance to start up.
|
// to the actual value, to give it a chance to start up.
|
||||||
#define StartingFansTime 200
|
#define PWMStartingFansTime 200
|
||||||
|
|
||||||
|
|
||||||
// The minimum value at which the fan still spins reliably. Anything below
|
// The minimum value at which the fan still spins reliably. Anything below
|
||||||
// will be considered as 0.
|
// will be considered as 0.
|
||||||
#define MinimumValue 32
|
#define PWMMinimumValue 32
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Servo mode configuration
|
||||||
|
*/
|
||||||
|
// The minimum pulse width. Servo library uses a default of 544.
|
||||||
|
#define ServoMinPulseWidth 544
|
||||||
|
|
||||||
|
// The maximum pulse width. Servo library uses a default of 2400.
|
||||||
|
#define ServoMaxPulseWidth 2400
|
||||||
|
|
||||||
|
#define ServoPulseRange (ServoMaxPulseWidth - ServoMinPulseWidth)
|
||||||
|
|
||||||
|
|
||||||
|
// The pulse width when the input value is 0, in microseconds.
|
||||||
|
// Using microseconds instead of the Servo library's simpler 0-180 degree mapping
|
||||||
|
// allows for more finegrained changes when limiting the values.
|
||||||
|
#define ServoMinValue ServoMinPulseWidth
|
||||||
|
|
||||||
|
// The pulse width when the input value is 255, in microseconds.
|
||||||
|
// Use this to limit the maximum "throttle" to the ESC.
|
||||||
|
#define ServoMaxValue (ServoMinPulseWidth + (ServoPulseRange / 4))
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Actual code. Lasciate ogne speranza, voi ch'intrate.
|
* Actual code. Lasciate ogne speranza, voi ch'intrate.
|
||||||
*/
|
*/
|
||||||
|
#define IsPWM (FanMode == ModePWM)
|
||||||
|
#define IsServo (FanMode == ModeServo)
|
||||||
|
|
||||||
|
#if IsServo
|
||||||
|
#include <Servo.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
byte value;
|
byte value;
|
||||||
|
|
||||||
|
#if IsPWM
|
||||||
unsigned long startTime;
|
unsigned long startTime;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if IsServo
|
||||||
|
Servo servo;
|
||||||
|
#endif
|
||||||
} FanStatus;
|
} FanStatus;
|
||||||
|
|
||||||
|
|
||||||
|
#if IsPWM
|
||||||
unsigned long currentTime = 0;
|
unsigned long currentTime = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
FanStatus fanStatus[FanCount];
|
FanStatus fanStatus[FanCount];
|
||||||
|
|
||||||
|
|
||||||
@ -85,9 +143,11 @@ FanStatus fanStatus[FanCount];
|
|||||||
void handleInfoCommand();
|
void handleInfoCommand();
|
||||||
void handleGetFansCommand();
|
void handleGetFansCommand();
|
||||||
void handleSetFansCommand();
|
void handleSetFansCommand();
|
||||||
void handleUnknownCommand();
|
void handleUnknownCommand(char* command);
|
||||||
|
|
||||||
|
#if IsPWM
|
||||||
void checkStartingFans();
|
void checkStartingFans();
|
||||||
|
#endif
|
||||||
void setFan(byte fan, byte value);
|
void setFan(byte fan, byte value);
|
||||||
|
|
||||||
|
|
||||||
@ -96,8 +156,19 @@ void setup()
|
|||||||
memset(fanStatus, 0, sizeof(fanStatus));
|
memset(fanStatus, 0, sizeof(fanStatus));
|
||||||
|
|
||||||
for (byte fan = 0; fan < FanCount; fan++)
|
for (byte fan = 0; fan < FanCount; fan++)
|
||||||
|
{
|
||||||
|
#if IsPWM
|
||||||
pinMode(FanPin[fan], OUTPUT);
|
pinMode(FanPin[fan], OUTPUT);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if IsServo
|
||||||
|
fanStatus[fan].servo.attach(FanPin[fan]);
|
||||||
|
fanStatus[fan].servo.writeMicroseconds(ServoMinValue);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if IsPWM
|
||||||
// Set all clock select bits to clk/1024. This will result in a 30 ~ 60 Hz PWM frequency.
|
// Set all clock select bits to clk/1024. This will result in a 30 ~ 60 Hz PWM frequency.
|
||||||
// The default frequency causes annoying noises in the fan, the low frequency is still
|
// The default frequency causes annoying noises in the fan, the low frequency is still
|
||||||
// audible but good enough since the game sounds should easily drown that out.
|
// audible but good enough since the game sounds should easily drown that out.
|
||||||
@ -108,6 +179,7 @@ void setup()
|
|||||||
|
|
||||||
// 64 is the default prescaler
|
// 64 is the default prescaler
|
||||||
#define correctedMillis() (millis() * (1024 / 64))
|
#define correctedMillis() (millis() * (1024 / 64))
|
||||||
|
#endif
|
||||||
|
|
||||||
// Set up serial communication (through USB or the default pins)
|
// Set up serial communication (through USB or the default pins)
|
||||||
// 19.2k is fast enough for our purpose, and according to the ATMega's datasheet
|
// 19.2k is fast enough for our purpose, and according to the ATMega's datasheet
|
||||||
@ -118,13 +190,16 @@ void setup()
|
|||||||
|
|
||||||
char command[50];
|
char command[50];
|
||||||
byte commandLength;
|
byte commandLength;
|
||||||
|
byte offset;
|
||||||
char* token;
|
char* token;
|
||||||
|
|
||||||
|
|
||||||
void loop()
|
void loop()
|
||||||
{
|
{
|
||||||
|
#if IsPWM
|
||||||
currentTime = correctedMillis();
|
currentTime = correctedMillis();
|
||||||
checkStartingFans();
|
checkStartingFans();
|
||||||
|
#endif
|
||||||
|
|
||||||
if (Serial.available() > 0)
|
if (Serial.available() > 0)
|
||||||
{
|
{
|
||||||
@ -133,7 +208,16 @@ void loop()
|
|||||||
commandLength = Serial.readBytesUntil('\n', command, sizeof(command) - 1);
|
commandLength = Serial.readBytesUntil('\n', command, sizeof(command) - 1);
|
||||||
if (commandLength > 0 && commandLength < sizeof(command))
|
if (commandLength > 0 && commandLength < sizeof(command))
|
||||||
{
|
{
|
||||||
token = strtok(&command[0], ":");
|
// The Windows application always sends two 0xFF characters when it first connects.
|
||||||
|
// I'm sure it's a setting somewhere, but I couldn't figure it out just yet. Instead,
|
||||||
|
// simply look for the start of the command, thus skipping all other bytes. A bit
|
||||||
|
// of a hack, but if you know the proper solution, I'd love to hear it!
|
||||||
|
offset = 0;
|
||||||
|
|
||||||
|
while (offset < commandLength && command[offset] != '>')
|
||||||
|
offset++;
|
||||||
|
|
||||||
|
token = strtok(&command[offset], ":");
|
||||||
if (token != NULL)
|
if (token != NULL)
|
||||||
{
|
{
|
||||||
if (strcmp(token, ">Info") == 0)
|
if (strcmp(token, ">Info") == 0)
|
||||||
@ -143,7 +227,7 @@ void loop()
|
|||||||
else if (strcmp(token, ">SetFans") == 0)
|
else if (strcmp(token, ">SetFans") == 0)
|
||||||
handleSetFansCommand();
|
handleSetFansCommand();
|
||||||
else
|
else
|
||||||
handleUnknownCommand();
|
handleUnknownCommand(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,6 +238,13 @@ void handleInfoCommand()
|
|||||||
{
|
{
|
||||||
Serial.write("<Info:Fans=");
|
Serial.write("<Info:Fans=");
|
||||||
Serial.print(FanCount);
|
Serial.print(FanCount);
|
||||||
|
Serial.write(",Mode=");
|
||||||
|
#if IsPWM
|
||||||
|
Serial.write("PWM");
|
||||||
|
#endif
|
||||||
|
#if IsServo
|
||||||
|
Serial.write("Servo");
|
||||||
|
#endif
|
||||||
Serial.write("\n");
|
Serial.write("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,39 +266,76 @@ void handleGetFansCommand()
|
|||||||
|
|
||||||
|
|
||||||
void handleSetFansCommand()
|
void handleSetFansCommand()
|
||||||
|
{
|
||||||
|
token = strtok(NULL, ",");
|
||||||
|
if (token != NULL)
|
||||||
|
{
|
||||||
|
if (strcmp(token, "A") == 0)
|
||||||
|
{
|
||||||
|
// Set all fans to the same value
|
||||||
|
token = strtok(NULL, ",");
|
||||||
|
if (token != NULL)
|
||||||
|
{
|
||||||
|
int value = atoi(token);
|
||||||
|
|
||||||
|
if (value < 0) value = 0;
|
||||||
|
if (value > 255) value = 0;
|
||||||
|
|
||||||
|
for (byte fan = 0; fan < FanCount; fan++)
|
||||||
|
setFan(fan, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if IsServo
|
||||||
|
else if (strcmp(token, "M") == 0)
|
||||||
{
|
{
|
||||||
for (byte fan = 0; fan < FanCount; fan++)
|
for (byte fan = 0; fan < FanCount; fan++)
|
||||||
{
|
{
|
||||||
token = strtok(NULL, ",");
|
// Join me for a drive, maximum overdrive!
|
||||||
if (token == NULL)
|
fanStatus[fan].servo.writeMicroseconds(ServoMaxPulseWidth);
|
||||||
break;
|
fanStatus[fan].value = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set individual fan values
|
||||||
|
for (byte fan = 0; fan < FanCount; fan++)
|
||||||
|
{
|
||||||
int value = atoi(token);
|
int value = atoi(token);
|
||||||
|
|
||||||
if (value < 0) value = 0;
|
if (value < 0) value = 0;
|
||||||
if (value > 255) value = 0;
|
if (value > 255) value = 0;
|
||||||
|
|
||||||
setFan(fan, value);
|
setFan(fan, value);
|
||||||
|
|
||||||
|
token = strtok(NULL, ",");
|
||||||
|
if (token == NULL)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.write("<SetFans\n");
|
Serial.write("<SetFans\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void handleUnknownCommand()
|
void handleUnknownCommand(char* command)
|
||||||
{
|
{
|
||||||
Serial.write("<Error:unknown command\n");
|
Serial.write("<Error:unknown command:");
|
||||||
|
Serial.write(command);
|
||||||
|
Serial.write("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if IsPWM
|
||||||
void checkStartingFans()
|
void checkStartingFans()
|
||||||
{
|
{
|
||||||
// Check if any of the fans are currently starting up and
|
// Check if any of the fans are currently starting up and
|
||||||
// have been for at least StartingFansTime
|
// have been for at least PWMStartingFansTime
|
||||||
for (byte fan = 0; fan < FanCount; fan++)
|
for (byte fan = 0; fan < FanCount; fan++)
|
||||||
{
|
{
|
||||||
if ((fanStatus[fan].startTime > 0) &&
|
if ((fanStatus[fan].startTime > 0) &&
|
||||||
(currentTime - fanStatus[fan].startTime >= StartingFansTime))
|
(currentTime - fanStatus[fan].startTime >= PWMStartingFansTime))
|
||||||
{
|
{
|
||||||
fanStatus[fan].startTime = 0;
|
fanStatus[fan].startTime = 0;
|
||||||
setFan(fan, fanStatus[fan].value);
|
setFan(fan, fanStatus[fan].value);
|
||||||
@ -218,7 +346,7 @@ void checkStartingFans()
|
|||||||
void setFan(byte fan, byte value)
|
void setFan(byte fan, byte value)
|
||||||
{
|
{
|
||||||
byte correctedValue = value;
|
byte correctedValue = value;
|
||||||
if (correctedValue < MinimumValue)
|
if (correctedValue < PWMMinimumValue)
|
||||||
correctedValue = 0;
|
correctedValue = 0;
|
||||||
|
|
||||||
if ((fanStatus[fan].value == 0 || fanStatus[fan].startTime > 0) && correctedValue > 0)
|
if ((fanStatus[fan].value == 0 || fanStatus[fan].startTime > 0) && correctedValue > 0)
|
||||||
@ -240,3 +368,12 @@ void setFan(byte fan, byte value)
|
|||||||
|
|
||||||
fanStatus[fan].value = correctedValue;
|
fanStatus[fan].value = correctedValue;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if IsServo
|
||||||
|
void setFan(byte fan, byte value)
|
||||||
|
{
|
||||||
|
fanStatus[fan].servo.writeMicroseconds(map(value, 0, 255, ServoMinValue, ServoMaxValue));
|
||||||
|
fanStatus[fan].value = value;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|