SimulatorFans/SimulatorFans.ino

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;
}