Как стать автором
Обновить

Своя реализация монитора загрузки CPU и RAM из найденного в шухляде

Время на прочтение 5 мин
Количество просмотров 20K
Одним из самых любимых хабов на хабре всегда был для меня DIY, я и сам не прочь что-то сделать своими руками. Но так, как я в большей степени программист, а в меньшей — электронщик, сделанные мной «прототипы» всегда непрезентабельны. Данный девайс не исключение. Код тоже не причесан, т.к. это больше proof-of-concept чем коммерческое решение. Тем не менее, думаю данных пост будет полезен, и даже найдутся те, кто повторит эту поделку.
Вдохновившись постом о стрелочных Vu-метрах, показывающих загрузку CPU и использование RAM, решил сделать свой вариант. Не имея миниатюрных вольтметров, но имея покоривший своей ценой и простотой протокола 16х2 экран на базе контроллера hd44780 решил организовать визуализацию на нем. Как МК был выбран ланчпад MSP430G2, коих я купил жменьку, когда они были по $4.30. Ничего не мешает реализовать это все на любой arduino, нужно лишь поменять названия пинов.
Схема очень проста(взята из интернета):
image

Можно еще подключить подсветку и регулировку контраста, но думаю не составит труда разобраться с этим даже начинающим электронщикам(сужу по себе).
Программная часть состоит из двух частей, первая — скетч для Energia/Arduino. Все очень просто и понятно.
Скетч
#include <LiquidCrystal.h>
byte cpuByte;
byte ramByte;
unsigned long lastUpdateTime;
unsigned long disconntime;
int difer = 0;

LiquidCrystal lcd(P2_3, P2_4, P1_5, P2_0, P2_1, P2_2);

void setup()
{
  Serial.begin(9600);
  lcd.begin(16, 2);
  lcd.clear();
  lcd.print("FPanel V1.2");
}

void loop()
{
    if (Serial.available() == 2) {
      cpuByte = Serial.read();
      ramByte = Serial.read();
      lcd.setCursor(0, 0);
      lcd.print("CPU ");
      lcd.setCursor(4, 0);
      for (int i=0; i < cpuByte; i++) { lcd.write(255);}
      for (int i=cpuByte; i < 16; i++) { lcd.write(32);}

      lcd.setCursor(0, 1);
      lcd.print("RAM ");
      lcd.setCursor(4, 1);
      for (int i=0; i < ramByte; i++) { lcd.write(255);}
      for (int i=ramByte; i < 16; i++) { lcd.write(32);}
      
      lastUpdateTime = millis();
    } 
    if (millis()-lastUpdateTime > 3000) {
      lcd.setCursor(0, 0);
      lcd.print("   CONNECTING   ");
      lcd.setCursor(0, 1);
      difer = (millis()-disconntime-3000) / 250;
      if (difer > 16) {disconntime = millis()-3000;}
      for (int i=0; i < difer; i++) {lcd.write(255);}
      for (int i=difer; i < 16; i++) {lcd.write(32);}
      delay(10);
    }
    delay(50);
}


Логика проста, ждем два байта и выводим их значения в виде строки из символов с кодом 255. Прошлые значения затираю пробелами, ибо с lcd.clear() все мигает. Если более 3х секунд данные не приходят — DISCONNECTED.

Программирую я в основном под платформы 1Cх, а из компилируемых ЯП близок мне Delphi/Pascal больше всего. На Delphi версии XE2 и решил я писать вторую часть проекта. Так как планируется работа данного девайса на серверных ОС семейства Windows, ПО реализовано как служба.
Исходник на Delphi
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.SvcMgr, Vcl.Dialogs, Registry;

type
  TFPService = class(TService)
    procedure ServiceExecute(Sender: TService);
    procedure ServiceCreate(Sender: TObject);
  private
    { Private declarations }
  public
    function GetServiceController: TServiceController; override;
    { Public declarations }
  end;

var
  FPService: TFPService;

const
  SystemBasicInformation = 0;
  SystemPerformanceInformation = 2;
  SystemTimeInformation = 3;

type
  TPDWord = ^DWORD;

  TSystem_Basic_Information = packed record
    dwUnknown1: DWORD;
    uKeMaximumIncrement: ULONG;
    uPageSize: ULONG;
    uMmNumberOfPhysicalPages: ULONG;
    uMmLowestPhysicalPage: ULONG;
    uMmHighestPhysicalPage: ULONG;
    uAllocationGranularity: ULONG;
    pLowestUserAddress: Pointer;
    pMmHighestUserAddress: Pointer;
    uKeActiveProcessors: ULONG;
    bKeNumberProcessors: byte;
    bUnknown2: byte;
    wUnknown3: word;
  end;
type
  TSystem_Time_Information = packed record
    liKeBootTime: LARGE_INTEGER;
    liKeSystemTime: LARGE_INTEGER;
    liExpTimeZoneBias: LARGE_INTEGER;
    uCurrentTimeZoneId: ULONG;
    dwReserved: DWORD;
  end;
type
  TSystem_Performance_Information = packed record
    liIdleTime: LARGE_INTEGER; {LARGE_INTEGER}
    dwSpare: array[0..750] of DWORD;
  end;

