/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

/* 
  cpufreq.cc

  Carl Thompson <cet@carlthompson.net>

 ---------------------------------------------------------------------------
  Ported to support the rtdvs powernowK7 module instead of cpufreq 
  by Peter Weissgerber <p@p-weissgerber.de> 

  
  To use this software, you have to create two configuration files:

  /etc/powernow-speed-min
    this file must contain the minimal speed that should be used (in MHz).
    For example, if you want to set it to 500 MHz, type
    "echo 500 >/etc/powernow-speed-min".

  /etc/powernow-speed-max
    this file must contain the maximal speed that should be used (in MHz).
    For example, if you want to set it to 1 GHz, type
    "echo 1000 >/etc/powernow-speed-max".

 ---------------------------------------------------------------------------


  Contributors:  
    Enrico Tassi <gareuselesinge@libero.it>
    Gerald Teschl <gerald.teschl@univie.ac.at>
    Peter Weissgerber <p@p-weissgerber.de>
    This program is only for computers using CPUFreq.  You must have a CPU
  that supports frequency and/or voltage scaling via CPUFreq to use this 
  program.  Your CPUFreq must be compiled to support the "userspace"
  governor and the sysfs interface or the /proc/sys/cpu/ interface.
  I use this program on my Sony Vaio laptop to increase battery life and
  control performance.
  
  This program monitors the system's idle percentage and reduces or
  raises the CPU clock speed and voltage accordingly to minimize power
  consumption when idle and maximize performance when needed.  This is
  the default behavior.
  
  To tell the program to stay at the highest CPU clock speed to maximize
  performance, send the process the SIGUSR1 signal.
  
  To tell the program to stay at the lowest CPU clock speed to maximize
  battery life, send the process the SIGUSR2 signal.
  
  To tell the program to go back to scaling the CPU clock speed dynamically,
  send the process the SIGHUP signal.
  
  I recommend compiling this program with gcc instead of g++ so that the
  libstdc++ library is not linked.
  
  Usage:
  
    cpufreq [Options]

    Options:
        -d                         - Daemonize process (run in background)

        -i <interval>              - Change interval between idle ratio tests
                                     and possible speed change in 1/10 second
                                     increments (default is 20)

        -p <fast up> <up> <down>   - Set CPU idle:work ratios to speed up or
                                     down CPU (default is 0.10 0.25 0.75)

        -r <off> <on>             -  Set CPU throttling states
                                     (default is 0 10 and 0 0 disables)

        -t <temp file> <maxtemp>   - Set ACPI temperature file and temperature
                                     at which CPU will be set to minimum speed 

        -a <AC file>               - Set ACPI AC adapter state file and 
	                             set CPU to minimum speed 
				     when AC is disconnected (can be
				     overwritten by -ac-off)

        -ac-on <DYNAMIC|MAX|MIN>   - Overwrite strategy to use when AC is 
	                             connected (default: DYNAMIC)

	-ac-off <DYNAMIC|MAX|MIN>  - Overwrite strategy to use when AC is
	                             disconnected (default: MIN)


  Example:
  
    cpufreq -d -i 10 -t /proc/acpi/thermal_zone/THRM/temperature 79 \
            -a /proc/acpi/ac_adapter/ACAD/state 

    This will have the program decide check (and possibly change) the CPU speed
    once every second and go to minimum CPU speed if the temperature goes above
    79 or the AC adapter is disconnected.
*/
  
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/errno.h>
#include <stdarg.h>
#include <time.h>

// print debugging output
#define DEBUG   0

// Maximum number of speed steps.  Must be 2 or greater
#define MAX_SPEEDS  20

// To activate debug printings
#if DEBUG
#   define errprintf(A...) fprintf(stderr,A)
#else
#   define errprintf(A...) 
#endif

#   define MIN_SPEED_FILE       "/etc/powernow-speed-min"
#   define MAX_SPEED_FILE       "/etc/powernow-speed-max"
#   define CURRENT_SPEED_FILE   "/proc/powernow"
#   define THROTTLING_FILE      "/proc/acpi/processor/CPU0/throttling"

// should we throttle the cpu
bool use_throttling = true;
unsigned throttling_off = 0;
unsigned throttling_on = 10;
unsigned current_throttling = throttling_off;

// defines what info we care about for each speed step
struct {
    unsigned khz;
    //unsigned volts;
    //unsigned fsb;
} speeds[MAX_SPEEDS + 1];

