요즘에는 라즈베리파이니 오렌지파이니 해서 소형 리눅스 디바이스가 활약하는 일이 잦아졌다.
sudo systemctl daemon-reload
sudo systemctl enable orange-net
sudo systemctl start orange-net
it블로그
요즘에는 라즈베리파이니 오렌지파이니 해서 소형 리눅스 디바이스가 활약하는 일이 잦아졌다.
sudo systemctl daemon-reload
sudo systemctl enable orange-net
sudo systemctl start orange-net
파워쉘이 강력하다는것은 전부터 익히 들어 알고 있었다.
Azure 원격관리도 할수 있고 자동화 스크립트 이런걸로 말이다..
그리고 파워쉘을 사용하기 위해 다음과 같은 배치파일 (myapp.ps1 파일을 실행)
PowerShell.exe -executionpolicy bypass -file "%~dp0myapp.ps1"
pause
를 보통 사용할것이다. 명령프롬프트가 없으려면 다음처럼
PowerShell.exe -windowstyle Hidden -executionpolicy bypass -file "%~dp0myapp.ps1"
그런데 얼마전 일을하다가 파워쉘의 다른 모습을 봤는데..
이 파워쉘이라는것이 닷넷프레임워크 4.x 기반위에서 동작하고
C# 문법과 비슷하게 프로그래밍이 가능하다는 기능이 있다는걸 알게됬다.
물론 완전한 인터렉션용 프로그램은 아닐지라도 자동화처리를 함에 있어
스트립트 라인바이라인 형식의 프로그래밍을 사용할수 있다는걸 알았다.
그래서 그 위상이 한층 높아졌는데..
닷넷프레임워크를 사용할수 있으니 윈폼이라던가 Math, Socket을 사용할수 있다.
결과적으로
복잡한로직은 c# 라이브러리로 만들고
이걸 파워쉘에서 불러서 호출한다던가
Bitmap을 이용한 간단한 이미지를 생성한다던가
예를들어 직원 명단이 있는 csv파일을 불러다가
Bitmap을 루프돌면서 명찰 PNG파일을 만들어서
각 이름 폴더에 저장한다던가 dll파일이 있는데
호출규약이나 구조만 알아낸뒤 이부분만 떼어서 별도로 호출해서
결과를 바로 본다던가..
이런행위들은 이제 별도의 프로그램을 만들지말고 파워쉘로 하면된다.
물론 파이썬도 이런거 할수 있긴하지만 명령창까지 품은
파워쉘이 훨씬 강력하다고 느낀다.
게다가 AI가 판지는 요즈에는 커맨드릿 이라던가 이런거 외우지 않아도
바로바로 질문해서 답을 얻을수 있다.
바이브 AI코딩 + 파워쉘... 장난없다..
게다가 파워쉘로 snaketail을 다음명령으로 대체가능하다.
Get-Content "텍스트파일경로" -Wait -Tail 20
ㅇㅇ
using System;using System.Runtime.InteropServices;namespace GpioTest;/// <summary>/// 네이티브 바인딩/// </summary>public static class NativeBindings{ //라이브러리 파일명 private const string LibGpiod = "libgpiod.so"; //gpiod.h 의 풀업플레그값 public const int GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP = 1 << 8; // so 파일 메서드 시그니처 [DllImport(LibGpiod, EntryPoint = "gpiod_chip_open_by_name")] public static extern IntPtr GpiodChipOpenByName(string name); [DllImport(LibGpiod, EntryPoint = "gpiod_chip_close")] public static extern void GpiodChipClose(IntPtr chip); [DllImport(LibGpiod, EntryPoint = "gpiod_chip_get_line")] public static extern IntPtr GpiodChipGetLine(IntPtr chip, uint offset); [DllImport(LibGpiod, EntryPoint = "gpiod_line_request_input_flags")] public static extern int GpiodLineRequestInputFlags(IntPtr line, string consumer, int flags); [DllImport(LibGpiod, EntryPoint = "gpiod_line_request_input")] public static extern int GpiodLineRequestInput(IntPtr line, string consumer); [DllImport(LibGpiod, EntryPoint = "gpiod_line_get_value")] public static extern int GpiodLineGetValue(IntPtr line); [DllImport(LibGpiod, EntryPoint = "gpiod_line_release")] public static extern void GpiodLineRelease(IntPtr line);}using System;using System.Collections.Generic;using System.Linq;using System.Threading;namespace GpioTest;/// <summary>/// 버튼정보 구조체/// </summary>public class GpioButtonConfig{ public IntPtr LinePtr = IntPtr.Zero; public uint Lineoffset { get; set; } = 0; public string Description { get; set; } = ""; public int LastValue { get; set; } = 1; // 1 (HIGH): 떼어짐 (풀업 가정) public Action<uint>? d_Actor = null;}/// <summary>/// Gpio 모니터/// </summary>public class GpioMonitor: IDisposable{ private IntPtr _chipPtr = IntPtr.Zero; private string ChipName = ""; private List<GpioButtonConfig> _buttonConfigs = new List<GpioButtonConfig>(); // 전체 버튼의 디바운스 타임 private const int DebounceDelayMs = 250; private DateTime _lastPushDebounceTime = DateTime.MinValue; private DateTime _lastPullDebounceTime = DateTime.MinValue; /// <summary> /// 초기화처리 /// </summary> public bool Initialize(List<GpioButtonConfig> a_configs, string a_chipName = "gpiochip1") { // _buttonConfigs.AddRange(a_configs); ChipName = a_chipName; // try { // 1. 칩열기 _chipPtr = NativeBindings.GpiodChipOpenByName(ChipName); if (_chipPtr == IntPtr.Zero) { throw new Exception($"칩 {ChipName} 열기 실패. 권한 문제확인."); } // 2. 등록된 모든 라인 초기화 및 요청 for (int i = 0; i < _buttonConfigs.Count; i++) { GpioButtonConfig one_config = _buttonConfigs[i]; IntPtr linePtr = NativeBindings.GpiodChipGetLine(_chipPtr, one_config.Lineoffset); if (linePtr == IntPtr.Zero) { Console.WriteLine($"경고: 라인 {one_config.Lineoffset} 가져오기 실패. 건너뜀"); continue; } // 3. 라인 요청 (Input + Pull Up) int flags = NativeBindings.GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP; int ret = NativeBindings.GpiodLineRequestInputFlags(linePtr, one_config.Description, flags); if (ret < 0) { Console.WriteLine($"경고: 라인 {one_config.Lineoffset} 풀업요청 실패."); } //라인포인터 추가 및 액션 바인딩 one_config.LinePtr = linePtr; one_config.LastValue = NativeBindings.GpiodLineGetValue(linePtr); one_config.d_Actor += (lineno) => System.Console.WriteLine($"눌린 라인번호{lineno}번."); Console.WriteLine($"라인 {one_config.Lineoffset} ({one_config.Description}) 설정 완료."); } // if (!_buttonConfigs.Any()) { throw new Exception("모니터링할 유효한 GPIO라인이 없습니다."); } // return true; } catch (Exception ex) { Console.WriteLine($"\n 초기화 실패: {ex.Message}"); Dispose(); return false; } } /// <summary> /// 모니터링 시작 /// </summary> public void StartMonitoring() { //루핑 while (true) { foreach (var item in _buttonConfigs.Where(c => c.LinePtr != IntPtr.Zero)) { int currentValue = NativeBindings.GpiodLineGetValue(item.LinePtr); // if (currentValue == 0 && item.LastValue == 1) { //버튼 눌림 감지 DateTime nowtime = DateTime.Now; if ((nowtime - _lastPushDebounceTime).TotalMilliseconds > DebounceDelayMs) { Console.WriteLine($"버튼 눌림 감지: {item.Description}"); item.d_Actor?.Invoke(item.Lineoffset); _lastPushDebounceTime = nowtime; } } else if (currentValue == 1 && item.LastValue == 0) { //버튼 떼짐 감지 DateTime nowtime = DateTime.Now; if ((nowtime - _lastPullDebounceTime).TotalMilliseconds > DebounceDelayMs) { Console.WriteLine($"버튼 떼임 감지: {item.Description}"); _lastPullDebounceTime = nowtime; } } // 상태 업데이트 item.LastValue = currentValue; } //한바퀴 다돌고 5ms Sleep Thread.Sleep(5); } } /// <summary> /// 자원 해지 /// </summary> public void Dispose() { foreach (var config in _buttonConfigs) { NativeBindings.GpiodLineRelease(config.LinePtr); } if (_chipPtr != IntPtr.Zero) { NativeBindings.GpiodChipClose(_chipPtr); Console.WriteLine("GPIO 자원 해제 완료."); } }}using System;using System.Collections.Generic;namespace GpioTest;/// <summary>/// 메인 클래스/// </summary>public class MainApp{ /// <summary> /// 주진입점 /// doent빌드 명령어: dotnet publish -c Release -r linux-arm64 /// </summary> public static void Main(string[] args) { //모니터링할 버튼 리스트 정의 //(PC6=70) var buttonConfigurations = new List<GpioButtonConfig> { new GpioButtonConfig { Lineoffset = 70, Description = "Button PC6" }, }; // GpioMonitor 인스턴스를 using 구문으로 생성하여 자동 해제되도록 처리 using var monitor = new GpioMonitor(); bool bsetup = monitor.Initialize(buttonConfigurations); if (!bsetup) { System.Console.WriteLine("GPIO버튼 초기화 실패!!"); Environment.Exit(0); } //콘솔 종료키 감지 Console.CancelKeyPress += (sender, e) => { System.Console.WriteLine(Environment.NewLine + "종료요청 감지.."); e.Cancel = true; Environment.Exit(0); }; //모니터링 시작 try { monitor.StartMonitoring(); } catch (Exception ex) { Console.WriteLine($"런타임 오류: {ex.Message}"); } }}콘솔프로그램이 C#언어를 배우는 초기 단계에서는 Console.WriteLine() 등으로
어떤 출력내용을 확인하고 나서 그 다음 단원으로 가고 이런걸로 많이 사용한다.
개발자로써 어느정도 경력이 쌓이다 보면 관심사의 분리 같은걸 알게 되면서
어느순간 콘솔프로그램은 테스트유닛으로 활용되거나 백그라운드 테스크용도로
사용된다.
asp.net도 엄밀히 따지면 콘솔프로그램이라고 할수 있을정도로 무시할수 없는
레벨로 콘솔프로그램의 위상이 올라선다.
그리고 이를 활용하는 눈에 보이지 않는 어플을 만들기 시작하는데
비단 웹뿐만이 아니라 백그라운드 서비스.. (리눅스에서는 데몬)
으로 활용된다. 게다가 아두이노 같은 싱글보드에서 돌아가는 한개의 프로그램을
만드는데에도 적극적으로 활용된다. 물론 아두이노에서 C#은 안돌아가지만..
그런 비슷한 느낌으로 만드는 모듈이 바로 라즈베리파이가 아니라 중국에서 만드는 값싼
오렌지파이 제로나 오렌지파이 3B같은 부류이다.
물론 CPU가격이 메모리 대란같은것으로 들쭉날쭉하게 되면 어쩔수 없고 또한
비싼 비메모리 예를 들어 인텔이나 브로드컴 같은 CPU들은 아두이노랑 비빌수
없다. 하지만 오렌지파이 CPU같은것들은 RockChip같은 브렌드의 부류로
구성되고 알리익스프레스같은 사이트를 이용해서 중국에서 구매하는경우
실제로 한국에서 아두이노를 돈주고 구매하는것과 맞먹는 가격까지
내려간다. 이를 이용해서 C#으로 콘솔어플을 돌리면 GPIO나 I2C같은걸 이용해서
마치 아두이노처럼 프로그램을 할수 있다. C#으로.. 그리고 그렇게 돌아가기 위해
오렌지 파이에는 암비안이나 우분투서버 같은것을 OS로 포팅하고 dotnet 런타임을
설치하고 나서 시스템이 시작할때 데몬으로 등록처리하면 실제로 동작한다.
세상 참 많이 발전했다.. 그리고 이러한 콘솔프로그램을 돌릴수 있는
그 중심에는 이 앱이 하나의 정식적인 앱으로 활용할수 있게되는
MS의 핵심 확장모듈이 있다. 그 모듈은
dotnet add package Microsoft.Extensions.Hosting;
dotnet add package Microsoft.Extensions.Logging;
dotnet add package Microsoft.Extensions.DependencyInjection;
이라 할수 있다..
이번 포스팅에서는 이 Hosting을 이용하여 지난번 로거와 HelloWorld를
출력하는 서비스 그리고 1초마다 한번씩 현재시간을 로거에 출력하는 서비스와
랜덤한 숫자를 출력하는 프로그램을 만든다.. 물론 이를 활용하여 서비스앱을
만드는데 기초재료로 사용될것을 기대한다.
[주요순서]
1. 콘솔프로그램을 만들고 위의 3개 package를 설치한다.
2. 메인 진입점 작성하고 4개(로거, 단순출력, 랜덤숫자출력, 시간출력)의 서비스를 포함한다.
3. 각서비스를 구현한다.
4. 빌드후 실행
[진입점]
/// <summary>/// 메인 프로그램/// </summary>public class MainApp{ /// <summary> /// 진입점 /// </summary> public async static Task Main() { //초기로드 Console.WriteLine("Application StartUp!!"); LoadSetup(); //서비스 로딩 IHost host = Host.CreateDefaultBuilder() .ConfigureServices(services => { //로거 바인딩 services.AddSingleton<ILoggerProvider>(sp => { return new FileLoggerProvider(); }); //서비스 호스트 바인딩 services.AddHostedService<NormalPrint>(); services.AddHostedService<TimePrinter>(); services.AddHostedService<RandomPrint>(); }) .Build(); // 호스트 실행 await host.RunAsync(); } /// <summary> /// 앱로딩 설정 /// </summary> public static void LoadSetup() { EUCKR.Init(); }}[로거 부분 생략 (지난번 포스팅 참조)]
...
[NormalPrint.cs]
/// <summary>/// 일반 메시지 출력용/// </summary>public class NormalPrint : IHostedService{ /// <summary> /// 생성자와 DI /// </summary> private readonly ILogger<NormalPrint> _logger; public NormalPrint(ILogger<NormalPrint> logger) { _logger = logger; } /// <summary> /// 서비스 시작 /// </summary> public Task StartAsync(CancellationToken cancellationToken) { _logger.LogWarning("NormalPrint 서비스 시작!!"); // 한번만 로그 출력하고 끝냄 _logger.LogWarning("아이엠 Just 프린트 노멀 유노암쎙??"); // return Task.CompletedTask; } /// <summary> /// 서비스 종료 /// </summary> public Task StopAsync(CancellationToken cancellationToken) { _logger.LogWarning("NormalPrint 서비스 종료!!"); return Task.CompletedTask; }}[RandomPrint.cs]
/// <summary>/// 랜덤한 숫자 출력/// </summary>public class RandomPrint: IHostedService{ /// <summary> /// 생성자와 내부변수 /// </summary> private readonly ILogger<RandomPrint> _logger; private Task _time_count_Task; private readonly CancellationTokenSource _stoppingToken = new CancellationTokenSource(); private Random rnd; public RandomPrint(ILogger<RandomPrint> logger) { _logger = logger; rnd = new Random(); } /// <summary> /// 서비스 시작 /// </summary> public Task StartAsync(CancellationToken cancellationToken) { _logger.LogWarning("RandomPrint 서비스 시작!!"); // 백그라운드 서비스 시작 _time_count_Task = Task.Run(async () => { await DoWorkAsync(_stoppingToken.Token); }); // return Task.CompletedTask; } /// <summary> /// 서비스 종료 /// </summary> public Task StopAsync(CancellationToken cancellationToken) { _logger.LogWarning("RandomPrint 서비스 중지!!"); // try { //작업중지 신호 _stoppingToken.Cancel(); //백그라운드 Task가 완료되기를 기다림 return Task.WhenAny(_time_count_Task, Task.Delay(Timeout.Infinite, cancellationToken)); } catch (Exception ex) { _logger.LogError(ex.Message); return Task.CompletedTask; } } /// <summary> /// 작업로직 /// </summary> private async Task DoWorkAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { // int rnd_count = rnd.Next(100); _logger.LogWarning($"0 ~ 99 사이의 랜덤숫자 -> { rnd_count.ToString() }"); //0.5초쉼.. await Task.Delay(500); } catch (Exception ex) { _logger.LogError(ex.Message); } } }}[TimePrinter.cs]
/// <summary>/// 시간출력용 클래스/// </summary>public class TimePrinter : IHostedService{ /// <summary> /// 생성자와 내부변수 /// </summary> private readonly ILogger<TimePrinter> _logger; private Task _time_count_Task; private readonly CancellationTokenSource _stoppingToken = new CancellationTokenSource(); public TimePrinter(ILogger<TimePrinter> logger) { _logger = logger; } /// <summary> /// 서비스 시작 /// </summary> public Task StartAsync(CancellationToken cancellationToken) { _logger.LogWarning("TimerPrinter 서비스 시작!!"); // 백그라운드 서비스 시작 _time_count_Task = Task.Run(async () => { await DoWorkAsync(_stoppingToken.Token); }); // return Task.CompletedTask; } /// <summary> /// 서비스 종료 /// </summary> public Task StopAsync(CancellationToken cancellationToken) { _logger.LogWarning("TimerPrinter 서비스 중지!!"); // try { //작업중지 신호 _stoppingToken.Cancel(); //백그라운드 Task가 완료되기를 기다림 return Task.WhenAny(_time_count_Task, Task.Delay(Timeout.Infinite, cancellationToken)); } catch (Exception ex) { _logger.LogError(ex.Message); return Task.CompletedTask; } } /// <summary> /// 작업로직 /// </summary> private async Task DoWorkAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { // DateTime nowtime = DateTime.Now; _logger.LogWarning("TimePrinter의 현재시간 기록: " + nowtime.ToString("yyyy-MM-dd HH:mm:ss")); //1초쉼.. await Task.Delay(1000); } catch (Exception ex) { _logger.LogError(ex.Message); } } } }이렇게 완성하고 나서 dotnet run으로 돌려보면 로그를 잘찍는다..
배포를 하려면
[윈도우 배포 - 64비트]
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true
[리눅스 배포 - 64비트]
dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishSingleFile=true
요로코롬 하면된다.. 물론 윈도우나 리눅스에서 시작프로그램에 등록해야 하지만..
ㅇㅇ
C#에서 event나 delegate를 구독할때 처리하는 방법은
총 3가지 이다.
mybutton이 있었을때 이버튼의 click이벤트를 바인딩하려 한다.
1. 람다식 (인라인으로 처리)
mybutton.click += (_, _) => OtherMethod()
이는 람다식으로 표현하는 방법인데 원래 파라미터 object, eventargs의
내용을 무시하고 내부처리를 이어하기 위함 (컴파일러가 추론할수 있음)
2. 델리게이트처리 (인라인)
mybutton.click += delegate (object sender, EventArgs e) { ... }
델리게이트를 선언하면서 호출형태를 명시하고 { ... } 안에 내용을 기술
파라미터의 형태를 컴파일러가 추론할수 없음으로 데이터형을 기술해야함
3. 별도 함수 지정 (인라인 아님)
public void MyClick(object sender, EventArgs e) { ... }
---
mybutton.click += MyClick;
---
별도의 함수에서 처리하는 방법임
※ 파라미터를 처리할때 EventArgs 가 아닌 다른 Args를 원하는 경우 해당 클래스로
바인딩 해줘야함 대리자의 시그니처가 정확한지 판단하기 때문임. ※