Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TEST: Jetson power fan + log-streaming for testing only #2390

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion src/api-binder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import log from '../lib/supervisor-console';
import * as deviceState from '../device-state';
import * as globalEventBus from '../event-bus';
import * as TargetState from './poll';
import * as logger from '../logger';
import * as logger from '../logging';

import * as apiHelper from '../lib/api-helper';
import { startReporting, stateReportErrors } from './report';
Expand Down
2 changes: 1 addition & 1 deletion src/compose/application-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type StrictEventEmitter from 'strict-event-emitter-types';

import * as config from '../config';
import type { Transaction } from '../db';
import * as logger from '../logger';
import * as logger from '../logging';
import LocalModeManager from '../local-mode';

import * as dbFormat from '../device-state/db-format';
Expand Down
2 changes: 1 addition & 1 deletion src/compose/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
StatusError,
} from '../lib/errors';
import * as LogTypes from '../lib/log-types';
import * as logger from '../logger';
import * as logger from '../logging';
import { ImageDownloadBackoffError } from './errors';

import type { Service } from './service';
Expand Down
2 changes: 1 addition & 1 deletion src/compose/network-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { isNotFoundError } from '../lib/errors';
import logTypes = require('../lib/log-types');
import log from '../lib/supervisor-console';

import * as logger from '../logger';
import * as logger from '../logging';
import { Network } from './network';
import { ResourceRecreationAttemptError } from './errors';

Expand Down
2 changes: 1 addition & 1 deletion src/compose/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type dockerode from 'dockerode';

import { docker } from '../lib/docker-utils';
import logTypes = require('../lib/log-types');
import * as logger from '../logger';
import * as logger from '../logging';
import log from '../lib/supervisor-console';
import * as ComposeUtils from './utils';

Expand Down
2 changes: 1 addition & 1 deletion src/compose/service-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type StrictEventEmitter from 'strict-event-emitter-types';

import * as config from '../config';
import { docker } from '../lib/docker-utils';
import * as logger from '../logger';
import * as logger from '../logging';

import { PermissiveNumber } from '../config/types';
import * as constants from '../lib/constants';
Expand Down
2 changes: 1 addition & 1 deletion src/compose/volume-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { pathOnData } from '../lib/host-utils';
import { docker } from '../lib/docker-utils';
import * as LogTypes from '../lib/log-types';
import log from '../lib/supervisor-console';
import * as logger from '../logger';
import * as logger from '../logging';
import { ResourceRecreationAttemptError } from './errors';
import type { VolumeConfig } from './types';
import { Volume } from './volume';
Expand Down
2 changes: 1 addition & 1 deletion src/compose/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { docker } from '../lib/docker-utils';
import { InternalInconsistencyError } from '../lib/errors';
import * as LogTypes from '../lib/log-types';
import type { LabelObject } from '../types';
import * as logger from '../logger';
import * as logger from '../logging';
import * as ComposeUtils from './utils';