// display an error message and exit the program
void
die(bool system_error, const char *fmt, ...)
{
    fprintf(stderr, "Error: ");
    
    va_list ap;
    va_start(ap, fmt);         // get variable argument list passed
    vfprintf(stderr, fmt, ap); // display message passed on stderr
    va_end(ap);
    
    fprintf(stderr, "\n");
    if (system_error)
        fprintf(stderr, "Error: %s\n", strerror(errno));

    exit(1);
}

// read a line from a file
void
read_line(const char * filename, char * line, unsigned len)
{
    FILE * fp = fopen(filename, "r");
    if (!fp)
        die(true, "Could not open file for reading: %s", filename);
    if ( (!fgets(line, len, fp)) )
        die(true, "Could not read from file: %s", filename);
    fclose(fp);
}

// write a line to a file
void
write_line(const char * filename, const char *fmt, ...)
{
    FILE * fp = fopen(filename, "w+");
    if (!fp)
        die(true, "Could not open file for writing: %s", filename);
    va_list ap;
    va_start(ap, fmt);         // get variable argument list passed
    if (vfprintf(fp, fmt, ap) < 0)
        die(true, "Could not write to file: %s", filename);
    va_end(ap);
    fclose(fp);
}

// read an integer value from a file
unsigned
read_value(const char * filename)
{
    char line[256];
    read_line(filename, line, sizeof line);
    errprintf("Line: %s\n",line);
    errprintf("This means: %u\n",atoi(line));
    return atoi(line);
}

// set the current CPU throttling state
void set_throttling(unsigned value)
{
    errprintf("Setting throttling state to: %u\n", value);
    write_line(THROTTLING_FILE, "%u\n", value);
	current_throttling= value;
}

// get the current CPU speed
unsigned get_speed() 
{ 
  char line[256];
  char line2[256];
  read_line(CURRENT_SPEED_FILE, line, sizeof line);
  int max;
  max = 0;
  int i;
  for (i=0; ((i<256) && (max==0)); i++) {
    if (line[i]=='M') max=i;
  }
  strncpy(line2,&(line[9]),max-9);
  return atoi(line2); 
}

// set the current CPU speed
void set_speed(unsigned value)
{
    write_line(CURRENT_SPEED_FILE, "%u\n", value);
    usleep(5000); // CET - FIXME - Is this necessary for any CPUs?
}

// This code smoothly transitions the CPU speed from 'current' to 'target'
// instead of jumping directly to the new speed because some AMD Mobile
// Athlon parts seem to choke on large differentials causing kernel panics.
void set_speed(unsigned current, unsigned target)
{
    if (current == target)
        return;
    int delta = (current > target) ? -1 : 1;
    do
    {
        current += delta;
        set_speed(speeds[current].khz);
    }
    while (current != target);
}

// lowest speed step
unsigned last_step;

// get the speed steps supported by the CPU
void
get_supported_speeds()
{
    // this code is a hack to get the various speed steps supported by the
    // CPU by looping from the maximum speed to the minimum speed and trying
    // to set every possible speed divisible by step! 
    unsigned min = read_value(MIN_SPEED_FILE);
    unsigned max = read_value(MAX_SPEED_FILE);
    const unsigned step = 50;
    errprintf("Min speed: %u Max speed: %u\n", min, max);
    
    speeds[0].khz = max;
    unsigned current_speed = 0;    
    for (unsigned current = max - step; current > min - step; current -= step)
    {
        set_speed(current);
        unsigned real = get_speed();
        if (real != speeds[current_speed].khz)
        {
            speeds[++current_speed].khz = real;
            if (current_speed + 1 == MAX_SPEEDS)
                break;
        }
    }
    speeds[current_speed + 1].khz = 0;
    
    // go back to maximum speed because program expects to start there
    set_speed(current_speed, 0);

    // the last step is the lowest found speed
    last_step = current_speed;
    
#if DEBUG
    errprintf("Available speeds:\n");
    for (current_speed = 0; speeds[current_speed].khz; current_speed++)
        errprintf(" %2u: %9uMHz\n", current_speed, speeds[current_speed].khz);
#endif
}

