온도를 읽어서 서버로 보내는 장치가 있다. 어느 날부터 온도가 0으로 올라왔는데 실제 온도가 0도는 아니었다.
Table of contents
Open Table of contents
실패를 값으로 돌려주고 있었다
읽는 코드는 이랬다.
float read_temp(void) {
uint8_t buf[2];
if (i2c_read(SENSOR_ADDR, REG_TEMP, buf, 2) != 0) {
return 0.0f;
}
return ((buf[0] << 8) | buf[1]) / 256.0f;
}
i2c_read 가 실패하면 0.0 을 돌려주고 부르는 쪽은 이 값을 그대로 쓴다.
float t = read_temp();
send_to_server(t);
실패가 유효한 값으로 둔갑한다. 0도는 있을 수 있는 온도라 구분이 안 된다.
범위 밖의 값을 쓰면 되지 않느냐고 할 수 있는데 그 방법도 약하다. 센서가 바뀌어 범위가 넓어지면 그 값이 유효한 값이 되고 예전 코드가 정상 값을 실패로 읽는다.
NaN 을 쓰는 방법도 나왔는데 이쪽은 어떤 유효한 온도와도 안 겹친다. 다만 NaN 은 비교가 전부 거짓이 되므로 범위 검사에 안 걸리고 그냥 지나간다.
실패를 값과 분리했다
성공 여부를 따로 돌려주게 바꿨다.
int read_temp(float *out) {
uint8_t buf[2];
if (i2c_read(SENSOR_ADDR, REG_TEMP, buf, 2) != 0) {
return -1;
}
*out = ((buf[0] << 8) | buf[1]) / 256.0f;
return 0;
}
부르는 쪽이 판단한다.
float t;
if (read_temp(&t) == 0) {
send_to_server(t);
} else {
log_error("온도 읽기 실패");
send_error_to_server("TEMP_READ_FAIL");
}
표준 라이브러리에도 같은 자리가 있다. fgetc 는 실패하면 EOF 를 돌려주는데 그것이 파일 끝이라서인지 읽기 오류라서인지가 안 갈려서 규격이 feof 나 ferror 로 구분하라고 못 박아 두었다.
못 보냈다는 것도 정보다
값을 못 읽으면 아무것도 안 보낼까 했는데 그러면 서버 쪽에서 장치가 죽은 것인지 센서만 문제인지 모른다. 오류를 보내게 했다.
정상 {"ts":1470628800,"temp":24.5}
실패 {"ts":1470628800,"error":"TEMP_READ_FAIL"}
무응답 (아무것도 안 옴)
세 가지가 구분된다. 무응답이면 장치나 통신 문제고 오류가 오면 센서 문제다.
값의 범위와 급변
읽기가 성공해도 값이 이상할 수 있다. 센서가 고장 나면 엉뚱한 값이 나온다.
#define TEMP_MIN (-40.0f)
#define TEMP_MAX ( 85.0f)
if (t < TEMP_MIN || t > TEMP_MAX) {
log_error("온도 범위 밖: %.1f", t);
return -2;
}
센서 사양서에 있는 동작 범위이고 그 밖의 값은 신뢰할 수 없다. 실제로 이것을 넣고 나서 -128도가 걸렸는데 통신 오류로 앞 바이트가 깨진 것이었다.
범위 안이어도 이상한 경우가 있었다. 24도에서 갑자기 60도가 되고 다시 24도로 돌아온다.
static float last_temp = NAN;
if (!isnan(last_temp) && fabsf(t - last_temp) > MAX_DELTA) {
log_warn("급변: %.1f -> %.1f", last_temp, t);
/* 한 번은 무시하고 다음 값을 본다 */
skip_count++;
if (skip_count < 3) return -3;
}
skip_count = 0;
last_temp = t;
세 번 연속 그러면 실제로 바뀐 것으로 보고 한 번만 튀는 것은 무시한다. MAX_DELTA 는 측정 주기와 물리적 변화 속도로 정했는데 1초에 5도 이상 변하는 것은 이 환경에서 없다.
검증 — 마지막 정상값과 시각
값을 못 읽는 동안 화면에 무엇을 보여 줄지 정해야 했다.
typedef struct {
float value;
uint32_t ts;
int valid;
} reading_t;
마지막 정상값과 그 시각을 들고 있다. 화면에는 값과 함께 얼마나 오래된 것인지 보여 줬다.
24.5°C (3초 전)
24.5°C (2분 전) ← 갱신이 멈춤
시각이 없으면 옛 값을 현재 값으로 읽는다.
if (now - r.ts > STALE_SEC) {
display_text("--.-°C");
} else {
display_temp(r.value);
}
일정 시간이 지나면 값을 안 보이게 했다. 옛 값을 계속 보여 주는 것보다 모름이 낫고 그 값으로 판단하면 안 되니까.
정리
- 실패를 값으로 돌려주면 유효한 값과 구분이 안 된다
- 범위 밖의 값을 쓰는 방법도 센서가 바뀌면 겹친다
NaN은 겹치지는 않지만 비교가 전부 거짓이라 범위 검사를 통과한다- 성공 여부와 값을 분리해서 돌려준다
fgetc가EOF로 두 상태를 못 가르고feof가 따로 필요한 것과 같은 자리다- 아무것도 안 보내는 것과 오류를 보내는 것은 다르고 셋을 구분한다
- 읽기가 성공해도 사양서의 동작 범위를 확인한다
- 범위 안이어도 급변하면 몇 번 연속인지로 판단한다
- 마지막 정상값과 그 시각을 같이 들고 있는다
- 오래되면 값을 안 보이고 옛 값보다 모름이 낫다