import type {
Expand Down
6 changes: 6 additions & 0 deletions src/config/backends/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export abstract class ConfigBackend {
// Example an empty string should return null.
public abstract createConfigVarName(configName: string): string | null;

// Is a reboot required for the given config options?
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async isRebootRequired(_opts: ConfigOptions): Promise<boolean> {
return true;
}

// Allow a chosen config backend to be initialised
public async initialise(): Promise<ConfigBackend> {
return this;
Expand Down
2 changes: 1 addition & 1 deletion src/config/backends/config-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ConfigBackend } from './backend';
import { exec, exists } from '../../lib/fs-utils';
import * as hostUtils from '../../lib/host-utils';
import * as constants from '../../lib/constants';
import * as logger from '../../logger';
import * as logger from '../../logging';
import log from '../../lib/supervisor-console';

/**
Expand Down
15 changes: 13 additions & 2 deletions src/config/backends/config-txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import _ from 'lodash';

import type { ConfigOptions } from './backend';
import { ConfigBackend } from './backend';
import { isSupportedConfig as isPowerFanSupportedConfig } from './power-fan';
import * as constants from '../../lib/constants';
import log from '../../lib/supervisor-console';
import { exists } from '../../lib/fs-utils';
Expand Down Expand Up @@ -231,11 +232,21 @@ export class ConfigTxt extends ConfigBackend {
}

public isSupportedConfig(configName: string): boolean {
return !ConfigTxt.forbiddenConfigKeys.includes(configName);
return (
!ConfigTxt.forbiddenConfigKeys.includes(configName) &&
// power_mode and fan_profile are managed by the power-fan backend, so
// need to be excluded here as the config var name prefix is the same.
!isPowerFanSupportedConfig(configName)
);
}

public isBootConfigVar(envVar: string): boolean {
return envVar.startsWith(ConfigTxt.bootConfigVarPrefix);
return (
envVar.startsWith(ConfigTxt.bootConfigVarPrefix) &&
// power_mode and fan_profile are managed by the power-fan backend, so
// need to be excluded here as the config var name prefix is the same.
!isPowerFanSupportedConfig(envVar)
);
}

public processConfigVarName(envVar: string): string {
Expand Down
3 changes: 3 additions & 0 deletions src/config/backends/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ConfigTxt } from './config-txt';
import { ConfigFs } from './config-fs';
import { Odmdata } from './odmdata';
import { SplashImage } from './splash-image';
import { PowerFanConfig } from './power-fan';
import { configJsonBackend } from '..';

export const allBackends = [
new Extlinux(),
Expand All @@ -12,6 +14,7 @@ export const allBackends = [
new ConfigFs(),
new Odmdata(),
new SplashImage(),
new PowerFanConfig(configJsonBackend),
];

export function matchesAnyBootConfig(envVar: string): boolean {
Expand Down
183 changes: 183 additions & 0 deletions src/config/backends/power-fan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { isRight } from 'fp-ts/lib/Either';
import Reporter from 'io-ts-reporters';
import * as t from 'io-ts';
import * as _ from 'lodash';

import { ConfigBackend } from './backend';
import type { ConfigOptions } from './backend';
import { schemaTypes } from '../schema-type';
import log from '../../lib/supervisor-console';
import * as constants from '../../lib/constants';

const isNullOrUndefined = (v: unknown): v is null | undefined =>
v === null || v === undefined;

type ConfigJsonBackend = {
get: (key: 'os') => Promise<unknown>;
set: (opts: { os: Record<string, any> }) => Promise<void>;
};

// Exported to exclude power & fan configs from being set for other backends
export const isSupportedConfig = (name: string): boolean => {
return PowerFanConfig.CONFIGS.has(PowerFanConfig.stripPrefix(name));
};

/**
* A backend to handle Jetson power and fan control
*
* Supports:
* - {BALENA|RESIN}_HOST_CONFIG_power_mode = "low" | "mid" | "high" | "default" |"$MODE_ID"
* - {BALENA|RESIN}_HOST_CONFIG_fan_profile = "quiet" | "cool" | "default" |"$MODE_ID"
*/
export class PowerFanConfig extends ConfigBackend {
public static readonly CONFIGS = new Set(['power_mode', 'fan_profile']);
private static readonly PREFIX = `${constants.hostConfigVarPrefix}CONFIG_`;
private static readonly SCHEMA = t.exact(
t.partial({
power: t.exact(
t.partial({
mode: t.string,
}),
),
fan: t.exact(
t.partial({
profile: t.string,
}),
),
}),
);

private readonly configJson: ConfigJsonBackend;
public constructor(configJson: ConfigJsonBackend) {
super();
this.configJson = configJson;
}

public static stripPrefix(name: string): string {
if (!name.startsWith(PowerFanConfig.PREFIX)) {
return name;
}
return name.substring(PowerFanConfig.PREFIX.length);
}

public async matches(deviceType: string): Promise<boolean> {
// We only support Jetpack 6 devices for now, which includes all Orin devices
// except for jetson-orin-nx-xv3 which is still on Jetpack 5 as of OS v5.1.36
return new Set([
'jetson-agx-orin-devkit',
'jetson-agx-orin-devkit-64gb',
'jetson-orin-nano-devkit-nvme',
'jetson-orin-nano-seeed-j3010',
'jetson-orin-nx-seeed-j4012',
'jetson-orin-nx-xavier-nx-devkit',
]).has(deviceType);
}

public async getBootConfig(): Promise<ConfigOptions> {
// Get raw config.json contents
let rawConf: unknown;
try {
rawConf = await this.configJson.get('os');
} catch (e: unknown) {
log.error(
`Failed to read config.json while getting power / fan configs: ${(e as Error).message ?? e}`,
);
return {};
}

// Decode to power fan schema from object type, filtering out unrelated values
const powerFanConfig = PowerFanConfig.SCHEMA.decode(rawConf);

if (isRight(powerFanConfig)) {
const conf = powerFanConfig.right;
return {
...(!isNullOrUndefined(conf.power?.mode) && {
power_mode: conf.power.mode,
}),
...(!isNullOrUndefined(conf.fan?.profile) && {
fan_profile: conf.fan.profile,
}),
};
} else {
return {};
}
}

public async setBootConfig(opts: ConfigOptions): Promise<void> {
// Read raw configs for "os" key from config.json
let rawConf;
try {
rawConf = await this.configJson.get('os');
} catch (err: unknown) {
log.error(`${(err as Error).message ?? err}`);
return;
}

// Decode to "os" object type while leaving in unrelated values
const maybeCurrentConf = schemaTypes.os.type.decode(rawConf);
if (!isRight(maybeCurrentConf)) {
log.error(
'Failed to decode current os config:',
Reporter.report(maybeCurrentConf),
);
return;
}
// Current config could be undefined if there's no os key in config.json, so default to empty object
const currentConf = maybeCurrentConf.right ?? {};

// Filter out unsupported options in target config
const supportedOpts = Object.fromEntries(
Object.entries(opts).filter(([key]) => this.isSupportedConfig(key)),
) as { power_mode?: string; fan_profile?: string };

const targetConf = structuredClone(currentConf);

// Update or delete power mode
if ('power_mode' in supportedOpts) {
targetConf.power = {
mode: supportedOpts.power_mode,
};
} else {
delete targetConf?.power;
}

// Update or delete fan profile
if ('fan_profile' in supportedOpts) {
targetConf.fan = {
profile: supportedOpts.fan_profile,
};
} else {
delete targetConf?.fan;
}

await this.configJson.set({ os: targetConf });
}

public isSupportedConfig = isSupportedConfig;

public isBootConfigVar(envVar: string): boolean {
return PowerFanConfig.CONFIGS.has(PowerFanConfig.stripPrefix(envVar));
}

public async isRebootRequired(opts: ConfigOptions): Promise<boolean> {
const supportedOpts = _.pickBy(
_.mapKeys(opts, (_value, key) => PowerFanConfig.stripPrefix(key)),
(_value, key) => this.isSupportedConfig(key),
);
const current = await this.getBootConfig();
// A reboot is only required if the power mode is changing
return current.power_mode !== supportedOpts.power_mode;
}

public processConfigVarName(envVar: string): string {
return PowerFanConfig.stripPrefix(envVar).toLowerCase();
}

public processConfigVarValue(_key: string, value: string): string {
return value;
}

public createConfigVarName(name: string): string | null {
return `${PowerFanConfig.PREFIX}${name}`;
}
}
Loading
Loading