// gets the elapsed total time and elapsed idle time since it was last called
void
get_times(unsigned long & total_elapsed, unsigned long & idle_elapsed)
{
    char what[32];
    unsigned long user_time, nice_time, system_time, idle_time, total_time;
    static unsigned long last_total_time = 0, last_idle_time = 0;
    
    bool found;
    char line[256];
    FILE * fp = fopen("/proc/stat", "r");
    if (!fp)
        die(true, "Could not open /proc/stat for reading!");
    while ( (found = fgets(line, sizeof line, fp)) )
    {
        sscanf(line, "%s %lu %lu %lu %lu", what, &user_time, &nice_time,
               &system_time, &idle_time);
        if (!strcmp(what, "cpu0"))
            break;
    }
    fclose(fp);
    
    if (!found)
        die(false, "Could not find entry for cpu0 in /proc/stat!?");

    // count nice time as idle time
    idle_time += nice_time;
    
    total_time = user_time + system_time + idle_time;
    total_elapsed = total_time - last_total_time;
    last_total_time = total_time;
    idle_elapsed = idle_time - last_idle_time;
    last_idle_time = idle_time;
    
    //errprintf("time: %lu    idle: %lu\n", total_elapsed, idle_elapsed);
}

// resets the elapsed total time and elapsed idle time counters
void reset_times()
{
    unsigned long dummy1, dummy2;
    get_times(dummy1, dummy2);
}

// are we currently dynamically scaling the CPU or at min or max?
enum Mode { DYNAMIC, MIN, MAX } mode;

// what strategy should be used if the laptop is ac on-line or off-line
Mode ac_on = DYNAMIC;
Mode ac_off = MIN;

// should the current mode be locked?
int lockmode = 0;

// if CPU idle/work ratio is below this, CPU will be set to fastest speed
float clock_up_idle_fast=0.10;
// if CPU idle/work ratio is below this, CPU will be set to next higher speed
float clock_up_idle=0.25;
// if CPU idle/work ratio is above this, CPU will be set to next lower speed
float clock_down_idle=0.75;

// if this is set (via command line) throttle down CPU to minimum if
// temperature is too high
const char * temperature_filename = 0;
unsigned max_temperature;

// if this is set (via command line) throttle down CPU to minimum if
// AC power is disconnected
const char * ac_filename = 0;

// handles the periodic check of idle and setting CPU speed
void
alarm_handler(int)
{
    static unsigned new_speed = 0;
    static unsigned current_speed = 0;
    static unsigned new_throttling;

    // current state
    Mode state = mode;
    
    char line[256], *p;

    if (lockmode==0) {
      // check to see if AC power is disconnected
      if (ac_filename)
	{
	  read_line(ac_filename, line, sizeof line);
	  if (strstr(line, "off-line"))
	    {
	      state = ac_off;
	      // reset_times();
	    }
	  else state=ac_on;
	}
      else state=ac_on;
      if (state==MIN) reset_times();
    }
    
    if (mode == DYNAMIC)
    {    
        // check that we are not getting too hot
        if (temperature_filename)
        {
            read_line(temperature_filename, line, sizeof line);
            p = strpbrk(line, "0123456789");
            if (!p)
                die(false, "Could not find temperature in file: %s",
                    temperature_filename);
            if (strtol(p, 0, 10) > (long)max_temperature)
            {
                state = MIN;
		lockmode = 1;
                reset_times();
				if (use_throttling)
					set_throttling(throttling_on);
            }
        }
    }

    new_throttling = current_throttling;
    
    // figure out what our current speed should be
    if (state == MAX)
	{
        new_throttling = throttling_off;
		new_speed = 0;
	}
	else
    {
		unsigned long elapsed_time, idle_time;
		double        idle_ratio;

		// get the elapsed and idle times since we last checked
		get_times(elapsed_time, idle_time);

		if (elapsed_time > 0)
		{
        	idle_ratio = (double)idle_time / (double)elapsed_time;
			if (state == MIN)
            	new_speed = last_step;
			else
			{
				// state is DYNAMIC
           		if (idle_ratio <= clock_up_idle_fast)
            		new_speed = 0;
            	else if (idle_ratio <= clock_up_idle && new_speed != 0)
            		new_speed--;
            	else if (idle_ratio >= clock_down_idle && speeds[new_speed + 1].khz != 0)
            		new_speed++;
			}
            errprintf("Idle ratio: %.2f   (%4dMHz, T%u)\n",
				idle_ratio, speeds[current_speed].khz, current_throttling);
			if (use_throttling)
			{
            	if (idle_ratio <= clock_up_idle && new_speed == 0)
            		new_throttling= throttling_off;
            	else if (idle_ratio >= clock_down_idle && new_speed == last_step)
            		new_throttling= throttling_on;
			}   
        }
    }

    // if the last set throttling is not what it currently should be, set it            
    if (current_throttling != new_throttling)
    {
        set_throttling(new_throttling);
    }
    // if the last set speed is not what it currently should be, set it            
    if (current_speed != new_speed)
    {
        set_speed(current_speed, new_speed);
        current_speed = new_speed;
    }
}

