Added Servo mode

Refactored client to allow more simulators
This commit is contained in:
Mark van Renswoude 2017-09-10 15:15:32 +02:00
parent 0c142ed577
commit 96737bdc9a
31 changed files with 6482 additions and 382 deletions

11
.gitignore vendored
View File

@ -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

View File

@ -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.

Binary file not shown.

View File

@ -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

View File

@ -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
View 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.

View File

@ -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

Binary file not shown.

BIN
Client/resources/lock.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

View 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

View 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
View 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
View 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.

View 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

View 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.

View File

@ -0,0 +1,7 @@
object SimulatorBaseFrame: TSimulatorBaseFrame
Left = 0
Top = 0
Width = 467
Height = 165
TabOrder = 0
end

View 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.

View 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

View 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.

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

BIN
Logo/SimulatorFans-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

BIN
Logo/SimulatorFans-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
Logo/SimulatorFans-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

4950
Logo/SimulatorFans.ai Normal file

File diff suppressed because one or more lines are too long

BIN
Logo/SimulatorFans.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
Logo/SimulatorFans.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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");
} }
@ -176,38 +267,75 @@ 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++)
{
// Join me for a drive, maximum overdrive!
fanStatus[fan].servo.writeMicroseconds(ServoMaxPulseWidth);
fanStatus[fan].value = 255;
}
}
#endif
else
{
// Set individual fan values
for (byte fan = 0; fan < FanCount; fan++) for (byte fan = 0; fan < FanCount; fan++)
{ {
token = strtok(NULL, ",");
if (token == NULL)
break;
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