373 lines
9.0 KiB
C++
373 lines
9.0 KiB
C++
/*
|
|
* SimulatorFans
|
|
* Copyright (c) 2017 Mark van Renswoude
|
|
* https://git.x2software.net/pub/SimulatorFans
|
|
*
|
|
*
|
|
* Accepts serial commands to control one or more fans.
|
|
*
|
|
* 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
|
|
* Arduino's Serial Monitor.
|
|
*
|
|
* Baud rate is 19200 by default.
|
|
*
|
|
*
|
|
* Commands:
|
|
* >Info
|
|
* Used for validating that the device is actually a SimulatorFans device.
|
|
* Returns the number of connected fans.
|
|
*
|
|
* Example response:
|
|
* <Info:Fans=2
|
|
*
|
|
* >GetFans
|
|
* Returns the currently set values for all fans.
|
|
*
|
|
* Example response:
|
|
* <GetFans:0,255
|
|
*
|
|
* >SetFans:v1,v2,v...
|
|
* 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:
|
|
* <SetFans
|
|
*/
|
|
|
|
|
|
/*
|
|
* 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
|
|
// PWM-enabled or Servo-supported pin.
|
|
#define FanCount 1
|
|
|
|
// The pin on which each of the fans is connected. The number of
|
|
// values must be equal to FanCount (not sure why the compiler
|
|
// doesn't enforce this if FanCount is bigger). The order is
|
|
// assumed to be left to right.
|
|
const byte FanPin[FanCount] =
|
|
{
|
|
9
|
|
};
|
|
|
|
|
|
// When a fan goes from completely off to a value below full, this
|
|
// determines how long the fan will run on a higher value before changing
|
|
// to the actual value, to give it a chance to start up.
|
|
#define StartingFansTime 200
|
|
|
|
// The value used when the fan is starting up (range 0 - 255)
|
|
#define StartingValue 128
|
|
|
|
|
|
// The minimum value at which the fan still spins reliably. Anything below
|
|
// will be considered as 0.
|
|
#define MinimumValue 30
|
|
|
|
|
|
/*
|
|
* Servo mode configuration
|
|
*/
|
|
// The minimum pulse width. Servo library uses a default of 544.
|
|
#define ServoMinPulseWidth 1000
|
|
|
|
// The maximum pulse width. Servo library uses a default of 2400.
|
|
#define ServoMaxPulseWidth 2000
|
|
|
|
#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
|
|
|
|
|
|
/*
|
|
* Actual code. Lasciate ogne speranza, voi ch'intrate.
|
|
*/
|
|
#define IsPWM (FanMode == ModePWM)
|
|
#define IsServo (FanMode == ModeServo)
|
|
|
|
|
|
#if IsServo
|
|
#include <Servo.h>
|
|
#endif
|
|
|
|
typedef struct
|
|
{
|
|
byte value;
|
|
unsigned long startTime;
|
|
|
|
#if IsServo
|
|
Servo servo;
|
|
#endif
|
|
} FanStatus;
|
|
|
|
|
|
unsigned long currentTime = 0;
|
|
FanStatus fanStatus[FanCount];
|
|
|
|
|
|
// Forward declarations
|
|
void handleInfoCommand();
|
|
void handleGetFansCommand();
|
|
void handleSetFansCommand();
|
|
void handleUnknownCommand(char* command);
|
|
|
|
void checkStartingFans();
|
|
void setFan(byte fan, byte value);
|
|
|
|
|
|
void setup()
|
|
{
|
|
memset(fanStatus, 0, sizeof(fanStatus));
|
|
|
|
for (byte fan = 0; fan < FanCount; fan++)
|
|
{
|
|
#if IsPWM
|
|
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.
|
|
// 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.
|
|
// It messes up the millis() function though, so compensate for that later on.
|
|
TCCR0B = TCCR0B & 0b11111000 | 0b101;
|
|
TCCR1B = TCCR1B & 0b11111000 | 0b101;
|
|
TCCR2B = TCCR2B & 0b11111000 | 0b101;
|
|
|
|
// 64 is the default prescaler
|
|
#define correctedMillis() (millis() * (1024 / 64))
|
|
#endif
|
|
|
|
|
|
#if IsServo
|
|
#define correctedMillis() millis()
|
|
#endif
|
|
|
|
|
|
// 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
|
|
// has a low error percentage across the common oscillator frequencies.
|
|
Serial.begin(19200);
|
|
}
|
|
|
|
|
|
char command[50];
|
|
byte commandLength;
|
|
byte offset;
|
|
char* token;
|
|
|
|
|
|
void loop()
|
|
{
|
|
currentTime = correctedMillis();
|
|
checkStartingFans();
|
|
|
|
if (Serial.available() > 0)
|
|
{
|
|
// Try to read a serial command
|
|
memset(command, 0, sizeof(command));
|
|
commandLength = Serial.readBytesUntil('\n', command, sizeof(command) - 1);
|
|
if (commandLength > 0 && commandLength < sizeof(command))
|
|
{
|
|
// 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 (strcmp(token, ">Info") == 0)
|
|
handleInfoCommand();
|
|
else if (strcmp(token, ">GetFans") == 0)
|
|
handleGetFansCommand();
|
|
else if (strcmp(token, ">SetFans") == 0)
|
|
handleSetFansCommand();
|
|
else
|
|
handleUnknownCommand(token);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void handleInfoCommand()
|
|
{
|
|
Serial.write("<Info:Fans=");
|
|
Serial.print(FanCount);
|
|
Serial.write(",Mode=");
|
|
#if IsPWM
|
|
Serial.write("PWM");
|
|
#endif
|
|
#if IsServo
|
|
Serial.write("Servo");
|
|
#endif
|
|
Serial.write("\n");
|
|
}
|
|
|
|
|
|
void handleGetFansCommand()
|
|
{
|
|
Serial.write("<GetFans:");
|
|
|
|
for (byte fan = 0; fan < FanCount; fan++)
|
|
{
|
|
if (fan > 0)
|
|
Serial.write(",");
|
|
|
|
Serial.print(fanStatus[fan].value);
|
|
}
|
|
|
|
Serial.write("\n");
|
|
}
|
|
|
|
|
|
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++)
|
|
{
|
|
int value = atoi(token);
|
|
|
|
if (value < 0) value = 0;
|
|
if (value > 255) value = 0;
|
|
|
|
setFan(fan, value);
|
|
|
|
token = strtok(NULL, ",");
|
|
if (token == NULL)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Serial.write("<SetFans\n");
|
|
}
|
|
|
|
|
|
void handleUnknownCommand(char* command)
|
|
{
|
|
Serial.write("<Error:unknown command:");
|
|
Serial.write(command);
|
|
Serial.write("\n");
|
|
}
|
|
|
|
|
|
void checkStartingFans()
|
|
{
|
|
// Check if any of the fans are currently starting up
|
|
for (byte fan = 0; fan < FanCount; fan++)
|
|
{
|
|
if ((fanStatus[fan].startTime > 0) &&
|
|
(currentTime - fanStatus[fan].startTime >= StartingFansTime))
|
|
{
|
|
fanStatus[fan].startTime = 0;
|
|
setFan(fan, fanStatus[fan].value);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if IsPWM
|
|
#define writeFanValue(fan, value) analogWrite(FanPin[fan], value)
|
|
#endif
|
|
#if IsServo
|
|
#define writeFanValue(fan, value) fanStatus[fan].servo.writeMicroseconds(map(value, 0, 255, ServoMinValue, ServoMaxValue))
|
|
#endif
|
|
|
|
void setFan(byte fan, byte value)
|
|
{
|
|
byte correctedValue = value;
|
|
if (correctedValue < MinimumValue)
|
|
correctedValue = 0;
|
|
|
|
if ((fanStatus[fan].value == 0 || fanStatus[fan].startTime > 0) && correctedValue > 0)
|
|
{
|
|
// Fan was off or still starting up, start with full power to kick it off
|
|
writeFanValue(fan, StartingValue);
|
|
|
|
if (fanStatus[fan].startTime == 0)
|
|
{
|
|
fanStatus[fan].startTime = currentTime;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Already running, simply change the speed and reset the start time if necessary
|
|
fanStatus[fan].startTime = 0;
|
|
writeFanValue(fan, correctedValue);
|
|
}
|
|
|
|
fanStatus[fan].value = correctedValue;
|
|
}
|