Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
fffc3c66fThis file is pinned to the selected release. Download the original to open it in your design tool.
import { t } from '../i18n.mjs'
export const CAPABILITIES = Object.freeze({
FACE: 'face',
SPEECH: 'audio.speech',
SINGING: 'audio.singing',
TONE: 'audio.tone',
MOTION: 'motion',
LIGHTING: 'lighting',
BUTTONS: 'input.buttons',
IMU: 'input.imu',
HEAD_TOUCH: 'input.headTouch',
DRAWER: 'ui.drawer',
APPROVAL: 'ui.approval',
NETWORK: 'connectivity.network',
USB_AUDIO: 'audio.usb',
REMOTE_CONVERSATION: 'conversation.remote',
})
export const CAPABILITY_HOST_API_VERSIONS = Object.freeze({
[CAPABILITIES.APPROVAL]: 1,
[CAPABILITIES.USB_AUDIO]: 1,
[CAPABILITIES.REMOTE_CONVERSATION]: 1,
})
export const DEVICE_PROFILES = Object.freeze({
'm5stackchan-cores3': {
label: 'M5Stack-chan CoreS3',
status: 'supported',
deviceInstall: true,
xsArchiveVersion: [17, 8, 0],
firmwareVersionPrefixes: ['8.3.', '9.0.'],
chipPatterns: ['ESP32-S3'],
entrypoints: ['mod', 'miniapp'],
capabilities: Object.values(CAPABILITIES),
},
simulator: {
label: 'Webシミュレーター',
status: 'supported',
deviceInstall: false,
xsArchiveVersion: [17, 8, 0],
firmwareVersionPrefixes: ['8.3.', '9.0.'],
chipPatterns: [],
entrypoints: ['mod', 'miniapp'],
capabilities: [
CAPABILITIES.FACE,
CAPABILITIES.SPEECH,
CAPABILITIES.SINGING,
CAPABILITIES.TONE,
CAPABILITIES.MOTION,
CAPABILITIES.BUTTONS,
CAPABILITIES.DRAWER,
],
},
portable: {
label: '機種を限定しない',
status: 'experimental',
deviceInstall: false,
xsArchiveVersion: null,
firmwareVersionPrefixes: [],
chipPatterns: [],
entrypoints: ['mod'],
capabilities: [CAPABILITIES.FACE, CAPABILITIES.SPEECH, CAPABILITIES.TONE, CAPABILITIES.MOTION],
},
})
export const BLOCK_CAPABILITIES = Object.freeze({
stackchan_on_button: [CAPABILITIES.BUTTONS],
stackchan_on_imu: [CAPABILITIES.IMU],
stackchan_on_head_touch: [CAPABILITIES.HEAD_TOUCH],
stackchan_on_drawer_button: [CAPABILITIES.DRAWER],
stackchan_set_emotion: [CAPABILITIES.FACE],
stackchan_set_color: [CAPABILITIES.FACE],
stackchan_set_mouth: [CAPABILITIES.FACE],
stackchan_say: [CAPABILITIES.SPEECH],
stackchan_sing: [CAPABILITIES.SINGING],
stackchan_sing_score: [CAPABILITIES.SINGING],
stackchan_song_note_tuple: [CAPABILITIES.SINGING],
stackchan_song_rest_tuple: [CAPABILITIES.SINGING],
stackchan_song_note: [CAPABILITIES.SINGING],
stackchan_song_rest: [CAPABILITIES.SINGING],
stackchan_show_balloon: [CAPABILITIES.DRAWER],
stackchan_hide_balloon: [CAPABILITIES.DRAWER],
stackchan_tone: [CAPABILITIES.TONE],
stackchan_look_at: [CAPABILITIES.MOTION],
stackchan_look_away: [CAPABILITIES.MOTION],
stackchan_set_torque: [CAPABILITIES.MOTION],
stackchan_set_pose: [CAPABILITIES.MOTION],
stackchan_light_on: [CAPABILITIES.LIGHTING],
stackchan_light_off: [CAPABILITIES.LIGHTING],
stackchan_light_rainbow: [CAPABILITIES.LIGHTING],
stackchan_light_blink: [CAPABILITIES.LIGHTING],
stackchan_drawer_control: [CAPABILITIES.DRAWER],
stackchan_show_face: [CAPABILITIES.FACE],
})
export function profileFor(target) {
return DEVICE_PROFILES[target] ?? DEVICE_PROFILES.portable
}
export function requirementsForBlockTypes(blockTypes) {
return [...new Set(blockTypes.flatMap((type) => BLOCK_CAPABILITIES[type] ?? []))].sort()
}
export function unsupportedRequirements(target, requirements) {
const supported = new Set(profileFor(target).capabilities)
return requirements.filter((capability) => !supported.has(capability))
}
export function toolboxForTarget(toolbox, target) {
const supported = new Set(profileFor(target).capabilities)
const clone = structuredClone(toolbox)
for (const category of clone.contents ?? []) {
if (!Array.isArray(category.contents)) continue
category.contents = category.contents.filter((entry) => {
const requirements = BLOCK_CAPABILITIES[entry.type] ?? []
return requirements.every((capability) => supported.has(capability))
})
}
return clone
}
export function inspectDeploymentCompatibility(
target,
{
chip,
xsVersion,
firmwareVersion,
hostApiVersion = 0,
entrypoints = ['mod'],
requirements = [],
requireFirmware = false,
requireArchive = false,
} = {}
) {
const profile = profileFor(target)
const diagnostics = []
const unsupportedEntrypoints = entrypoints.filter((entrypoint) => !profile.entrypoints.includes(entrypoint))
if (unsupportedEntrypoints.length) {
diagnostics.push({
code: 'VP_ARCHIVE_ENTRYPOINT_UNSUPPORTED',
message: t('{profile}はarchiveの実行入口「{entrypoints}」に対応していません', {
profile: t(profile.label),
entrypoints: unsupportedEntrypoints.join(', '),
}),
})
}
const unsupportedCapabilities = [...new Set(requirements)].filter(
(capability) => !profile.capabilities.includes(capability)
)
if (unsupportedCapabilities.length) {
diagnostics.push({
code: 'VP_UNSUPPORTED_CAPABILITY',
message: t('{profile}は「{capability}」に対応していません', {
profile: t(profile.label),
capability: unsupportedCapabilities.join(', '),
}),
})
}
if (requireFirmware && !profile.deviceInstall) {
diagnostics.push({
code: 'VP_DEVICE_TARGET_UNSUPPORTED',
message: t('{profile}はWebSerial実機書き込みの対象ではありません', { profile: t(profile.label) }),
})
}
if (profile.chipPatterns.length) {
if (requireFirmware && !chip) {
diagnostics.push({
code: 'VP_DEVICE_CHIP_MISSING',
message: t('{profile}の実チップを確認できません', { profile: t(profile.label) }),
})
} else if (chip && !profile.chipPatterns.some((pattern) => String(chip).includes(pattern))) {
diagnostics.push({
code: 'VP_DEVICE_CHIP_MISMATCH',
message: t('{profile}の対象チップ({expected})と検出結果「{chip}」が一致しません', {
profile: t(profile.label),
expected: profile.chipPatterns.join(', '),
chip,
}),
})
}
}
if (profile.xsArchiveVersion) {
if (requireArchive && xsVersion === undefined) {
diagnostics.push({
code: 'VP_XS_VERSION_MISSING',
message: t('MODのXSバージョンを確認できません'),
})
} else if (
xsVersion !== undefined &&
(!Array.isArray(xsVersion) || profile.xsArchiveVersion.join('.') !== xsVersion.join('.'))
) {
const detectedXsVersion = Array.isArray(xsVersion) ? xsVersion.join('.') : t('不明')
diagnostics.push({
code: 'VP_XS_VERSION_MISMATCH',
message: t('{profile}のXS {expected}に対して、MODはXS {actual}です', {
profile: t(profile.label),
expected: profile.xsArchiveVersion.join('.'),
actual: detectedXsVersion,
}),
})
}
}
if (requireFirmware && profile.firmwareVersionPrefixes.length) {
if (!firmwareVersion) {
diagnostics.push({
code: 'VP_FIRMWARE_VERSION_MISSING',
message: t('{profile}のファームウェアバージョンを確認できません', { profile: t(profile.label) }),
})
} else if (!profile.firmwareVersionPrefixes.some((prefix) => String(firmwareVersion).startsWith(prefix))) {
diagnostics.push({
code: 'VP_FIRMWARE_VERSION_MISMATCH',
message: t('ファームウェア {firmwareVersion} は、{profile}の対応範囲({prefixes}系)に含まれません', {
firmwareVersion,
profile: t(profile.label),
prefixes: profile.firmwareVersionPrefixes.join(', '),
}),
})
}
}
if (requireFirmware) {
const detectedHostApiVersion = Number.isSafeInteger(hostApiVersion) && hostApiVersion >= 0 ? hostApiVersion : 0
const unavailableCapabilities = [...new Set(requirements)].filter(
(capability) =>
profile.capabilities.includes(capability) &&
(CAPABILITY_HOST_API_VERSIONS[capability] ?? 0) > detectedHostApiVersion
)
if (unavailableCapabilities.length) {
const requiredHostApiVersion = Math.max(
...unavailableCapabilities.map((capability) => CAPABILITY_HOST_API_VERSIONS[capability])
)
diagnostics.push({
code: 'VP_HOST_CAPABILITY_UNAVAILABLE',
message: t(
'ファームウェア {firmwareVersion} のhost API {actual}では「{capabilities}」を利用できません(API {required}以上が必要です)。host firmwareを更新してください',
{
firmwareVersion: firmwareVersion ?? t('不明'),
actual: detectedHostApiVersion,
capabilities: unavailableCapabilities.join(', '),
required: requiredHostApiVersion,
}
),
})
}
}
return { compatible: diagnostics.length === 0, profile, diagnostics }
}