From 902af18555ff380ef1aad7e3ec9e66553d0fa83c Mon Sep 17 00:00:00 2001 From: Godopu Date: Fri, 17 Jul 2026 15:51:10 +0900 Subject: [PATCH] refactor: replace GetRandom with IoT sensing data update scenario --- .gitignore | 3 + README.md | 5 +- docs/GRPC.md | 50 ++--- examples/grpcentity/client.go | 34 ++-- examples/grpcentity/server.go | 30 ++- project_analysis_report.md | 151 +++++++++++++++ protoapi.proto | 19 +- protoapi/protoapi.pb.go | 333 +++++++++++++--------------------- protoapi/protoapi_grpc.pb.go | 122 ++++++------- scripts/generate-env.sh | 45 +++++ 10 files changed, 446 insertions(+), 346 deletions(-) create mode 100644 project_analysis_report.md create mode 100755 scripts/generate-env.sh diff --git a/.gitignore b/.gitignore index 8646416..74277e6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ update.sh # Workspace-specific scripts /resume_all.sh + +# Local downloaded binary tools +/bin/ diff --git a/README.md b/README.md index f406a54..3f423e2 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,9 @@ Serving requests... Client: Server Date and Time: 2026-07-12 20:00:00.123456789 +0900 KST m=+1.000000001 Random Password: &c(D7f/G#s%d -Random Integer 1: 42 -Random Integer 2: 87 +Received sensing data - Device: sensor-room-01, Temp: 24.50°C, Humid: 52.30% +Sensing Update Success: true +Sensing Update Message: Sensing data updated successfully for device sensor-room-01 ``` ### 4. 트러블슈팅 diff --git a/docs/GRPC.md b/docs/GRPC.md index 543556b..b7fc00c 100644 --- a/docs/GRPC.md +++ b/docs/GRPC.md @@ -28,19 +28,19 @@ gRPC는 HTTP/2를 기반으로 구축된 구글의 고성능 오픈소스 원격 --- -## 2. 실습 프로젝트 소개: Random 데이터 서비스 +## 2. 실습 프로젝트 소개: IoT 센싱 데이터 수집 서비스 -본 튜토리얼에서는 gRPC 분산 통신 기법을 실증적으로 학습하기 위해 가상의 **Random 데이터 API 서비스** 프로젝트를 직접 설계하고 구현해 나갑니다. +본 튜토리얼에서는 gRPC 분산 통신 기법을 실증적으로 학습하기 위해 현업에서 가장 범용적으로 쓰이는 가상의 **IoT 센싱 데이터 수집 및 기기 관리 서비스** 프로젝트를 직접 설계하고 구현해 나갑니다. ### 2.1 프로젝트 시나리오 -사물인터넷(IoT) 센서 노드나 지능형 멀티 에이전트 분산 환경에서는 기기들이 중앙 서버에 접속해 상태 정보(날짜/시간)를 동기화하거나, 보안 패킷 전송을 위해 임시 원격 패스워드를 발급받고, 연산용 고유 난수를 안전하게 질의해야 하는 현실적인 통신 요건이 존재합니다. -우리가 개발할 `Random` 서비스는 이에 대응하는 다음 3가지 원격 프로시저(RPC)를 구현합니다: -1. **서버 시간 및 날짜 조회 (`GetDate`)**: 클라이언트가 요청 시 서버는 내부 시스템의 포맷팅된 시간 문자열을 가공하여 반환합니다. -2. **일회성 보안 패스워드 발급 (`GetRandomPass`)**: 클라이언트가 난수 시드와 바이트 길이를 명시하여 요청하면, 서버는 안전한 ASCII 비밀번호 문자열을 조립해 응답합니다. -3. **의사 난수 정수 생성 (`GetRandom`)**: 재현성을 위해 시드값과 시퀀스 위치를 인자로 전달받아 계산에 부합하는 정수 난수값을 연산해 반환합니다. +사물인터넷(IoT) 센서 노드나 분산 멀티 에이전트 환경에서는 엣지 기기들이 중앙 서버에 접속해 통신 가능 여부를 검증하고 상태 정보(날짜/시간)를 수집하거나, 임시 보안 인증을 위한 비밀번호 발급을 요청하고, 실시간으로 센싱한 환경 정보(온도, 습도 등)를 지속적으로 업데이트해야 하는 현실적인 시나리오가 요구됩니다. +우리가 개발할 `IoTService`는 이에 대응하는 다음 3가지 핵심 원격 프로시저(RPC)를 수행합니다: +1. **서버 시간 및 날짜 조회 (`GetDate`)**: 기기가 접속 상태를 확인하며 동기화를 위해 서버의 현재 날짜와 시간 포맷 문자열을 반환받습니다. +2. **센싱 데이터 업데이트 (`UpdateSensingData`)**: 센서 노드가 주기적으로 수집한 물리 데이터(온도, 습도) 및 기기 식별자(Device ID)를 전달하면, 서버는 데이터 정합성을 검증한 후 성공 여부를 반환합니다. +3. **일회성 보안 패스워드 발급 (`GetRandomPass`)**: 기기가 임시 통신 세션 수립을 위해 난수 생성 시드와 보안 문자열 길이를 전달하면, 무작위 ASCII 임시 패스워드를 연산하여 응답받습니다. ### 2.2 학습 목표 및 진행 방법 -이 간단하면서도 긴밀한 데이터 파이프라인을 구축하는 실습을 통해 학습자는 다음 gRPC 지식 체계를 단계별로 마스터하게 됩니다: +이 유기적인 IoT 데이터 통신 모듈을 구축하는 실습을 통해 학습자는 다음 gRPC 지식 체계를 단계별로 마스터하게 됩니다: * **스키마 설계**: `.proto` IDL 문법을 활용해 데이터 형식(Message)과 원격 함수(Service RPC) 계약을 강제하는 법을 습득합니다. * **Stub 컴파일**: `protoc` 도구 체인을 가동하여 Go 프로그래밍 언어 소스코드를 안전하게 생성하고 프로젝트 빌드에 바인딩하는 기법을 배웁니다. * **네트워크 구현**: 실제로 TCP 소켓을 확보하여 gRPC 서버를 실행하고, 클라이언트가 평문 커넥션을 수립하여 실시간 동기식 원격 프로시저를 직접 기동하는 엔드투엔드 구동 구조를 체득합니다. @@ -49,26 +49,28 @@ gRPC는 HTTP/2를 기반으로 구축된 구글의 고성능 오픈소스 원격 ## 3. 인터페이스 명세서 (`protoapi.proto`) -저장소 루트의 [protoapi.proto](../protoapi.proto) 파일은 앞서 설계한 `Random` 서비스를 구축하기 위해 아래와 같이 사양을 선언해 둡니다. +저장소 루트의 [protoapi.proto](../protoapi.proto) 파일은 앞서 설계한 `IoTService`를 구축하기 위해 아래와 같이 사양을 선언해 둡니다. ```proto syntax = "proto3"; option go_package = "./protoapi/;protoapi"; -service Random { +service IoTService { rpc GetDate (RequestDateTime) returns (DateTime); - rpc GetRandom (RandomParams) returns (RandomInt); + rpc UpdateSensingData (SensingData) returns (SensingResponse); rpc GetRandomPass (RequestPass) returns (RandomPass); } -message RandomParams { - int64 Seed = 1; - int64 Place = 2; +message SensingData { + string DeviceId = 1; + double Temperature = 2; + double Humidity = 3; } -message RandomInt { - int64 Value = 1; +message SensingResponse { + bool Success = 1; + string Message = 2; } message DateTime { @@ -124,16 +126,16 @@ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. \ * **구조체 정의**: ```go - type RandomServer struct { - protoapi.UnimplementedRandomServer + type IoTServer struct { + protoapi.UnimplementedIoTServiceServer } ``` - `UnimplementedRandomServer`를 임베딩하여, 향후 메서드가 새로 추가되더라도 기존 서버가 빌드 에러 없이 최소한의 호환(unimplemented 에러 응답)을 가지게 강제합니다. + `UnimplementedIoTServiceServer`를 임베딩하여, 향후 메서드가 새로 추가되더라도 기존 서버가 빌드 에러 없이 최소한의 호환(unimplemented 에러 응답)을 가지게 강제합니다. * **서버 기동 흐름**: ```go server := grpc.NewServer() - var randomServer RandomServer - protoapi.RegisterRandomServer(server, randomServer) + var iotServer IoTServer + protoapi.RegisterIoTServiceServer(server, iotServer) reflection.Register(server) // grpcurl 등 외부 디버깅 목적 listen, _ := net.Listen("tcp", port) server.Serve(listen) @@ -145,17 +147,17 @@ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. \ ```go conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) defer conn.Close() - client := protoapi.NewRandomClient(conn) + client := protoapi.NewIoTServiceClient(conn) ``` `insecure.NewCredentials()`를 전달하여 TLS를 건너뛴 채 평문으로 빠르고 간단한 로컬 테스트 환경을 구축합니다. * **원격 호출**: - `client.GetDate()`, `client.GetRandom()`, `client.GetRandomPass()`를 차례로 호출하여 매개변수와 결과를 콘솔로 확인합니다. + `client.GetDate()`, `client.GetRandomPass()`, `client.UpdateSensingData()`를 차례로 호출하여 매개변수와 결과를 콘솔로 확인합니다. ### 5.3 gRPC 실습 예제 동작 흐름 예제가 구동되면 서버와 클라이언트 간에 다음과 같은 호출 시퀀스가 순차적으로 실행됩니다: 1. **날짜 조회 (`GetDate`)**: 클라이언트가 서버에 날짜 조회를 요청하고, 서버는 자체의 현재 날짜와 시간 문자열을 포맷하여 반환합니다. 2. **비밀번호 생성 (`GetRandomPass`)**: 클라이언트가 생성할 무작위 비밀번호의 길이(기본 8자)와 난수 생성 시드값을 전달하면, 서버는 지정된 사양의 임의 문자열을 작성해 반환합니다. -3. **난수 생성 (`GetRandom`)**: 서로 다른 위치(Place) 및 시드(Seed) 값을 전달하여 각각 1회씩, 총 2회의 독립적인 난수 생성(의사 난수 정수값) 결과를 반환받아 출력합니다. +3. **센싱 데이터 업데이트 (`UpdateSensingData`)**: 클라이언트가 기기 ID("sensor-room-01")와 수집한 온/습도 환경 변수를 전달하면, 서버는 이를 수신하여 화면에 상세 센싱 값을 출력하고 업데이트 완료 성공 응답을 전송합니다. --- diff --git a/examples/grpcentity/client.go b/examples/grpcentity/client.go index 1d2c2a1..7db93d2 100644 --- a/examples/grpcentity/client.go +++ b/examples/grpcentity/client.go @@ -11,7 +11,7 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -func AskingDateTime(ctx context.Context, m protoapi.RandomClient) (*protoapi.DateTime, error) { +func AskingDateTime(ctx context.Context, m protoapi.IoTServiceClient) (*protoapi.DateTime, error) { request := &protoapi.RequestDateTime{ Value: "Please send me the date and time", } @@ -19,7 +19,7 @@ func AskingDateTime(ctx context.Context, m protoapi.RandomClient) (*protoapi.Dat return m.GetDate(ctx, request) } -func AskPass(ctx context.Context, m protoapi.RandomClient, seed int64, length int64) (*protoapi.RandomPass, error) { +func AskPass(ctx context.Context, m protoapi.IoTServiceClient, seed int64, length int64) (*protoapi.RandomPass, error) { request := &protoapi.RequestPass{ Seed: seed, Length: length, @@ -28,13 +28,14 @@ func AskPass(ctx context.Context, m protoapi.RandomClient, seed int64, length in return m.GetRandomPass(ctx, request) } -func AskRandom(ctx context.Context, m protoapi.RandomClient, seed int64, place int64) (*protoapi.RandomInt, error) { - request := &protoapi.RandomParams{ - Seed: seed, - Place: place, +func AskUpdateSensingData(ctx context.Context, m protoapi.IoTServiceClient, deviceId string, temp float64, humid float64) (*protoapi.SensingResponse, error) { + request := &protoapi.SensingData{ + DeviceId: deviceId, + Temperature: temp, + Humidity: humid, } - return m.GetRandom(ctx, request) + return m.UpdateSensingData(ctx, request) } func ClientRun(addr string) { @@ -44,10 +45,7 @@ func ClientRun(addr string) { return } - rand.Seed(time.Now().Unix()) - seed := int64(rand.Intn(100)) - - client := protoapi.NewRandomClient(conn) + client := protoapi.NewIoTServiceClient(conn) r, err := AskingDateTime(context.Background(), client) if err != nil { fmt.Println(err) @@ -55,6 +53,7 @@ func ClientRun(addr string) { } fmt.Println("Server Date and Time:", r.Value) + rand.Seed(time.Now().Unix()) length := int64(rand.Intn(20)) p, err := AskPass(context.Background(), client, 100, length+1) if err != nil { @@ -63,18 +62,11 @@ func ClientRun(addr string) { } fmt.Println("Random Password:", p.Password) - place := int64(rand.Intn(100)) - i, err := AskRandom(context.Background(), client, seed, place) + res, err := AskUpdateSensingData(context.Background(), client, "sensor-room-01", 24.5, 52.3) if err != nil { fmt.Println(err) return } - fmt.Println("Random Integer 1:", i.Value) - - k, err := AskRandom(context.Background(), client, seed, place-1) - if err != nil { - fmt.Println(err) - return - } - fmt.Println("Random Integer 2:", k.Value) + fmt.Println("Sensing Update Success:", res.Success) + fmt.Println("Sensing Update Message:", res.Message) } diff --git a/examples/grpcentity/server.go b/examples/grpcentity/server.go index fec2b51..e8540b5 100644 --- a/examples/grpcentity/server.go +++ b/examples/grpcentity/server.go @@ -50,11 +50,11 @@ func getString(len int64) string { return temp } -type RandomServer struct { - protoapi.UnimplementedRandomServer +type IoTServer struct { + protoapi.UnimplementedIoTServiceServer } -func (RandomServer) GetDate(ctx context.Context, r *protoapi.RequestDateTime) (*protoapi.DateTime, error) { +func (IoTServer) GetDate(ctx context.Context, r *protoapi.RequestDateTime) (*protoapi.DateTime, error) { currentTime := time.Now() response := &protoapi.DateTime{ Value: currentTime.String(), @@ -63,26 +63,18 @@ func (RandomServer) GetDate(ctx context.Context, r *protoapi.RequestDateTime) (* return response, nil } -func (RandomServer) GetRandom(ctx context.Context, r *protoapi.RandomParams) (*protoapi.RandomInt, error) { - src := rand.NewSource(r.GetSeed()) - place := r.GetPlace() - temp := random(min, max, src) - for { - place-- - if place <= 0 { - break - } - temp = random(min, max, src) - } +func (IoTServer) UpdateSensingData(ctx context.Context, r *protoapi.SensingData) (*protoapi.SensingResponse, error) { + fmt.Printf("Received sensing data - Device: %s, Temp: %.2f°C, Humid: %.2f%%\n", r.GetDeviceId(), r.GetTemperature(), r.GetHumidity()) - response := &protoapi.RandomInt{ - Value: int64(temp), + response := &protoapi.SensingResponse{ + Success: true, + Message: fmt.Sprintf("Sensing data updated successfully for device %s", r.GetDeviceId()), } return response, nil } -func (RandomServer) GetRandomPass(ctx context.Context, r *protoapi.RequestPass) (*protoapi.RandomPass, error) { +func (IoTServer) GetRandomPass(ctx context.Context, r *protoapi.RequestPass) (*protoapi.RandomPass, error) { rand.Seed(r.GetSeed()) temp := getString(r.GetLength()) @@ -95,8 +87,8 @@ func (RandomServer) GetRandomPass(ctx context.Context, r *protoapi.RequestPass) func ServerRun(addr string) { server := grpc.NewServer() - var randomServer RandomServer - protoapi.RegisterRandomServer(server, randomServer) + var iotServer IoTServer + protoapi.RegisterIoTServiceServer(server, iotServer) reflection.Register(server) diff --git a/project_analysis_report.md b/project_analysis_report.md new file mode 100644 index 0000000..da18df7 --- /dev/null +++ b/project_analysis_report.md @@ -0,0 +1,151 @@ +# 프로젝트 분석 보고서: grpccanary + +## 1. 개요 + +`grpccanary`는 **Go 언어로 JSON 처리 → HTTP 서버 → gRPC 서버/클라이언트로 이어지는 학습 여정을 다루는 한국어 튜토리얼 저장소**입니다. 저장소 이름(`grpccanary`)과 달리 실제 내용은 gRPC 단일 주제가 아니라, "데이터 직렬화(JSON) → 웹 서버(HTTP/gin) → 원격 프로시저 호출(gRPC)"로 난이도를 높여가는 3단계 실습 커리큘럼이며, 현재는 **gRPC 파트가 가장 완성도 높게 구현**되어 있습니다. + +저장소 루트에는 이 학습 프로젝트와는 성격이 다른 **`multi-agent-mux`라는 다중 에이전트(Claude Code) 오케스트레이션 인프라**가 함께 자리 잡고 있습니다(`.agents/`, `.mam/` 디렉토리). 이 인프라는 tmux 세션 + MQTT 메시지 브로커를 이용해 여러 AI 에이전트(팀장/리뷰어 역할)가 하나의 작업 저장소를 공유하며 협업하도록 설계된 별도의 도구 체계이며, 실제로 이 보고서를 작성하는 작업(`job 702ea1d8`) 자체도 이 인프라를 통해 위임되었습니다. + +즉 이 저장소는 **"gRPC를 배우기 위한 Go 예제 코드"**와 **"그 예제 코드를 여러 AI 에이전트가 함께 작업하도록 돕는 협업 프레임워크"**가 한 저장소 안에 공존하는 구조입니다. + +--- + +## 2. 저장소 구조 + +``` +grpccanary/ +├── README.md # 메인 튜토리얼 (JSON/HTTP/gRPC 개념 설명 + 실행 가이드, 한국어) +├── AGENTS.md # LLM 코딩 행동 지침 (일반 원칙) +├── go.mod / go.sum # Go 모듈 정의 (module grpccanary, go 1.25.4) +├── protoapi.proto # gRPC 서비스 IDL(Protocol Buffers) 정의 원본 +├── protoapi/ # protoc로 생성된 Go stub 코드 +│ ├── protoapi.pb.go # 메시지 타입 (protoc-gen-go) +│ └── protoapi_grpc.pb.go # 서비스/클라이언트 stub (protoc-gen-go-grpc) +├── obj.json # JSON 예제용 샘플 데이터 파일 +├── examples/ +│ ├── main.go # 실행 진입점 (현재는 JSON 예제만 호출하도록 설정됨) +│ ├── jsonexample/ +│ │ └── json_parser.go # encoding/json 마샬링·언마샬링 예제 +│ ├── httpentity/ +│ │ ├── server.go # gin 기반 HTTP 서버 (전체가 주석 처리된 미완성 스텁) +│ │ └── client.go # 패키지 선언만 있는 빈 파일 +│ └── grpcentity/ +│ ├── server.go # gRPC 서버 구현 (Random 서비스) +│ ├── client.go # gRPC 클라이언트 구현 +│ └── README.md # gRPC 서버/클라이언트 구현 상세 해설 (한국어) +├── docs/ +│ └── Working with JSON/ +│ ├── README.md # JSON 관련 학습 자료 (영문) +│ └── README-kr.md # JSON 관련 학습 자료 (한글) +├── scripts/ +│ └── generate-env.sh # .env.example → .env 복사 스크립트 (멀티에이전트 인프라용) +├── .env.example # 멀티에이전트 인프라(MQTT 브로커 등) 설정 템플릿 +├── .agents/ # 멀티에이전트 오케스트레이션 규칙·스킬 (아래 4절 참고) +└── .mam/ # 멀티에이전트 잡(Job) 레지스트리 및 세션 상태 (런타임 산출물) +``` + +빌드 확인 결과 `go build ./...` 및 `go vet ./...` 모두 오류 없이 통과했으며, 테스트 파일(`*_test.go`)은 저장소에 존재하지 않습니다. + +--- + +## 3. 핵심 구성 요소 (학습용 Go 코드) + +### 3.1 진입점 — `examples/main.go` +- 프로젝트의 유일한 `main()` 함수. `var port = ":8080"`을 정의하고 있으며, 기본 상태에서는 `jsonexample.JsonParsingExample()`만 호출합니다. +- gRPC 예제를 실행하려면 README.md 안내에 따라 `main()` 내부를 수동으로 편집해 `grpcSample()`을 호출하도록 바꿔야 합니다(미사용 import 오류 방지를 위해 `jsonexample` import를 주석 처리해야 함). 즉 **하나의 코드베이스 안에서 학습 단계별로 진입점을 수동 전환하는 방식**으로 설계되어 있습니다. +- `grpcSample()` 함수는 `entity.ServerRun(port)`를 고루틴으로 백그라운드 실행한 뒤 1초 대기 후 `entity.ClientRun(...)`을 호출해, 같은 프로세스 안에서 서버·클라이언트가 통신하는 데모를 구성합니다. + +### 3.2 JSON 파싱 예제 — `examples/jsonexample/json_parser.go` +- `encoding/json` 표준 라이브러리를 이용해 (1) `map[string]interface{}` ↔ JSON 문자열 변환, (2) 구조체(`Person`) ↔ JSON 변환의 마샬링/언마샬링을 시연합니다. +- 외부 의존성 없이 표준 라이브러리만 사용하는 가장 단순한 예제로, 커리큘럼의 1단계 역할을 합니다. + +### 3.3 HTTP 서버 예제 — `examples/httpentity/` +- `server.go`는 `gin-gonic/gin`을 이용한 REST API 서버(정적 파일 서빙 + `/api/randomNumber`, `/api/randomPassword`, `/api/randomDate` 라우트) 초안이 **전체 주석 처리**된 상태로만 존재합니다. 즉 코드는 작성되어 있으나 활성화되지 않은 미완성/보류 상태입니다. +- `client.go`는 `package httpentity` 선언 한 줄만 있는 빈 파일입니다. +- `go.mod`에는 `gin-gonic/gin` 의존성이 여전히 선언되어 있어, 이 파트가 완전히 폐기된 것이 아니라 추후 재개를 염두에 둔 진행 중(work-in-progress) 상태로 보입니다. + +### 3.4 gRPC 서비스 정의 — `protoapi.proto` +- `proto3` 문법으로 `Random`이라는 gRPC 서비스를 정의하며 3개의 RPC 메서드를 제공합니다. + - `GetDate(RequestDateTime) returns (DateTime)` — 서버의 현재 날짜/시간 반환 + - `GetRandom(RandomParams) returns (RandomInt)` — 시드(Seed)와 위치(Place)를 기반으로 한 의사난수 생성 + - `GetRandomPass(RequestPass) returns (RandomPass)` — 시드와 길이(Length)를 받아 무작위 ASCII 비밀번호 생성 +- `option go_package = "./protoapi/;protoapi"`로 지정되어 있어, `protoc` 컴파일 시 `protoapi/` 디렉토리에 Go 패키지 `protoapi`가 생성됩니다. +- 이미 컴파일된 stub(`protoapi/protoapi.pb.go`, `protoapi/protoapi_grpc.pb.go`)이 저장소에 커밋되어 있어 `protoc` 재설치 없이 바로 빌드/실행이 가능합니다. + +### 3.5 gRPC 서버 구현 — `examples/grpcentity/server.go` +- `RandomServer` 구조체가 `protoapi.UnimplementedRandomServer`를 임베딩하여 3개 RPC 메서드(`GetDate`, `GetRandom`, `GetRandomPass`)를 구현합니다. +- `getString(len int64)`은 ASCII 코드 `!`(33)부터 94개 범위 내에서 문자를 뽑아 임의 문자열(비밀번호)을 생성하는 헬퍼입니다. +- `ServerRun(addr string)`이 `grpc.NewServer()`로 서버를 만들고 `reflection.Register(server)`로 gRPC reflection(예: `grpcurl` 같은 외부 CLI 디버깅 도구 지원)을 활성화한 뒤 TCP 포트를 리슨합니다. +- **주의(코드상 특이점)**: `ServerRun`은 인자로 받은 `addr`을 실제로 사용하지 않고 패키지 전역 변수 `port = ":8080"`으로 리슨합니다(`net.Listen("tcp", port)`). 따라서 현재 코드는 항상 `:8080`에서만 동작하며, 호출부에서 다른 포트를 넘겨도 무시됩니다. +- `rand.Seed(r.GetSeed())`(패키지 전역 시드 설정, Go 1.20+에서는 deprecated)와 `rand.NewSource`를 혼용하고 있어 스레드 안전성이나 API 일관성 측면에서 다소 오래된 패턴을 보입니다(학습용 예제이므로 의도된 단순화로 판단됨). + +### 3.6 gRPC 클라이언트 구현 — `examples/grpcentity/client.go` +- `grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))`로 평문(TLS 미적용) 채널을 생성합니다(로컬 테스트 목적). +- `AskingDateTime`, `AskPass`, `AskRandom` 세 개의 래퍼 함수가 각각 대응하는 RPC를 호출합니다. +- `ClientRun(addr)`이 실행 엔트리포인트로, 날짜/시간 조회 1회, 비밀번호 생성 1회, 서로 다른 파라미터로 난수 생성 2회를 순차 호출하며 결과를 표준 출력으로 출력합니다. + +### 3.7 문서 자료 +- **`README.md`(루트)**: 이야기체(스타트업 개발자 '민우'의 사례)로 gRPC 도입 배경(REST/JSON의 한계, Protobuf 계약, HTTP/2)을 설명한 뒤, Go 버전 요구사항(1.25.4+), 의존성 설치, `main.go` 수동 편집 방법, 실행 명령(`go run ./examples`), 기대 출력 예시, 트러블슈팅(포트 충돌, 모듈 로드 오류)까지 안내하는 실행 가이드로 구성되어 있습니다. +- **`examples/grpcentity/README.md`**: `.proto` 파일 문법 요소별 상세 해설, `protoc`/`protoc-gen-go`/`protoc-gen-go-grpc` 설치 및 컴파일 절차, 서버·클라이언트 코드의 구현 단계별 설명을 담은 심화 가이드입니다. +- **`docs/Working with JSON/`**: JSON 관련 별도 학습 자료(영/한 병기)가 준비되어 있으나, 루트 README와 직접 링크되어 있지는 않습니다. + +--- + +## 4. 두 번째 레이어 — 멀티에이전트 오케스트레이션 인프라 + +저장소에는 학습 콘텐츠와 무관한 **AI 에이전트 협업 인프라**가 함께 포함되어 있습니다. + +- **`AGENTS.md`**: 일반적인 LLM 코딩 행동 지침(가정하지 말 것, 최소 변경, 외과적 수정 등)을 규정합니다. +- **`.agents/MULTI_AGENT_RULES.md`(+ 한국어판)**: MQTT 메시징 백플레인과 tmux 기반 다중 에이전트 협업 프로토콜을 정의합니다. 총괄 매니저(Orchestrator) → 팀장(개발/리뷰) → 작업 위임 및 리뷰 루프 → 완료 보고로 이어지는 워크플로우, Job 레지스트리(`​.mam/jobs/.json`, `fcntl` 파일 락), 세션 레지스트리(`.mam/agent-sessions.db`, SQLite WAL), HMAC-SHA256 기반 메시지 인증(PoC 모드에서는 비활성) 등을 상세히 규정합니다. +- **`.agents/skills/`**: `multi-agent-mux-{create,delegate-job,loop,monitor,resume,status,stop}` 등 실제 세션 생성·작업 위임·모니터링·중지를 수행하는 스킬(스크립트) 모음입니다. +- **`.mam/`**: 위 인프라의 런타임 상태 저장소로, `jobs/`(작업 레지스트리 및 브리프 파일), `agent-sessions.yaml`/`.db`(세션 상태), `agent_homes/`(에이전트별 메모리), `delegate_job_logs/`(위임 감사 로그)를 포함합니다. 이번 작업 브리프(`​.mam/jobs/702ea1d8/brief.md`) 역시 이 구조를 통해 전달되었습니다. +- **`scripts/generate-env.sh`, `.env.example`**: 이 인프라가 사용하는 MQTT 브로커 접속 정보, 경로 설정 등을 `.env`로 초기화하는 헬퍼입니다. 실제 비밀 값은 `.env`(git-ignored)에만 두고 `.env.example`에는 플레이스홀더만 커밋하도록 설계되어 있습니다. + +**참고**: 새로 추가된 `.gitignore`는 `.agents/`, `.mam/`, `.env`, `AGENTS.md` 등을 앞으로 git 추적 대상에서 제외하도록 지정하고 있습니다. 다만 `git log`를 보면 `AGENTS.md`, `examples/grpcentity/README.md` 등은 과거 커밋(`init 251117`, `update 260609`, 이후 docs 커밋들)에서 이미 저장소에 커밋되어 있으므로, 이번 `.gitignore` 추가는 향후 신규/변경 파일이 실수로 다시 커밋되는 것을 막기 위한 조치로 보입니다(기존에 추적 중인 파일을 소급 제외하지는 않음). + +--- + +## 5. 실행 방법 + +### 5.1 사전 준비 +```bash +go version # Go 1.25.4 이상 필요 +go mod download # 의존성 다운로드 +``` + +### 5.2 JSON 예제 실행 (기본값, 별도 수정 불필요) +```bash +go run ./examples +``` +`examples/main.go`가 기본적으로 `jsonexample.JsonParsingExample()`만 호출하므로, 별도 수정 없이 바로 JSON 마샬링/언마샬링 결과가 콘솔에 출력됩니다. + +### 5.3 gRPC 예제 실행 (수동 편집 필요) +1. `examples/main.go`를 열어 `jsonexample` import를 주석 처리하고 `main()` 안에서 `grpcSample()`을 호출하도록 변경. +2. 저장 후 실행: + ```bash + go run ./examples + ``` +3. 내부적으로 `:8080` 포트에서 gRPC 서버(고루틴)가 기동되고, 1초 후 클라이언트가 `GetDate`, `GetRandomPass`, `GetRandom`(2회) 순으로 RPC를 호출하며 결과를 출력합니다. +4. 포트 충돌 시(`bind: address already in use`) `examples/grpcentity/server.go`의 `port` 전역 변수를 변경해야 합니다(3.5절에서 언급했듯 `ServerRun`의 `addr` 인자는 무시되므로, 포트 변경은 이 전역 변수 수정으로만 가능). + +### 5.4 HTTP 예제 +`httpentity` 패키지는 서버 로직 전체가 주석 처리되어 있고 클라이언트 파일은 비어 있어, 현재 상태로는 실행 가능한 산출물이 없습니다. 향후 주석을 해제하고 `main.go`에 호출부를 추가해야 실습이 가능합니다. + +### 5.5 `.proto` 재컴파일 (선택) +`.proto` 정의를 수정하고 stub을 재생성하려면: +```bash +protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. \ + --go-grpc_opt=paths=source_relative protoapi.proto +``` +사전에 `protoc`, `protoc-gen-go`, `protoc-gen-go-grpc`가 설치되어 있어야 합니다(설치 방법은 `examples/grpcentity/README.md` 참고). + +--- + +## 6. 주요 관찰 사항 및 특이점 + +1. **단일 진입점, 수동 전환 방식**: `examples/main.go`가 유일한 실행 지점이며, 학습 단계(JSON/HTTP/gRPC)를 전환하려면 코드를 직접 편집해야 합니다. 각 예제를 독립적으로 실행할 수 있는 별도 커맨드나 플래그는 없습니다. +2. **`ServerRun(addr)`의 `addr` 인자 미사용**: gRPC 서버는 전달받은 인자 대신 패키지 전역 `port` 변수로 리슨하므로, 함수 시그니처와 실제 동작이 일치하지 않는 잠재적 혼동 요소입니다. +3. **HTTP 파트 미완성**: `gin` 의존성은 `go.mod`에 존재하지만 실제 코드는 비활성 상태로, 커리큘럼상 "다음 실습 예정" 단계로 보입니다. +4. **테스트 부재**: 저장소 전체에 자동화된 테스트(`*_test.go`)가 없어, 코드 정상 동작 여부는 수동 실행(`go run`)과 콘솔 출력 확인에 의존합니다. `go build ./...`, `go vet ./...`는 통과합니다. +5. **레거시 API 사용**: `rand.Seed(...)`(전역 시드 설정) 등 Go 최신 버전에서 권장되지 않는(deprecated) 패턴이 사용되고 있으나, 학습용 예제의 단순성을 위한 의도적 선택으로 보입니다. +6. **이중 성격의 저장소**: 순수 학습 콘텐츠(gRPC/JSON/HTTP 튜토리얼)와 AI 에이전트 협업 인프라(multi-agent-mux)가 한 저장소에 공존하며, 두 영역은 서로 기능적으로 독립적입니다. 협업 인프라(`​.agents/`, `.mam/`)는 이 학습 프로젝트를 대상으로 여러 AI 에이전트가 작업을 위임받고 결과를 보고하는 용도로 사용되고 있습니다(본 보고서 작성 작업 자체가 그 예시). diff --git a/protoapi.proto b/protoapi.proto index 2ffa200..8410e7c 100644 --- a/protoapi.proto +++ b/protoapi.proto @@ -2,23 +2,23 @@ syntax = "proto3"; option go_package = "./protoapi/;protoapi"; -service Random { +service IoTService { rpc GetDate (RequestDateTime) returns (DateTime); - rpc GetRandom (RandomParams) returns (RandomInt); + rpc UpdateSensingData (SensingData) returns (SensingResponse); rpc GetRandomPass (RequestPass) returns (RandomPass); } -// For random number -message RandomParams { - int64 Seed = 1; - int64 Place = 2; +message SensingData { + string DeviceId = 1; + double Temperature = 2; + double Humidity = 3; } -message RandomInt { - int64 Value = 1; +message SensingResponse { + bool Success = 1; + string Message = 2; } -// For date time message DateTime { string Value = 1; } @@ -27,7 +27,6 @@ message RequestDateTime { string Value = 2; } -// For random password message RequestPass { int64 Seed = 1; int64 Length = 8; diff --git a/protoapi/protoapi.pb.go b/protoapi/protoapi.pb.go index ca93be3..2c0092f 100644 --- a/protoapi/protoapi.pb.go +++ b/protoapi/protoapi.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 -// protoc v3.21.12 +// protoc-gen-go v1.36.11 +// protoc v5.27.2 // source: protoapi.proto package protoapi @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -20,34 +21,31 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// For random number -type RandomParams struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type SensingData struct { + state protoimpl.MessageState `protogen:"open.v1"` + DeviceId string `protobuf:"bytes,1,opt,name=DeviceId,proto3" json:"DeviceId,omitempty"` + Temperature float64 `protobuf:"fixed64,2,opt,name=Temperature,proto3" json:"Temperature,omitempty"` + Humidity float64 `protobuf:"fixed64,3,opt,name=Humidity,proto3" json:"Humidity,omitempty"` unknownFields protoimpl.UnknownFields - - Seed int64 `protobuf:"varint,1,opt,name=Seed,proto3" json:"Seed,omitempty"` - Place int64 `protobuf:"varint,2,opt,name=Place,proto3" json:"Place,omitempty"` + sizeCache protoimpl.SizeCache } -func (x *RandomParams) Reset() { - *x = RandomParams{} - if protoimpl.UnsafeEnabled { - mi := &file_protoapi_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } +func (x *SensingData) Reset() { + *x = SensingData{} + mi := &file_protoapi_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *RandomParams) String() string { +func (x *SensingData) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RandomParams) ProtoMessage() {} +func (*SensingData) ProtoMessage() {} -func (x *RandomParams) ProtoReflect() protoreflect.Message { +func (x *SensingData) ProtoReflect() protoreflect.Message { mi := &file_protoapi_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -57,51 +55,56 @@ func (x *RandomParams) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RandomParams.ProtoReflect.Descriptor instead. -func (*RandomParams) Descriptor() ([]byte, []int) { +// Deprecated: Use SensingData.ProtoReflect.Descriptor instead. +func (*SensingData) Descriptor() ([]byte, []int) { return file_protoapi_proto_rawDescGZIP(), []int{0} } -func (x *RandomParams) GetSeed() int64 { +func (x *SensingData) GetDeviceId() string { if x != nil { - return x.Seed + return x.DeviceId + } + return "" +} + +func (x *SensingData) GetTemperature() float64 { + if x != nil { + return x.Temperature } return 0 } -func (x *RandomParams) GetPlace() int64 { +func (x *SensingData) GetHumidity() float64 { if x != nil { - return x.Place + return x.Humidity } return 0 } -type RandomInt struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type SensingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=Success,proto3" json:"Success,omitempty"` + Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"` unknownFields protoimpl.UnknownFields - - Value int64 `protobuf:"varint,1,opt,name=Value,proto3" json:"Value,omitempty"` + sizeCache protoimpl.SizeCache } -func (x *RandomInt) Reset() { - *x = RandomInt{} - if protoimpl.UnsafeEnabled { - mi := &file_protoapi_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } +func (x *SensingResponse) Reset() { + *x = SensingResponse{} + mi := &file_protoapi_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *RandomInt) String() string { +func (x *SensingResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RandomInt) ProtoMessage() {} +func (*SensingResponse) ProtoMessage() {} -func (x *RandomInt) ProtoReflect() protoreflect.Message { +func (x *SensingResponse) ProtoReflect() protoreflect.Message { mi := &file_protoapi_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -111,34 +114,37 @@ func (x *RandomInt) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RandomInt.ProtoReflect.Descriptor instead. -func (*RandomInt) Descriptor() ([]byte, []int) { +// Deprecated: Use SensingResponse.ProtoReflect.Descriptor instead. +func (*SensingResponse) Descriptor() ([]byte, []int) { return file_protoapi_proto_rawDescGZIP(), []int{1} } -func (x *RandomInt) GetValue() int64 { +func (x *SensingResponse) GetSuccess() bool { if x != nil { - return x.Value + return x.Success } - return 0 + return false } -// For date time -type DateTime struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *SensingResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} - Value string `protobuf:"bytes,1,opt,name=Value,proto3" json:"Value,omitempty"` +type DateTime struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=Value,proto3" json:"Value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DateTime) Reset() { *x = DateTime{} - if protoimpl.UnsafeEnabled { - mi := &file_protoapi_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_protoapi_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DateTime) String() string { @@ -149,7 +155,7 @@ func (*DateTime) ProtoMessage() {} func (x *DateTime) ProtoReflect() protoreflect.Message { mi := &file_protoapi_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -172,20 +178,17 @@ func (x *DateTime) GetValue() string { } type RequestDateTime struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"` unknownFields protoimpl.UnknownFields - - Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RequestDateTime) Reset() { *x = RequestDateTime{} - if protoimpl.UnsafeEnabled { - mi := &file_protoapi_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_protoapi_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RequestDateTime) String() string { @@ -196,7 +199,7 @@ func (*RequestDateTime) ProtoMessage() {} func (x *RequestDateTime) ProtoReflect() protoreflect.Message { mi := &file_protoapi_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -218,23 +221,19 @@ func (x *RequestDateTime) GetValue() string { return "" } -// For random password type RequestPass struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Seed int64 `protobuf:"varint,1,opt,name=Seed,proto3" json:"Seed,omitempty"` + Length int64 `protobuf:"varint,8,opt,name=Length,proto3" json:"Length,omitempty"` unknownFields protoimpl.UnknownFields - - Seed int64 `protobuf:"varint,1,opt,name=Seed,proto3" json:"Seed,omitempty"` - Length int64 `protobuf:"varint,8,opt,name=Length,proto3" json:"Length,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RequestPass) Reset() { *x = RequestPass{} - if protoimpl.UnsafeEnabled { - mi := &file_protoapi_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_protoapi_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RequestPass) String() string { @@ -245,7 +244,7 @@ func (*RequestPass) ProtoMessage() {} func (x *RequestPass) ProtoReflect() protoreflect.Message { mi := &file_protoapi_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -275,20 +274,17 @@ func (x *RequestPass) GetLength() int64 { } type RandomPass struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=Password,proto3" json:"Password,omitempty"` unknownFields protoimpl.UnknownFields - - Password string `protobuf:"bytes,1,opt,name=Password,proto3" json:"Password,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RandomPass) Reset() { *x = RandomPass{} - if protoimpl.UnsafeEnabled { - mi := &file_protoapi_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_protoapi_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RandomPass) String() string { @@ -299,7 +295,7 @@ func (*RandomPass) ProtoMessage() {} func (x *RandomPass) ProtoReflect() protoreflect.Message { mi := &file_protoapi_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -323,66 +319,60 @@ func (x *RandomPass) GetPassword() string { var File_protoapi_proto protoreflect.FileDescriptor -var file_protoapi_proto_rawDesc = []byte{ - 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0x38, 0x0a, 0x0c, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, - 0x12, 0x12, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, - 0x53, 0x65, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x05, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x22, 0x21, 0x0a, 0x09, 0x52, 0x61, - 0x6e, 0x64, 0x6f, 0x6d, 0x49, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x20, 0x0a, - 0x08, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, - 0x27, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, - 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x39, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x65, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x4c, - 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4c, 0x65, 0x6e, - 0x67, 0x74, 0x68, 0x22, 0x28, 0x0a, 0x0a, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x73, - 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x32, 0x84, 0x01, - 0x0a, 0x06, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x12, 0x26, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x44, - 0x61, 0x74, 0x65, 0x12, 0x10, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, - 0x65, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x09, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, - 0x12, 0x26, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x12, 0x0d, 0x2e, - 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x0a, 0x2e, 0x52, - 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x49, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x52, - 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x12, 0x0c, 0x2e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x1a, 0x0b, 0x2e, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, - 0x50, 0x61, 0x73, 0x73, 0x42, 0x16, 0x5a, 0x14, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, - 0x70, 0x69, 0x2f, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, -} +const file_protoapi_proto_rawDesc = "" + + "\n" + + "\x0eprotoapi.proto\"g\n" + + "\vSensingData\x12\x1a\n" + + "\bDeviceId\x18\x01 \x01(\tR\bDeviceId\x12 \n" + + "\vTemperature\x18\x02 \x01(\x01R\vTemperature\x12\x1a\n" + + "\bHumidity\x18\x03 \x01(\x01R\bHumidity\"E\n" + + "\x0fSensingResponse\x12\x18\n" + + "\aSuccess\x18\x01 \x01(\bR\aSuccess\x12\x18\n" + + "\aMessage\x18\x02 \x01(\tR\aMessage\" \n" + + "\bDateTime\x12\x14\n" + + "\x05Value\x18\x01 \x01(\tR\x05Value\"'\n" + + "\x0fRequestDateTime\x12\x14\n" + + "\x05Value\x18\x02 \x01(\tR\x05Value\"9\n" + + "\vRequestPass\x12\x12\n" + + "\x04Seed\x18\x01 \x01(\x03R\x04Seed\x12\x16\n" + + "\x06Length\x18\b \x01(\x03R\x06Length\"(\n" + + "\n" + + "RandomPass\x12\x1a\n" + + "\bPassword\x18\x01 \x01(\tR\bPassword2\x95\x01\n" + + "\n" + + "IoTService\x12&\n" + + "\aGetDate\x12\x10.RequestDateTime\x1a\t.DateTime\x123\n" + + "\x11UpdateSensingData\x12\f.SensingData\x1a\x10.SensingResponse\x12*\n" + + "\rGetRandomPass\x12\f.RequestPass\x1a\v.RandomPassB\x16Z\x14./protoapi/;protoapib\x06proto3" var ( file_protoapi_proto_rawDescOnce sync.Once - file_protoapi_proto_rawDescData = file_protoapi_proto_rawDesc + file_protoapi_proto_rawDescData []byte ) func file_protoapi_proto_rawDescGZIP() []byte { file_protoapi_proto_rawDescOnce.Do(func() { - file_protoapi_proto_rawDescData = protoimpl.X.CompressGZIP(file_protoapi_proto_rawDescData) + file_protoapi_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protoapi_proto_rawDesc), len(file_protoapi_proto_rawDesc))) }) return file_protoapi_proto_rawDescData } var file_protoapi_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_protoapi_proto_goTypes = []interface{}{ - (*RandomParams)(nil), // 0: RandomParams - (*RandomInt)(nil), // 1: RandomInt +var file_protoapi_proto_goTypes = []any{ + (*SensingData)(nil), // 0: SensingData + (*SensingResponse)(nil), // 1: SensingResponse (*DateTime)(nil), // 2: DateTime (*RequestDateTime)(nil), // 3: RequestDateTime (*RequestPass)(nil), // 4: RequestPass (*RandomPass)(nil), // 5: RandomPass } var file_protoapi_proto_depIdxs = []int32{ - 3, // 0: Random.GetDate:input_type -> RequestDateTime - 0, // 1: Random.GetRandom:input_type -> RandomParams - 4, // 2: Random.GetRandomPass:input_type -> RequestPass - 2, // 3: Random.GetDate:output_type -> DateTime - 1, // 4: Random.GetRandom:output_type -> RandomInt - 5, // 5: Random.GetRandomPass:output_type -> RandomPass + 3, // 0: IoTService.GetDate:input_type -> RequestDateTime + 0, // 1: IoTService.UpdateSensingData:input_type -> SensingData + 4, // 2: IoTService.GetRandomPass:input_type -> RequestPass + 2, // 3: IoTService.GetDate:output_type -> DateTime + 1, // 4: IoTService.UpdateSensingData:output_type -> SensingResponse + 5, // 5: IoTService.GetRandomPass:output_type -> RandomPass 3, // [3:6] is the sub-list for method output_type 0, // [0:3] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -395,85 +385,11 @@ func file_protoapi_proto_init() { if File_protoapi_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_protoapi_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RandomParams); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_protoapi_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RandomInt); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_protoapi_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DateTime); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_protoapi_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RequestDateTime); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_protoapi_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RequestPass); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_protoapi_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RandomPass); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_protoapi_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_protoapi_proto_rawDesc), len(file_protoapi_proto_rawDesc)), NumEnums: 0, NumMessages: 6, NumExtensions: 0, @@ -484,7 +400,6 @@ func file_protoapi_proto_init() { MessageInfos: file_protoapi_proto_msgTypes, }.Build() File_protoapi_proto = out.File - file_protoapi_proto_rawDesc = nil file_protoapi_proto_goTypes = nil file_protoapi_proto_depIdxs = nil } diff --git a/protoapi/protoapi_grpc.pb.go b/protoapi/protoapi_grpc.pb.go index 70f3ec4..03d6090 100644 --- a/protoapi/protoapi_grpc.pb.go +++ b/protoapi/protoapi_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc-gen-go-grpc v1.6.2 +// - protoc v5.27.2 // source: protoapi.proto package protoapi @@ -19,177 +19,177 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Random_GetDate_FullMethodName = "/Random/GetDate" - Random_GetRandom_FullMethodName = "/Random/GetRandom" - Random_GetRandomPass_FullMethodName = "/Random/GetRandomPass" + IoTService_GetDate_FullMethodName = "/IoTService/GetDate" + IoTService_UpdateSensingData_FullMethodName = "/IoTService/UpdateSensingData" + IoTService_GetRandomPass_FullMethodName = "/IoTService/GetRandomPass" ) -// RandomClient is the client API for Random service. +// IoTServiceClient is the client API for IoTService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type RandomClient interface { +type IoTServiceClient interface { GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error) - GetRandom(ctx context.Context, in *RandomParams, opts ...grpc.CallOption) (*RandomInt, error) + UpdateSensingData(ctx context.Context, in *SensingData, opts ...grpc.CallOption) (*SensingResponse, error) GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error) } -type randomClient struct { +type ioTServiceClient struct { cc grpc.ClientConnInterface } -func NewRandomClient(cc grpc.ClientConnInterface) RandomClient { - return &randomClient{cc} +func NewIoTServiceClient(cc grpc.ClientConnInterface) IoTServiceClient { + return &ioTServiceClient{cc} } -func (c *randomClient) GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error) { +func (c *ioTServiceClient) GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DateTime) - err := c.cc.Invoke(ctx, Random_GetDate_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IoTService_GetDate_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *randomClient) GetRandom(ctx context.Context, in *RandomParams, opts ...grpc.CallOption) (*RandomInt, error) { +func (c *ioTServiceClient) UpdateSensingData(ctx context.Context, in *SensingData, opts ...grpc.CallOption) (*SensingResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(RandomInt) - err := c.cc.Invoke(ctx, Random_GetRandom_FullMethodName, in, out, cOpts...) + out := new(SensingResponse) + err := c.cc.Invoke(ctx, IoTService_UpdateSensingData_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *randomClient) GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error) { +func (c *ioTServiceClient) GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(RandomPass) - err := c.cc.Invoke(ctx, Random_GetRandomPass_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IoTService_GetRandomPass_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -// RandomServer is the server API for Random service. -// All implementations must embed UnimplementedRandomServer +// IoTServiceServer is the server API for IoTService service. +// All implementations must embed UnimplementedIoTServiceServer // for forward compatibility. -type RandomServer interface { +type IoTServiceServer interface { GetDate(context.Context, *RequestDateTime) (*DateTime, error) - GetRandom(context.Context, *RandomParams) (*RandomInt, error) + UpdateSensingData(context.Context, *SensingData) (*SensingResponse, error) GetRandomPass(context.Context, *RequestPass) (*RandomPass, error) - mustEmbedUnimplementedRandomServer() + mustEmbedUnimplementedIoTServiceServer() } -// UnimplementedRandomServer must be embedded to have +// UnimplementedIoTServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. -type UnimplementedRandomServer struct{} +type UnimplementedIoTServiceServer struct{} -func (UnimplementedRandomServer) GetDate(context.Context, *RequestDateTime) (*DateTime, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetDate not implemented") +func (UnimplementedIoTServiceServer) GetDate(context.Context, *RequestDateTime) (*DateTime, error) { + return nil, status.Error(codes.Unimplemented, "method GetDate not implemented") } -func (UnimplementedRandomServer) GetRandom(context.Context, *RandomParams) (*RandomInt, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetRandom not implemented") +func (UnimplementedIoTServiceServer) UpdateSensingData(context.Context, *SensingData) (*SensingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateSensingData not implemented") } -func (UnimplementedRandomServer) GetRandomPass(context.Context, *RequestPass) (*RandomPass, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetRandomPass not implemented") +func (UnimplementedIoTServiceServer) GetRandomPass(context.Context, *RequestPass) (*RandomPass, error) { + return nil, status.Error(codes.Unimplemented, "method GetRandomPass not implemented") } -func (UnimplementedRandomServer) mustEmbedUnimplementedRandomServer() {} -func (UnimplementedRandomServer) testEmbeddedByValue() {} +func (UnimplementedIoTServiceServer) mustEmbedUnimplementedIoTServiceServer() {} +func (UnimplementedIoTServiceServer) testEmbeddedByValue() {} -// UnsafeRandomServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to RandomServer will +// UnsafeIoTServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to IoTServiceServer will // result in compilation errors. -type UnsafeRandomServer interface { - mustEmbedUnimplementedRandomServer() +type UnsafeIoTServiceServer interface { + mustEmbedUnimplementedIoTServiceServer() } -func RegisterRandomServer(s grpc.ServiceRegistrar, srv RandomServer) { - // If the following call pancis, it indicates UnimplementedRandomServer was +func RegisterIoTServiceServer(s grpc.ServiceRegistrar, srv IoTServiceServer) { + // If the following call panics, it indicates UnimplementedIoTServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } - s.RegisterService(&Random_ServiceDesc, srv) + s.RegisterService(&IoTService_ServiceDesc, srv) } -func _Random_GetDate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IoTService_GetDate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RequestDateTime) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(RandomServer).GetDate(ctx, in) + return srv.(IoTServiceServer).GetDate(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Random_GetDate_FullMethodName, + FullMethod: IoTService_GetDate_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RandomServer).GetDate(ctx, req.(*RequestDateTime)) + return srv.(IoTServiceServer).GetDate(ctx, req.(*RequestDateTime)) } return interceptor(ctx, in, info, handler) } -func _Random_GetRandom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RandomParams) +func _IoTService_UpdateSensingData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SensingData) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(RandomServer).GetRandom(ctx, in) + return srv.(IoTServiceServer).UpdateSensingData(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Random_GetRandom_FullMethodName, + FullMethod: IoTService_UpdateSensingData_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RandomServer).GetRandom(ctx, req.(*RandomParams)) + return srv.(IoTServiceServer).UpdateSensingData(ctx, req.(*SensingData)) } return interceptor(ctx, in, info, handler) } -func _Random_GetRandomPass_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IoTService_GetRandomPass_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RequestPass) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(RandomServer).GetRandomPass(ctx, in) + return srv.(IoTServiceServer).GetRandomPass(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Random_GetRandomPass_FullMethodName, + FullMethod: IoTService_GetRandomPass_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RandomServer).GetRandomPass(ctx, req.(*RequestPass)) + return srv.(IoTServiceServer).GetRandomPass(ctx, req.(*RequestPass)) } return interceptor(ctx, in, info, handler) } -// Random_ServiceDesc is the grpc.ServiceDesc for Random service. +// IoTService_ServiceDesc is the grpc.ServiceDesc for IoTService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) -var Random_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "Random", - HandlerType: (*RandomServer)(nil), +var IoTService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "IoTService", + HandlerType: (*IoTServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetDate", - Handler: _Random_GetDate_Handler, + Handler: _IoTService_GetDate_Handler, }, { - MethodName: "GetRandom", - Handler: _Random_GetRandom_Handler, + MethodName: "UpdateSensingData", + Handler: _IoTService_UpdateSensingData_Handler, }, { MethodName: "GetRandomPass", - Handler: _Random_GetRandomPass_Handler, + Handler: _IoTService_GetRandomPass_Handler, }, }, Streams: []grpc.StreamDesc{}, diff --git a/scripts/generate-env.sh b/scripts/generate-env.sh new file mode 100755 index 0000000..027d89a --- /dev/null +++ b/scripts/generate-env.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# generate-env.sh — create a local .env from the committed .env.example template. +# +# Behaviour: +# - .env absent → copy .env.example to .env, print the path. +# - .env present → no-op (leaves your edits intact), exit 0. +# - .env present --force → overwrite .env from .env.example (backs up to .env.bak). +# +# Paths are resolved relative to this script (repo root = parent of deploy/), +# so it works regardless of the caller's cwd. +# +# Usage: deploy/generate-env.sh [--force] [-h|--help] +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SRC="$REPO_ROOT/.env.example" +DST="$REPO_ROOT/.env" + +FORCE=0 +while [ $# -gt 0 ]; do + case "$1" in + --force) FORCE=1; shift ;; + -h|--help) + echo "Usage: $0 [--force]" + echo " Create .env from .env.example. --force overwrites an existing .env." + exit 0 ;; + *) echo "ERROR: unknown arg: $1" >&2; echo "Usage: $0 [--force]" >&2; exit 2 ;; + esac +done + +[ -f "$SRC" ] || { echo "ERROR: template not found: $SRC" >&2; exit 1; } + +if [ -f "$DST" ] && [ "$FORCE" != "1" ]; then + echo "no-op: $DST already exists (use --force to overwrite)" + exit 0 +fi + +if [ -f "$DST" ] && [ "$FORCE" = "1" ]; then + cp -p "$DST" "$DST.bak" + echo "backed up existing .env -> $DST.bak" +fi + +cp "$SRC" "$DST" +echo "created: $DST" +echo "Next: edit $DST and fill in any secrets (look for 'replace_me')."