예전에 Garmin Edge 1000을 사용했다면 기기를 가로로 눕혀 쓰는 Landscape 화면이 익숙할 수 있습니다. Edge 1000 공식 설명서에는 실제로 디스플레이 Orientation에서 Portrait와 Landscape를 선택하는 기능이 명시되어 있습니다. 반면 Edge 1040·1040 Solar의 지도 Orientation은 North Up과 Track Up을 바꾸는 기능일 뿐 화면 자체를 90도로 회전시키는 설정은 아닙니다.
그래서 이번 글에서는 별도의 완성 PRG 파일을 내려받아 넣는 방식이 아니라, Garmin Connect IQ SDK를 직접 설치하고 Monkey C로 Data Field를 새로 만든 뒤 PRG로 빌드해 Edge 1040에 사이드로드하는 과정을 처음부터 정리합니다. 현재시간, 속도, 케이던스, 심박, 거리, 주행시간, Edge 배터리뿐 아니라 필요하면 Garmin 케이던스 센서와 SRAM AXS 전자변속 배터리 상태까지 확장할 수 있습니다.
Edge 1000은 가로모드가 되는데 Edge 1040은 왜 안 될까?
| 구분 | Edge 1000 | Edge 1040 / 1040 Solar |
|---|---|---|
| 시스템 화면 Landscape | 지원 | 기본 설정에는 없음 |
| 지도 Orientation | North Up / Track Up / 3D | North Up / Track Up |
| Connect IQ Data Field | 지원 | 지원 |
| 가로형 데이터 화면 우회 | 굳이 필요 없음 | 커스텀 Data Field로 구현 가능 |
Edge 1040·1040 Solar의 Connect IQ 기기 ID는 edge1040이며,
화면 크기는 282×470입니다. Garmin Device Reference를 보면 1 Field 레이아웃은 282×470 전체 영역을 사용할 수 있고,
Data Field 메모리 한도는 131,072바이트입니다.
준비물|Connect IQ SDK와 Java 설치
2026년 8월 기준 Garmin 개발자 사이트의 최신 Connect IQ SDK는 9.2.0입니다. SDK Manager를 설치한 뒤 SDK와 Edge 1040 장치 패키지를 내려받고 Current SDK로 지정합니다. 명령줄 Monkey C 환경은 Garmin 공식 문서 기준 Java 11 이상이 필요합니다.
- Garmin Connect IQ SDK Manager 설치
- Garmin 계정 로그인
- 최신 Connect IQ SDK 설치
- Devices에서 Edge 1040 / 1040 Solar 패키지 설치
- 설치한 SDK를 Current SDK로 지정
- Java 11 이상 설치 및
java -version확인
참고로 Monkey C 확장프로그램은 Chrome 확장프로그램이 아니라 Visual Studio Code 확장입니다.
다만 VS Code를 쓰지 않아도 SDK의 monkeyc 명령만으로 PRG를 빌드할 수 있습니다.
새 프로젝트 폴더 구조 만들기
Edge1040Landscape/
├─ manifest.xml
├─ monkey.jungle
├─ developer_key.der
├─ source/
│ ├─ LandscapeApp.mc
│ └─ LandscapeView.mc
└─ resources/
├─ strings/
│ └─ strings.xml
└─ drawables/
├─ drawables.xml
└─ launcher_icon.png
앱 ID는 실제 프로젝트를 만들 때 새로운 UUID를 사용해야 합니다. 아래 예제의 ID는 설명용이므로 그대로 배포용 앱 ID로 쓰기보다 새 값을 만드는 것을 권장합니다.
1. manifest.xml 작성
<?xml version="1.0"?>
<iq:manifest xmlns:iq="http://www.garmin.com/xml/connectiq" version="3">
<iq:application
id="0123456789abcdef0123456789abcdef"
type="datafield"
name="@Strings.AppName"
entry="LandscapeApp"
launcherIcon="@Drawables.LauncherIcon"
minApiLevel="4.2.2">
<iq:products>
<iq:product id="edge1040"/>
</iq:products>
<!-- 센서 배터리까지 읽을 때만 Ant 권한 추가 -->
<iq:permissions>
<iq:uses-permission id="Ant"/>
</iq:permissions>
<iq:languages>
<iq:language>eng</iq:language>
</iq:languages>
</iq:application>
</iq:manifest>
케이던스 센서나 SRAM AXS 같은 페어링된 ANT+ 장치를 직접 읽지 않는다면 Ant 권한은 뺄 수 있습니다.
반대로 Toybox.AntPlus를 이용해 센서 배터리 상태까지 읽을 계획이라면 Ant 권한을 manifest에 명시해야 합니다.
2. monkey.jungle 작성
project.manifest = manifest.xml
3. LandscapeApp.mc 작성
using Toybox.Application as Application;
class LandscapeApp extends Application.AppBase {
function initialize() {
AppBase.initialize();
}
function getInitialView() {
return [ new LandscapeView() ];
}
}
가로화면 핵심|이미지를 돌리지 말고 좌표를 직접 바꾼다
가장 중요한 부분입니다. 470×282 크기의 큰 오프스크린 비트맵을 만든 다음 통째로 90도 돌리는 방식은 Data Field의 메모리 한도와 실제 기기 렌더링 호환성을 함께 고려해야 합니다. 단순한 계기판이라면 훨씬 안전하게 가상의 470×282 가로 좌표를 282×470 실제 화면 좌표로 직접 변환할 수 있습니다.
이 방식의 장점은 BufferedBitmap, drawBitmap2(), AffineTransform에 의존하지 않고
fillRectangle()과 좌표 변환만으로 가로형 데이터를 그릴 수 있다는 점입니다.
숫자와 영문 라벨은 5×7 픽셀 글꼴로 직접 그리면 회전된 텍스트 문제도 피할 수 있습니다.
4. LandscapeView.mc 전체 예제
using Toybox.Graphics as Graphics;
using Toybox.System as System;
using Toybox.WatchUi as WatchUi;
class LandscapeView extends WatchUi.DataField {
private var _speed = 0.0f;
private var _distance = 0.0f;
private var _elapsed = 0;
private var _heartRate = null;
private var _cadence = null;
function initialize() {
DataField.initialize();
}
function compute(info) {
if (info == null) {
return;
}
_speed = (info.currentSpeed != null) ? info.currentSpeed * 3.6f : 0.0f;
_distance = (info.elapsedDistance != null) ? info.elapsedDistance / 1000.0f : 0.0f;
_elapsed = (info.elapsedTime != null) ? info.elapsedTime : 0;
_heartRate = info.currentHeartRate;
_cadence = info.currentCadence;
}
function onUpdate(dc) {
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
dc.clear();
var clock = System.getClockTime();
var now = two(clock.hour) + ":" + two(clock.min);
var edgeBat = System.getSystemStats().battery.format("%.0f") + "%";
var speedText = _speed.format("%.1f");
var distText = _distance.format("%.1f");
var cadText = (_cadence == null) ? "--" : _cadence.format("%d");
var hrText = (_heartRate == null) ? "--" : _heartRate.format("%d");
var rideText = formatElapsed(_elapsed);
// 논리 가로 캔버스: 470 x 282
// 실제 Edge 1040 캔버스: 282 x 470
drawText(dc, 14, 8, "TIME", 2);
drawText(dc, 14, 29, now, 7);
drawText(dc, 260, 8, "SPEED", 2);
drawText(dc, 260, 32, speedText, 6);
drawText(dc, 260, 86, "KMH", 2);
hLine(dc, 14, 108, 456, 2);
drawText(dc, 14, 121, "CAD", 2);
drawText(dc, 14, 143, cadText, 6);
drawText(dc, 14, 194, "RPM", 2);
drawText(dc, 152, 121, "HR", 2);
drawText(dc, 152, 143, hrText, 6);
drawText(dc, 152, 194, "BPM", 2);
drawText(dc, 278, 121, "DIST", 2);
drawText(dc, 278, 146, distText, 5);
drawText(dc, 278, 194, "KM", 2);
hLine(dc, 14, 216, 456, 2);
drawText(dc, 14, 228, "RIDE", 2);
drawText(dc, 82, 226, rideText, 3);
drawText(dc, 292, 228, "EDGE", 2);
drawText(dc, 365, 226, edgeBat, 3);
}
private function two(v) {
if (v < 10) {
return "0" + v.format("%d");
}
return v.format("%d");
}
private function formatElapsed(ms) {
var total = (ms / 1000).toNumber();
var h = total / 3600;
var m = (total % 3600) / 60;
var s = total % 60;
if (h > 0) {
return h.format("%d") + ":" + two(m);
}
return two(m) + ":" + two(s);
}
// 논리 가로 좌표를 실제 세로 프레임버퍼로 직접 변환한다.
// BufferedBitmap, drawBitmap2, AffineTransform을 사용하지 않는다.
private function lRect(dc, x, y, w, h) {
var px = 282 - y - h;
var py = x;
dc.fillRectangle(px, py, h, w);
}
private function hLine(dc, x, y, w, t) {
lRect(dc, x, y, w, t);
}
private function drawText(dc, x, y, text, scale) {
if (text == null) {
return;
}
var cursor = x;
for (var i = 0; i < text.length(); i += 1) {
var ch = text.substring(i, i + 1);
drawGlyph(dc, cursor, y, ch, scale);
cursor += 6 * scale;
}
}
private function drawGlyph(dc, x, y, ch, s) {
var rows = glyph(ch);
for (var r = 0; r < 7; r += 1) {
var bits = rows[r];
for (var c = 0; c < 5; c += 1) {
var mask = 1 << (4 - c);
if ((bits & mask) != 0) {
lRect(dc, x + c * s, y + r * s, s, s);
}
}
}
}
// 이 예제에서 사용하는 숫자·영문만 정의한 5x7 픽셀 글꼴
private function glyph(ch) {
switch (ch) {
case "0": return [14,17,19,21,25,17,14];
case "1": return [4,12,4,4,4,4,14];
case "2": return [14,17,1,2,4,8,31];
case "3": return [30,1,1,14,1,1,30];
case "4": return [2,6,10,18,31,2,2];
case "5": return [31,16,16,30,1,1,30];
case "6": return [14,16,16,30,17,17,14];
case "7": return [31,1,2,4,8,8,8];
case "8": return [14,17,17,14,17,17,14];
case "9": return [14,17,17,15,1,1,14];
case "A": return [14,17,17,31,17,17,17];
case "B": return [30,17,17,30,17,17,30];
case "C": return [14,17,16,16,16,17,14];
case "D": return [30,17,17,17,17,17,30];
case "E": return [31,16,16,30,16,16,31];
case "G": return [14,17,16,23,17,17,15];
case "H": return [17,17,17,31,17,17,17];
case "I": return [14,4,4,4,4,4,14];
case "K": return [17,18,20,24,20,18,17];
case "M": return [17,27,21,21,17,17,17];
case "P": return [30,17,17,30,16,16,16];
case "R": return [30,17,17,30,20,18,17];
case "S": return [15,16,16,14,1,1,30];
case "T": return [31,4,4,4,4,4,4];
case ":": return [0,4,4,0,4,4,0];
case ".": return [0,0,0,0,0,6,6];
case "-": return [0,0,0,31,0,0,0];
case "%": return [17,2,4,8,16,0,17];
case " ": return [0,0,0,0,0,0,0];
}
return [0,0,0,0,0,0,0];
}
}
현재시간·속도·케이던스·심박 값은 어디서 가져올까?
Connect IQ Data Field에서는 compute(info)로 현재 Activity 정보를 전달받습니다.
Activity.Info에는 currentSpeed, currentCadence, currentHeartRate, elapsedDistance, elapsedTime 등이 들어오며,
Garmin 공식 문서에서도 각 값이 null일 수 있으므로 반드시 null 체크를 하도록 안내합니다.
| 화면 항목 | Connect IQ 값 | 단위 |
|---|---|---|
| 현재 속도 | currentSpeed | m/s → km/h 변환 |
| 케이던스 | currentCadence | rpm |
| 심박 | currentHeartRate | bpm |
| 거리 | elapsedDistance | m → km 변환 |
| 주행시간 | elapsedTime | ms |
| Edge 본체 배터리 | System.getSystemStats().battery | % |
Garmin 케이던스 센서·SRAM AXS 배터리까지 표시하기
여기부터는 필수 기능이 아니라 확장 기능입니다. Connect IQ의 Toybox.AntPlus는 Garmin 기기에 이미 페어링된 ANT+ 장치에 접근할 수 있습니다.
BikeCadence와 Shifting 모두 AntPlus.Device를 상속하며,
Device에는 getBatteryStatus()와 getComponentIdentifiers()가 있습니다.
using Toybox.AntPlus as AntPlus;
private var _cadenceDevice = null;
private var _shiftingDevice = null;
private function initSensors() {
try {
_cadenceDevice = new AntPlus.BikeCadence(null);
} catch (e) {
_cadenceDevice = null;
}
try {
_shiftingDevice = new AntPlus.Shifting(null);
} catch (e) {
_shiftingDevice = null;
}
}
private function readCadenceBattery() {
if (_cadenceDevice == null) {
return null;
}
try {
return _cadenceDevice.getBatteryStatus(null);
} catch (e) {
return null;
}
}
private function readAxsBatteries() {
var result = [];
if (_shiftingDevice == null) {
return result;
}
try {
var ids = _shiftingDevice.getComponentIdentifiers();
if (ids == null || ids.size() == 0) {
var single = _shiftingDevice.getBatteryStatus(null);
if (single != null) {
result.add(single);
}
return result;
}
for (var i = 0; i < ids.size(); i += 1) {
var status = _shiftingDevice.getBatteryStatus(ids[i]);
if (status != null) {
result.add(status);
}
}
} catch (e) {
}
return result;
}
// BatteryStatus에는 batteryStatus 단계와 batteryVoltage가 들어온다.
// 정확한 0~100%가 항상 제공되는 구조는 아니다.
센서 배터리가 항상 정확한 73%처럼 나오는 것은 아닙니다. ANT+ BatteryStatus에는 NEW, GOOD, OK, LOW, CRITICAL 같은 상태값과 batteryVoltage가 제공될 수 있습니다. 따라서 화면에는 상태명이나 전압을 그대로 표시하는 것이 가장 보수적입니다. 필요하면 개발자가 상태값을 자신만의 범위 표시로 바꿀 수 있지만, 이를 Garmin이 제공하는 정확한 퍼센트라고 표현하면 안 됩니다.
SRAM AXS처럼 여러 구성품을 가진 시스템은 getComponentIdentifiers()로 확인된 구성품 ID에 대해
각각 getBatteryStatus(id)를 호출할 수 있습니다. 다만 각 ID가 왼쪽 쉬프터, 오른쪽 쉬프터, 뒤 변속기 중 무엇인지
항상 사람이 읽을 수 있는 이름으로 제공되는 것은 아니므로 실제 장치에서 값을 확인한 뒤 라벨을 붙이는 편이 안전합니다.
한글 가로 데이터필드는 어떻게 만들까?
가로 좌표를 직접 그리는 방식에서는 Garmin 기본 한글 폰트를 단순히 90도 돌리는 대신 한글 라벨을 작은 픽셀 글꼴 또는 미리 회전한 비트맵 리소스로 만들어 출력하는 편이 안정적입니다. 예를 들어 시간, 속도, 케이던스, 심박, 거리, 주행, 배터리처럼 실제 필요한 단어만 만들어 두면 전체 한글 폰트를 포함할 필요가 없어 Data Field 메모리 사용량도 줄일 수 있습니다.
시간 · 속도 · 케이던스 · 심박 · 거리 · 주행은 한글로 표시하고, km/h · rpm · bpm · 배터리 전압 같은 단위는 영문/숫자로 유지하면 화면이 깔끔합니다.
developer_key.der 만들기
Garmin PRG는 개발자 키로 서명해야 합니다. 명령줄에서 OpenSSL을 사용할 경우 Garmin 공식 문서에는 RSA 4096비트 키를 만들고 PKCS#8 DER 형식으로 변환하는 예제가 제공됩니다.
openssl genrsa -out developer_key.pem 4096
openssl pkcs8 -topk8 -inform PEM -outform DER ^
-in developer_key.pem ^
-out developer_key.der ^
-nocrypt
Edge 1040용 PRG 빌드
Windows에서 Current SDK 경로를 환경변수에 연결한 뒤 monkeyc로 빌드합니다.
Edge 1040·1040 Solar 타깃 ID는 edge1040입니다.
for /f usebackq %i in (%APPDATA%\Garmin\ConnectIQ\current-sdk.cfg) do set CIQ_HOME=%~pi
set PATH=%PATH%;%CIQ_HOME%\bin
monkeyc -d edge1040 ^
-f monkey.jungle ^
-o Edge1040Landscape.prg ^
-y developer_key.der ^
-r
성공하면 Edge1040Landscape.prg가 생성됩니다.
VS Code를 사용하는 경우에는 Garmin Monkey C 확장의 Build for Device 기능으로도 동일하게 실제 기기용 PRG를 만들 수 있습니다.
PRG 파일을 Edge 1040·1040 Solar에 설치하는 방법
- Edge 1040을 USB로 PC에 연결합니다.
- 생성한
Edge1040Landscape.prg하나를 복사합니다. Garmin → Apps폴더에 PRG를 넣습니다.- USB 연결을 안전하게 해제하고 Edge를 재부팅합니다.
- 활동 프로필 → 데이터 화면 → 1 Field 전체화면을 선택합니다.
- 데이터 필드 → Connect IQ → 직접 만든 Landscape Data Field를 선택합니다.
- 기기를 90도 돌려 가로 방향으로 장착합니다.
핵심은 반드시 1 Field 전체화면을 사용하는 것입니다. Edge 1040 Device Reference에서 1 Field는 282×470 전체 영역을 차지하므로 가로형 계기판을 구성하기 가장 적합합니다.
Connect IQ 필드를 선택했는데 Timer로 돌아갈 때
직접 만든 Data Field를 선택했는데 표시되지 않거나 기본 데이터 필드로 돌아간다면 코드 오류나 런타임 예외를 먼저 확인하는 것이 좋습니다.
Garmin 공식 디버깅 문서에 따르면 앱 크래시가 발생하면 /GARMIN/APPS/LOGS/CIQ_LOG.YAML에 오류 정보가 기록됩니다.
Garmin
└─ Apps
└─ Logs
├─ CIQ_LOG.YAML
└─ CIQ_LOG.BAK
로그에는 Error, Firmware-Version, Appname, Stack, pc 주소 등이 들어갑니다. 디버그 심볼이 포함된 빌드라면 소스 파일과 줄 번호까지 확인할 수 있습니다. 단순히 다시 설치하는 것보다 로그를 기준으로 그래픽 초기화, ANT 권한, 센서 객체 생성, 메모리 사용량을 차례로 분리해 확인하는 것이 빠릅니다.
Garmin Connect IQ 디버깅 공식문서가로모드 Data Field에서 피하는 것이 좋은 설계
- 전체 470×282 풀컬러 버퍼를 무조건 먼저 만드는 방식 — Data Field 메모리 한도를 함께 계산해야 합니다.
- null 체크 없이 센서값 바로 사용 — Activity.Info 값은 null일 수 있습니다.
- Ant 권한 없이 AntPlus 객체 생성 — manifest 권한을 먼저 확인합니다.
- 센서 배터리를 매 프레임마다 조회 — 배터리는 수십 초 단위로 갱신해도 충분합니다.
- AXS 구성품 ID를 무조건 LEFT/RIGHT/RD로 가정 — 실제 수신 ID를 확인한 뒤 라벨링하는 편이 안전합니다.
- 지도까지 가로로 바뀐다고 기대 — 이 방식은 커스텀 Data Field 화면만 가로형입니다.
출퇴근용이라면 어떤 데이터가 가장 실용적일까?
출퇴근처럼 매일 반복해서 사용하는 화면이라면 필드를 많이 넣는 것보다 현재시간과 속도를 가장 크게 두는 편이 좋습니다. 그 아래에 케이던스, 심박, 거리, 주행시간을 배치하고 Edge·센서·AXS 배터리는 구석에 작게 넣으면 주행 중 시선 이동을 줄일 수 있습니다.
자주 묻는 질문 FAQ
Edge 1040 Solar는 기본 가로모드를 지원하나요?
Edge 1000처럼 시스템 디스플레이를 Portrait/Landscape로 전환하는 기본 설정은 확인되지 않습니다. Edge 1040의 지도 Orientation은 North Up과 Track Up을 바꾸는 기능입니다.
Activity Profile을 수정하면 Landscape를 만들 수 있나요?
활동 프로필의 데이터 화면과 지도 방향은 변경할 수 있지만 시스템 화면 회전 속성을 추가하는 방식은 아닙니다. 가로형 주행 계기판이 필요하다면 Connect IQ Data Field를 별도로 만드는 방법이 현실적입니다.
가로 Data Field에 현재시간을 표시할 수 있나요?
가능합니다. System.getClockTime()으로 현재 로컬 시간을 읽어 시·분을 표시할 수 있습니다.
케이던스는 별도 ANT 코드를 사용해야 하나요?
케이던스 숫자 자체는 Data Field의 Activity.Info.currentCadence를 사용할 수 있습니다.
다만 케이던스 센서의 배터리 상태까지 직접 읽으려면 AntPlus API를 사용할 수 있습니다.
SRAM Rival AXS 배터리도 표시할 수 있나요?
Edge 1040은 Connect IQ의 Shifting API 지원 대상이며, 페어링된 전자변속 시스템의 구성품 ID와 BatteryStatus를 읽을 수 있습니다. 다만 정확한 퍼센트나 구성품 이름이 항상 그대로 제공된다고 가정하면 안 됩니다.
PRG만 Garmin Apps 폴더에 넣으면 되나요?
사이드로드하는 실제 실행 파일은 PRG입니다. 빌드 과정에서 생긴 debug.xml, gen, mir 같은 중간 산출물은 기기에 복사할 필요가 없습니다.
공식 참고자료
- Garmin Edge 1000 Display Settings — Orientation Portrait/Landscape
- Garmin Edge 1040 Map Orientation
- Garmin Connect IQ SDK
- Edge 1040 / 1040 Solar Device Reference
- Toybox.Activity.Info API
- Connect IQ ANT and ANT+
- Connect IQ Testing and Debugging
댓글
댓글 쓰기