// handles the USR1 signal (stay at maximum performance)
void
usr1_handler(int)
{
    mode = MAX;
    lockmode = 1;
    errprintf("New mode: MAX\n");
	if (use_throttling)
		set_throttling(throttling_off);
}

// handles the USR2 signal (stay at minimum performance)
void
usr2_handler(int)
{
    mode = MIN;
    lockmode = 1;
    errprintf("New mode: MIN\n");
}

// handles the HUP signal (dynamically scale performance)
void
hup_handler(int)
{
    reset_times();
    mode = DYNAMIC;
    lockmode = 0;
    errprintf("New mode: DYNAMIC\n");
}

long
get_int(const char * s)
{
    char * end;
    long r = strtol(s, &end, 10);
    if (*end != '\0')
        die(false, "Not an integer: %s", s);
    if (errno == ERANGE)
        die(false, "Number is out of range: %s", s);
    
    return r;
}
    
float
get_float(const char * s)
{
    char * end;
    float r = (float)strtod(s, &end);
    if (*end != '\0')
        die(false, "Not a floating point number: %s", s);
    if (errno == ERANGE)
        die(false, "Number is out of range: %s", s);
    
    return r;
}
    
int
main(unsigned argc, char * argv[])
{
    unsigned interval = 20; // 2 seconds            
    bool daemonize = false;
    
    // parse argv
    for(unsigned i = 1; i < argc; i++)
    {
        if(!strcmp(argv[i], "-d"))
            daemonize = true;
        else if(!strcmp(argv[i], "-i"))
        {
            if (argc <= i + 1)
                die(false, "The -i option must be followed by an interval in tenths of a second\n");        
            
            interval = get_int(argv[++i]);
            errprintf("Interval is %u\n", interval);
            
        }
        else if(!strcmp(argv[i], "-p"))
        {
            if (argc <= i + 3)
                die(false, "The -p option must be followed by 3 floats\n");        

            clock_up_idle_fast = get_float(argv[++i]);
            clock_up_idle = get_float(argv[++i]);
            clock_down_idle = get_float(argv[++i]);
            errprintf("Triggers are %f %f %f\n",clock_up_idle_fast,clock_up_idle,clock_down_idle);
        }
        else if(!strcmp(argv[i], "-r"))
        {
            if (argc <= i + 2)
                die(false, "The -r option must be followed by 2 integers\n");        

            throttling_off = get_int(argv[++i]);
            throttling_on = get_int(argv[++i]);
            errprintf("Throttling states are %u %u\n",throttling_off, throttling_on);
			if (throttling_off == 0 && throttling_on == 0)
				use_throttling = false;
        }
        else if (!strcmp(argv[i], "-t"))
        {
            if (argc <= i + 2)
                die(false, "The -t option must be followed by a filename and a temperature\n");
        
            temperature_filename = argv[++i];
            max_temperature = get_int(argv[++i]);
        }
        else if (!strcmp(argv[i], "-a"))
        {
            if (argc <= i + 1)
                die(false, "The -a option must be followed by a filename\n");
        
            ac_filename = argv[++i];
        }
	else if (!strcmp(argv[i], "-ac-on"))
	  {
	    if (argc <= i + 1)
                die(false, "The -ac-on option must be followed by MAX, MIN or DYNAMIC\n");
	    if (!strcmp(argv[i+1],"MAX")) ac_on=MAX;
	    else if (!strcmp(argv[i+1],"MIN")) ac_on=MIN;
	    else if (!strcmp(argv[i+1],"DYNAMIC")) ac_on=DYNAMIC;
	    else die(false, "The -ac-on option must be followed by MAX, MIN or DYNAMIC\n");
	    i++;
	  }
	else if (!strcmp(argv[i], "-ac-off"))
	  {
	    if (argc <= i + 1)
	      die(false, "The -ac-off option must be followed by MAX, MIN or DYNAMIC\n");
	    if (!strcmp(argv[i+1],"MAX")) ac_off=MAX;
	    else if (!strcmp(argv[i+1],"MIN")) ac_off=MIN;
	    else if (!strcmp(argv[i+1],"DYNAMIC")) ac_off=DYNAMIC;
	    else die(false, "The -ac-off option must be followed by MAX, MIN or DYNAMIC\n");
	    i++;
	  }	
        else
	  {
            die(false, "Bad command line\n"
                       "Usage: %s [Options]\n"
                       "Usage: Options:\n"
                       "Usage:   -d                       - Daemonize process (run in background)\n"
                       "Usage:\n"
                       "Usage:   -i <interval>            - Change interval between idle ratio tests\n"
                       "Usage:                              and possible speed change in 1/10 second\n"
                       "Usage:                              increments (default is 20)\n"
                       "Usage:\n"
                       "Usage:   -p <fast up> <up> <down> - Set CPU idle:work ratios to speed up or\n"
                       "Usage:                              down CPU (default is 0.10 0.25 0.75)\n"
                       "Usage:\n"
                       "Usage:   -r <off> <on>             - Set CPU throttling states\n"
                       "Usage:                              (default is 0 10 and 0 0 disables)\n"
                       "Usage:\n"
                       "Usage:   -t <temp file> <maxtemp> - Set ACPI temperature file and temperature\n"
                       "Usage:                              at which CPU will be set to minimum speed\n" 
                       "Usage:\n"
                       "Usage:   -a <AC file>             - Set ACPI AC adapter state file and set CPU\n"
                       "Usage:                              to minimum speed when AC is disconnected\n"
		       "Usage:                              (can be overwritten by -ac-off)\n"
		       "Usage:\n"
		       "Usage:   -ac-on <DYNAMIC|MAX|MIN> - Overwrite strategy to use when AC is\n"
                       "Usage:                              connected (default: DYNAMIC)\n"
		       "Usage:\n"
	               "Usage:   -ac-off <DYNAMIC|MAX|MIN>- Overwrite strategy to use when AC is\n"
		       "Usage:                              disconnected (default: MIN)\n"
                , argv[0]);
        }
    }

    // test if we can use throttling
    if (use_throttling && access(THROTTLING_FILE, W_OK) < 0)
	{
        errprintf("cpu throttling not supported.\n");
        use_throttling = false;
	}
	else
		set_throttling(throttling_off);

    // use the userspace governor
    //write_line(GOVERNOR_FILE, "%s\n", USERSPACE);
    
    if (access(CURRENT_SPEED_FILE, W_OK) < 0)
        die(true, "Cannot write to speed control file: %s", CURRENT_SPEED_FILE);

    // run in background if requested    
    if (daemonize)
        daemon(0, 0);

    // set up signal handling    
    struct sigaction signal_action;
    
    // block all of our signals during each signal handler to avoid possible
    // race condition (not that it would really matter)
    sigemptyset(&signal_action.sa_mask);
    sigaddset(&signal_action.sa_mask, SIGALRM);
    sigaddset(&signal_action.sa_mask, SIGUSR1);
    sigaddset(&signal_action.sa_mask, SIGUSR2);
    sigaddset(&signal_action.sa_mask, SIGHUP);
    signal_action.sa_flags = 0;
    
    // set the SIGALRM handler to our function        
    signal_action.sa_handler = alarm_handler;
    sigaction(SIGALRM, &signal_action, 0);

    // set the SIGUSR1 handler to our function        
    signal_action.sa_handler = usr1_handler;
    sigaction(SIGUSR1, &signal_action, 0);

    // set the SIGUSR2 handler to our function        
    signal_action.sa_handler = usr2_handler;
    sigaction(SIGUSR2, &signal_action, 0);

    // set the HUP handler to our function        
    signal_action.sa_handler = hup_handler;
    sigaction(SIGHUP, &signal_action, 0);

    // reset the speed steps
    get_supported_speeds();
    // reset the time counters
    reset_times();

    // we dynamically scale speed to start
    mode = DYNAMIC;
    
    // main loop
    while (1)
    {
        raise(SIGALRM);
        usleep(interval * 100000L);
    }
             
    // never reached
    return 0;
}