var
  NtQuerySystemInformation: function(infoClass: DWORD; buffer: Pointer; bufSize: DWORD; returnSize: TPDword): DWORD; stdcall = nil;

  SysBaseInfo: TSystem_Basic_Information;
  SysPerfInfo: TSystem_Performance_Information;
  SysTimeInfo: TSystem_Time_Information;

  status: Longint; {long}
  liOldIdleTime, liOldSystemTime: LARGE_INTEGER;
  dbSystemTime, dbIdleTime, dbIdleTimePercent: Double;
  hCom: THandle;

  DCB:TDCB;
  Errors, Bytes : Cardinal;
  TheStruct:TCOMSTAT;
  Timeouts: TCommTimeOuts;

  ComNum:string;

implementation

{$R *.DFM}

procedure ServiceController(CtrlCode: DWord); stdcall;
begin
  FPService.Controller(CtrlCode);
end;

function TFPService.GetServiceController: TServiceController;
begin
  Result := ServiceController;
end;

Procedure getComNum;
var Reg: TRegistry;
begin
  Reg := TRegistry.Create(KEY_READ or KEY_WOW64_64KEY);
  try
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    if Reg.OpenKey('\SOFTWARE\FPanel', false) then
    begin
      ComNum := Reg.ReadString('ComNum');
      Reg.CloseKey;
    end;
  finally
    Reg.Free;
  end;
end;

Procedure InitCPUUsage;
begin
  if @NtQuerySystemInformation = nil then NtQuerySystemInformation := GetProcAddress(GetModuleHandle('ntdll.dll'), 'NtQuerySystemInformation');
  status := NtQuerySystemInformation(SystemBasicInformation, @SysBaseInfo, SizeOf(SysBaseInfo), nil);
  if status <> 0 then exit;
end;

function CPUUsed: integer;

  function Li2Double(x: LARGE_INTEGER): Double;
  begin
    Result := x.HighPart * 4.294967296E9 + x.LowPart
  end;

begin
  result := 0;
  status := NtQuerySystemInformation(SystemTimeInformation, @SysTimeInfo, SizeOf(SysTimeInfo), nil);
  if status <> 0 then Exit;
  status := NtQuerySystemInformation(SystemPerformanceInformation, @SysPerfInfo, SizeOf(SysPerfInfo), nil);
  if status <> 0 then Exit;

  dbIdleTime := Li2Double(SysPerfInfo.liIdleTime) - Li2Double(liOldIdleTime);
  dbSystemTime := Li2Double(SysTimeInfo.liKeSystemTime) - Li2Double(liOldSystemTime);
  dbIdleTimePercent := dbIdleTime / dbSystemTime * 100;

  liOldIdleTime := SysPerfInfo.liIdleTime;
  liOldSystemTime := SysTimeInfo.liKeSystemTime;

  if (dbIdleTimePercent / SysBaseInfo.bKeNumberProcessors) < 0 then result := 0 else
  result := round(abs(100-(dbIdleTimePercent / SysBaseInfo.bKeNumberProcessors)));
 end;

function RAMUsed: byte;
var RamStats: TMemoryStatus;
begin
  GlobalMemoryStatus(RamStats);
  result := ramStats.dwMemoryLoad;
end;

procedure con2com;
var ComFN:string;
begin
  CloseHandle(hCom);
  ComFN := '\\.\COM' + comNum;
  hCom := CreateFile(PWidechar(ComFN), GENERIC_WRITE or GENERIC_READ, 0, nil, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL, 0); 
  if hCom = INVALID_HANDLE_VALUE then  Exit;

  SetupComm(hCom,100,100);

  GetCommState(hCom,DCB);
  with DCB do begin
    BaudRate:=9600;
    ByteSize:=8;
    Parity:=NoParity;
    StopBits:=OneStopBit;
  end;
  SetCommState(hCom,DCB);

  GetCommTimeouts(hCom,Timeouts);
  with TimeOuts do begin
    ReadIntervalTimeout := 1;
    ReadTotalTimeoutMultiplier := 0;
    ReadTotalTimeoutConstant := 1;
    WriteTotalTimeoutMultiplier := 2;
    WriteTotalTimeoutConstant := 2;
  end;
  SetCommTimeouts(hCom,Timeouts);
end;

procedure TFPService.ServiceExecute(Sender: TService);
var NumberWritten:LongWord;
    twoBytes:array[0..1] of byte;

begin
while not Terminated do
  begin
    twoBytes[0] := round(CPUUsed / 8.33);
    twoBytes[1] := round(RAMUsed / 8.33);
    if WriteFile(hCom, twoBytes, 2, NumberWritten, nil) = False then con2com;
    Sleep(250);
    ServiceThread.ProcessRequests(False);
  end;
end;

procedure TFPService.ServiceCreate(Sender: TObject);
begin
 initCPUUsage;
 getComNum;
 con2com;
end;

end.


Для установки службы файл FP_service.ехе нужно запустить с параметром /install с правами администратора. После этого нужно создать строковый ключ ComNum в реестре, со значением номера COM-порта, на котором «висит» наш ланчпад по пути HKEY_LOCAL_MACHINE\SOFTWARE\FPanel, в моем случае значение ComNum = «12». После этого достаточно запустить службу FPService через оснастку «Службы». В следующий раз она будет стартовать сама. Служба переподключается к ланчпаду автоматически, никак не мешает другим программам.
Видео работы:

Архив Delphi проекта+готовый.ехе
Спасибо за внимание.
UPD: За 5 дней тестов проблем не обнаружено.
UPD: Обновил скетч, улучшил алгоритм рендеринга плюс добавил анимацию ожидания подключения.
UPD: Обновил ссылку на архив проекта.
Теги:
Хабы:
+6
Комментарии 14
Комментарии Комментарии 14

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн