Firmware

FM1Kit

Peaking95/100

A Swift toolkit for the M-Vave FM-1 synthesiser

FM1Kit is custom firmware for the M-VAVE FM-1, by @ocean. Open source under MIT, latest release 0.5.1 (November 2026), 1 star on GitHub.

forM-VAVE FM-1·by@ocean

Unclaimed. Is this yours? Sign in with GitHub as @ocean to claim it.

Downloads
0
Star
1
Forks
0
Commits
49
Last commit
Get the source

No release files yet. Build from source.

MIT· ocean/FM1Kit·downloads come from the creator’s GitHub releases

49
Commits
12
Releases
49 commits and 12 releases in the last year
Activity · last 12 months
12 releases
  1. 0.5.1FM1Kit 0.5.1Latest0 files

    TransitFM1 refuses V16, as tested on an FM-1 in stage S, so the switching guide now sends people from it to V16 through MIDIMON 0.4 or LOOM-8.

    Fixed

    • LeavingGuide now sends people from TransitFM1 to V16 through MIDIMON 0.4 or LOOM-8: TransitFM1 refuses V16 at the header (tested on an FM-1 in stage S), since it takes only firmware images the size of V15's. Its steps to V16 no longer say the FM-1 keeps its bootloader, or that TransitFM1 must be confirmed as not V15. A TransitFM1 guide now has two warnings, not one.
    • packageRefused's explanation names TransitFM1, and says MIDIMON 0.4 and LOOM-8 take V16 too.
    • fm1probe install --reinstall-official describes the install by the package's identity, since it can't tell whether TransitFM1 is being installed over V15 or V15 over TransitFM1.

    No API changes: move from 0.5.0 with .upToNextMinor(from: "0.5.1").

  2. 0.5.0FM1Kit 0.5.00 files

    M-VAVE's V16, a guide to switching away from each firmware, and a fresh start that clears every firmware's saved data, all tested on an FM-1 in stage R. It also includes 0.5.0-beta.1's choice of device, which has been tested only with simulated devices so far.

    Added

    • From 0.5.0-beta.1: choosing a device when more than one is connected (FM1Updater.devices(), FM1Updater(transport:timing:device:), FM1Advice.Action.chooseDevice, and fm1probe devices and --device). It's tested with simulated devices only so far.
    • LeavingGuide: how to switch away from a firmware, worked out from its package before it's installed. FwscPackage.updateCode reads how the firmware installs others (UpdateCode: M-VAVE's, Felucca's original or fixed check, V15 recognised by hash, or a loader of its own), and the guide combines it with the FM-1's bootloader (FM1BootArea) to give a LeavingRoute to V15, V16, open firmware, other custom firmwares and m-bootloader, warnings to show before installing, and step-by-step instructions(to:). It's Codable, so apps can save it with the FM-1, which also tells TransitFM1 apart from V15. refusesV15OverOtherBootloaders is now updateCode == .feluccaOriginalCheck. Stage R confirmed its routes to and from V16 on hardware.
    • OfficialFirmware.v16: M-VAVE's V16 (FM-1_016), which their downloads page has served since 21 October 2026, with FM1Identity.officialV16 and FM1Identity.isOfficial. OfficialFirmware gains identity, isRedistributable (only V15 may be included in an app; download V16 from M-VAVE) and official (V15 and V16). FM1DeviceSummary names V16, and V16 over V16 is a reinstall, as for V15. Tested on hardware in stage R.
    • FlashBackup.firmware names V16 ("V16 (FM-1_016)") by its app.bin's SHA-256, as read from a real FM-1, instead of "unknown, naming FM-1_016".
    • InstallOptions.confirmsReinstallingOfficial and InstallCheck.canReinstallOfficial: installing M-VAVE's official firmware (V15) on an FM-1 that reports its identity, once the person confirms it's running a firmware that reports it on purpose, such as TransitFM1. Without it, FM1Kit couldn't take an FM-1 from TransitFM1 back to V15. alreadyInstalled for V15 now says so, and its advice is confirm. fm1probe install --reinstall-official does the same.

    Changed

    • FlashPlan.installing(_:onto:includingBootloader:erasingSettings:) is now installing(_:onto:includingBootloader:erasingSavedData:), and erases more: everything from the end of the new image up to 0xFBFFF, which covers every saved-data region firmwares declare (VM, BTIF, V15's voice banks in USR, V16's SEQ64) and what they leave outside them, so the new firmware starts from its defaults instead of meeting another firmware's leftovers. V16 leaves data in V15's USR region that makes V15 play one note at a time, which only this clears (tested on hardware, stage R). It never touches the boot area, 0xFC000 onwards or the factory data. Installing V15 this way writes its factory voices and the rest of its USR data straight back into the erased voice banks, so it's never silent and needs no voice restore. fm1probe flash-install --erase-settings is now --erase-saved-data.
    • bootloaderMismatch and refusesV15OverOtherBootloaders explain that V16 brings a different bootloader from V15, so firmwares with Felucca's original check refuse V16 on an FM-1 with V15's bootloader, and V15 once V16's is installed.
    • packageRefused says firmwares based on Felucca 1.6.1 refuse V16 as well as open firmware, whatever the bootloader (stage R). bootloaderMismatch no longer says download mode keeps the FM-1's bootloader: after V16, V15 is installed there with its own bootloader, fresh.
    • bootloaderMismatch and packageRefused name MIDIMON 0.4 as well as LOOM-8 as the firmware to install first, and put that before download mode: a firmware with Felucca's original check takes either, and they take V15 and open firmware (tested on hardware). Their advice is now chooseAnotherFirmware, not needsComputer, since the way out works from a phone.
    • fm1probe inspect suggests MIDIMON 0.4 or LOOM-8 for a firmware that refuses V15.

    Fixed

    • DownloadModeError.accessRefused promised that macOS would ask to allow access to a removable volume. macOS asks only a program in the logged-in person's session; a launchd helper is refused without being asked, whatever its Full Disk Access.
    • refusesV15OverOtherBootloaders was true for Quixotic7's TransitFM1 and RemoteFM1. They install every package with a loader of their own, with no boot-area compare, so they take V15 whatever the bootloader (though not open firmware).

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.5.0")),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.4.5...0.5.0

  3. 0.5.0-beta.1FM1Kit 0.5.0-beta.1Beta0 files

    A pre-release for testing with more than one FM-1 (or other M-VAVE device) connected. It has been tested only with simulated devices; use 0.4.5 otherwise.

    Added

    • Choosing a device when more than one is connected: FM1Updater.devices() lists every device that answers the handshake, and FM1Updater(transport:timing:device:) makes an updater that works with the chosen one alone. It leaves the other devices alone and follows the chosen one as it restarts into its update loader and back, on a new port. FM1DeviceSummary is Identifiable and has the port it answered on.
    • FM1Advice.Action.chooseDevice, the advice for multipleDevices, which was reconnect.
    • fm1probe devices lists the devices, numbered, and --device N|SERIAL chooses one for install, install-bootloader, restore-voices, backup and download-mode.

    Two devices in the chip's USB download mode at once are still refused.

    Using it

    Swift Package Manager doesn't pick pre-releases for a range, so pin it exactly:

    .package(url: "https://github.com/ocean/FM1Kit.git", exact: "0.5.0-beta.1"),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.4.5...0.5.0-beta.1

  4. 0.4.5FM1Kit 0.4.50 files

    FM1Kit no longer warns that SikkenWorks' firmwares refuse V15 when the FM-1's bootloader has been changed.

    Fixed

    • refusesV15OverOtherBootloaders was true for SikkenWorks' ETCH, BEETLE and B1. Their update code is on Felucca's platform with its original check, but it recognises V15 by its flash image's SHA-256 and takes it whatever the bootloader. They still refuse M-VAVE's open firmware over a changed bootloader.

    Hardware testing (stage P)

    A way out over USB-MIDI for firmwares with Felucca's original update check: Felucca 1.6.1, unable to install V15 over m-bootloader or Open V12's bootloader, took MIDIMON 0.4, which took Open V12 and V15. Any firmware on Felucca's fixed update code (MIDIMON, LOOM-8) works as that step between.

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.4.5")),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.4.4...0.4.5

  5. 0.4.4FM1Kit 0.4.40 files

    FM1Kit now tells a firmware refusing a package straight after its header apart from a stalled install, and its messages cover any changed bootloader: M-VAVE's open firmware traps firmwares with Felucca's original update check just as m-bootloader does.

    Added

    • FM1UpdateError.packageRefused(requests:): the firmware read the package's header and stopped, refusing it. Firmwares with Felucca's original update check do this for m-bootloader's installer and for update loaders they don't recognise, and copies of Felucca 1.6.1 for M-VAVE's open firmware too. These were reported as stalled(.staging), whose advice was to try again; the advice is now .needsComputer, as for bootloaderMismatch.
    • FM1Updater.installBootloader(_:fromAnyFirmware:events:) and fm1probe install-bootloader --from-any-firmware: send m-bootloader's installer whatever firmware is running, for hardware tests of how other firmwares treat it.
    • SimulatedFM1.Options.refusesAfterHeader.

    Changed

    • bootloaderMismatch and refusesV15OverOtherBootloaders describe any changed bootloader (m-bootloader, or M-VAVE's open firmware's), and point to firmwares with Felucca's updated check, such as LOOM-8, as a way back. Estampie is no longer listed among the firmwares that refuse V15.

    Fixed

    • The README's and llms-full.txt's recovery sections describe download mode as FM1Rescue uses it.

    Hardware testing (stage O)

    After Open V12, DUB-1 1.3.0 refused V15 but took Open V12 again; Felucca 1.6.1 refused both (FAILED -15, FAILED -5); LOOM-8 0.8 took Open V12 and V15. Melodee also refused V15 over m-bootloader.

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.4.4")),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.4.3...0.4.4

  6. 0.4.3FM1Kit 0.4.30 files

    Apps can now tell which firmwares really refuse M-VAVE's V15 over USB-MIDI while m-bootloader is installed. Firmwares built on the fixed update code of Felucca's platform take V15 and are no longer flagged.

    Added

    • FwscPackage.refusesV15OverOtherBootloaders: true when a firmware has Felucca's update code without Charles Vestal's fix (felucca-bsp afd83ed), so it refuses V15 over USB-MIDI once m-bootloader is installed. Firmwares on the fixed code, such as LOOM-8 0.8 and Del Rio 0.9, only check that the package's bootloader is one the FM-1 can start, and aren't flagged. Use this to warn before installing a firmware; usesFeluccaUpdateCode now just says whether a firmware is on the platform.
    • fm1probe inspect tells the two apart.
    • Hardware testing (stage M): LOOM-8 0.8 took V15 over USB-MIDI on an FM-1 with m-bootloader 1.2, where DUB-1 1.3.0 refused it.

    On the packages tried, the new property is true for Felucca 1.6.1, AMB-1 1.3, DUB-1 1.3.0, X0X 1.1.0, FoMni 0.4, Sloop 2.8.1, Melodee 1.5.0, Skiff 2.0.1 and CS-1 0.3.2, and false for LOOM-8 0.8, Del Rio 0.9, V15 and m-bootloader's installer. TransitFM1, which installs every package with its own loader, takes V15 too but is still flagged.

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.4.3")),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.4.2...0.4.3

  7. 0.4.2FM1Kit 0.4.20 files

    Apps can now tell from a package whether a firmware uses Felucca's update code, which refuses M-VAVE's V15 over USB-MIDI while m-bootloader is installed, and warn before installing it.

    Added

    • FwscPackage.usesFeluccaUpdateCode: whether a firmware package's code uses Felucca's update code (AMB-1, HYP-1, DUB-1, Estampie, LOF-1, Felucca), read from the text that code depends on. It works on iOS and macOS, and fm1probe inspect shows it. On the packages tried it's true for AMB-1 1.3, DUB-1 1.3.0, Estampie 0.1, Felucca 1.6.1 and LOF-1 0.0.77, and false for V15, Open V12 and m-bootloader's installer.
    • Reading a package's or backup's firmware code moved from FM1Rescue into FM1Kit, ported from M-Flasher's formats.rs with its MIT notice, so both libraries share it.

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.4.2")),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.4.1...0.4.2

  8. 0.4.1FM1Kit 0.4.10 files

    FM1Kit now explains why some custom firmwares refuse to install M-VAVE's V15 while m-bootloader is installed, and installs over DUB-1 again.

    Added

    • FM1UpdateError.bootloaderMismatch(requests:): firmwares built on Felucca's platform (AMB-1, HYP-1, DUB-1, Estampie) compare the boot area of a package with M-VAVE's update loader (V15, open firmware) with the FM-1's before installing it, so they refuse V15 while m-bootloader is installed. An install that stops on that comparison now says so, and that installing in download mode from a Mac keeps the FM-1's bootloader, instead of the general staging advice. SimulatedFM1.Options.comparesBootArea simulates it.

    Fixed

    • Firmwares numbering their builds FM-1_8 and six digits, as DUB-1 does (FM-1_8010300 for 1.3.0), were taken for M-VAVE's open firmware, so installing over them stopped with capabilityUnavailable. FM1Identity.isOpenFirmware now takes FM-1_8000000 to FM-1_8000999 (and FM1_COMMUNITY_).
    • FlashBackup.firmware.name names a custom firmware only when its code also holds the name and version it shows, as forks share identities: HYP-1 numbers its builds as AMB-1 does, so a HYP-1 build could be named "AMB-1 1.3".
    • FlashPlan.installing(_:onto:includingBootloader:erasingSettings:) no longer warns that the installed firmware keeps its settings elsewhere when it erases the new firmware's settings.
    • fm1probe's timestamps were on a 12-hour clock without AM or PM when run under sudo; they're always 24-hour now.

    Tested on hardware

    With m-bootloader installed, over USB-MIDI: V15 → DUB-1 1.3.0 → AMB-1 1.3 → Estampie 0.1 all installed and kept m-bootloader. AMB-1 refused Open V12 and Estampie refused V15 at the boot-area check (as HYP-1 did), and FM1Kit reported the bootloader mismatch. Open V12 ran on m-bootloader after a download-mode install, and Open V12 took V15 over MIDI, which put M-VAVE's bootloader back. With M-VAVE's bootloader, DUB-1 took V15. See HARDWARE_TESTING.md.

    In short: with m-bootloader, firmwares built on Felucca's platform switch between each other over USB-MIDI, but going back to V15 from them needs download mode on a Mac.

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.4.1")),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.4.0...0.4.1

  9. 0.4.0FM1Kit 0.4.00 files

    FM1Kit can now install m-bootloader over USB-MIDI, and its new macOS library, FM1Rescue, backs up, restores and writes the FM-1's whole flash in the chip's USB download mode, whatever firmware is installed.

    Added

    • FM1Updater.installBootloader(_:events:) installs m-bootloader from its fm1-uboot.fwsc package on an FM-1 running V15, keeping the firmware. BootloaderInstaller reads the package, decompresses its installer and checks every checksum, as the bootloader does before running it, and finds the bootloader it writes; BootloaderImage reads and checks m-bootloader's .boot files. FM1Kit can't read the bootloader back over USB-MIDI, so the FM-1's screen confirms the result.
    • FwscPackage.isFirmware: packages that hold only an installer are now read, and install, resume and check refuse them (FM1UpdateError.notFirmware).
    • InstallProgress: an install's events as one bar that never goes backwards, with four stages.
    • FM1Advice (and FM1UpdateError.advice): any error as a headline, an explanation, whether the FM-1 is still fine, one action and technical details.
    • FM1Updater.summary() and FM1DeviceSummary: the connected FM-1's firmware name and version (for V15 and open firmware), whether it's waiting mid-update, and its USB serial number (MIDIPortDescriptor.serialNumber).
    • SimulatedFM1 reports a serial number and runs bootloader installers (Options.serialNumber, bootloaderVersion; installedBootloaderVersion), and installing V15 on it puts M-VAVE's bootloader back, as on a real FM-1.
    • fm1probe install-bootloader, inspect for installers and .boot files, and serial numbers in identify.
    • A second library, FM1Rescue (macOS only; empty on iOS), for the chip's USB download mode: FM1Updater.enterDownloadMode() asks V15 or Felucca to switch with a MIDI message; USBDownloadModeConnection takes the FM-1 from macOS's disk driver (needs root) and speaks USB mass storage to it; FlashSession uploads m-bootloader's flash loader (bundled, MIT) and reads the flash; FlashSession.backUp(progress:) reads it twice and checks both reads against the chip's CRC-32; FlashBackup.write(to:) saves it as M-Flasher does. SimulatedDownloadMode stands in for the chip.
    • FlashBackup.firmware says whether the firmware's files in a backup are whole (as m-bootloader's menu reports them), which identities its code names and whether it's V15, naming Felucca 1.6.1 and AMB-1 1.3 too (name), and the backup's .json gains M-Flasher's firmware and app fields. FlashBackup(contentsOf:) reads a saved backup.
    • Writing in download mode, which works whatever the firmware: FlashPlan works out a change against the flash just read (restoring(_:onto:) a backup, installing(_:onto:) m-bootloader, installingBootloader(from:onto:) M-VAVE's bootloader from a firmware package, or installing(_:onto:includingBootloader:erasingSettings:) a firmware), listing the 4 KB sectors that change. FlashSession.write(_:progress:) checks the flash hasn't changed, erases and writes only those sectors, checks each run against the chip's CRC-32 and shows progress on the FM-1's screen. Another FM-1's backup never replaces this one's factory data, and nothing is planned for flash that shows it's another model's (FlashBackup.model; the SMK-37 has the same chip). If writing stops part-way, the error says not to switch the FM-1 off but to restore the backup. New errors: FlashPlanError, and DownloadModeError.flashChanged, writeNotVerified(_:) and writeInterrupted(at:_:). SimulatedDownloadMode erases and writes as NOR flash does.
    • fm1probe download-mode and fm1probe flash-backup FOLDER, with --trace to print every USB transfer; fm1probe inspect shows the bootloader and firmware in a .bin backup; fm1probe flash-restore, flash-bootloader and flash-install write in download mode after saving a backup and asking to confirm, or rehearse with --simulated BACKUP.bin.

    Changed

    • When an install stops before the FM-1 restarts, the error explains that a custom firmware's own update code may have refused the package, and that replacing it needs download mode.
    • fm1probe install-bootloader shows staging progress as a count of requests.

    Fixed

    • An install could wait forever when the FM-1 stopped asking for data but kept sending other SysEx: only read requests now count as activity, so it stops as stalled after the idle limit.

    Tested on hardware

    m-bootloader 1.2 installed over USB-MIDI from V15. Then in download mode: a verified 1 MB backup; Felucca 1.6.1 installed over AMB-1 (125 sectors in 8.6 s); a backup of V15 restored over Felucca; M-VAVE's bootloader put back from V15's package and m-bootloader installed again. The final flash was byte for byte the same as the backup from before. See HARDWARE_TESTING.md.

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.4.0")),
    

    FM1Rescue needs root to take the FM-1 from macOS's disk driver, so apps reach it through a privileged helper.

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.3.0...0.4.0

  10. 0.3.0FM1Kit 0.3.00 files

    FM1Kit can now back up an FM-1 running M-VAVE's open firmware before it's switched to another firmware.

    Added

    • FM1Updater.backUpOpenFirmware(progress:) backs up open firmware's 16 project slots, the project being edited (with unsaved changes) and its sample bank, through the firmware's project library. Open firmware can only read the project being edited, so each slot is loaded in turn and the workspace is put back afterwards; no slot is saved and the sample bank isn't written.
    • OpenFirmwareBackup.write(to:) saves the backup as the .fm1 project and .fm1s sample files M-VAVE's workbench restores, with a backup.json describing them. Open V13's per-slot tuning tables aren't included yet.
    • fm1probe backup FOLDER (with --simulated and --log FILE).
    • SimulatedFM1 answers the project library while running open firmware (Options.projectSlots, workspace, currentSlot, sampleBank).
    • llms.txt and llms-full.txt describe the package for language models.

    Fixed

    • SimulatedFM1 could ignore the upgrade command from a host that found its loader very quickly after staging, which made an open firmware test fail now and then.

    Tested on hardware

    V15 → Open V12, then a backup that kept the unsaved tempo of the project being edited; M-VAVE's workbench read the backed-up project and wrote it back to the FM-1. Then V15 over Open V12 in one go, without --resume (24 s), and V15's factory voices restored. See HARDWARE_TESTING.md.

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.3.0")),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.2.0...0.3.0

  11. 0.2.0FM1Kit 0.2.00 files

    Custom firmwares can now be reinstalled, or updated to another release that keeps the same identity.

    Changed

    • Installing the identity a custom firmware already reports is no longer refused. Felucca's update code, which Estampie inherits, doesn't compare identities, and firmwares that keep one identity across releases are updated this way. The new InstallKind.sameIdentity needs confirming with InstallOptions.confirmsSameIdentity (otherwise FM1UpdateError.sameIdentityNotConfirmed).
    • Only V15 over itself is still refused as reinstall (alreadyInstalled), and open firmware still compares builds.
    • SimulatedFM1.Options.refusesRunningFirmware now applies only to V15.

    Source-breaking: the new InstallKind case means exhaustive switches over it need updating.

    Tested on hardware

    Estampie 0.1 (FM-1_378001) installed over V15 (a firmware switch, 37 s), then over itself (sameIdentity, 31 s). See HARDWARE_TESTING.md.

    Using it

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.2.0")),
    

    Full changelog: https://github.com/ocean/FM1Kit/compare/0.1.0...0.2.0

  12. 0.1.0FM1Kit 0.1.00 files

    FM1Kit's first release: a Swift package for installing firmware on the M-VAVE FM-1 over USB-MIDI, from iOS and macOS.

    .package(url: "https://github.com/ocean/FM1Kit.git", .upToNextMinor(from: "0.1.0"))
    

    Tested on one FM-1 from a Mac: installing M-VAVE's official V15, Felucca 1.6.1 and M-VAVE's open firmware V12 and switching between them, pulling the cable part-way through writing and resuming, and restoring V15's factory voices. See HARDWARE_TESTING.md for the results. Reports from other devices and hosts are very welcome.

    FM1Kit isn't affiliated with M-VAVE, and doesn't include M-VAVE's firmware. Installing custom firmware is at your own risk.

    Added

    • FwscPackage: reads .fwsc firmware packages, their product identity, logical image and SHA-256.
    • OfficialFirmware: recognises M-VAVE's official releases (V15, and Open V12) by size and checksum.
    • FM1Identity: parses what a device or package reports, including the ota- update loader.
    • FM1Updater: installs a package over USB-MIDI, resumes an interrupted update or a device in USB recovery mode, and checks the device reports the new firmware afterwards.
    • CoreMIDITransport for real devices on iOS and macOS, and SimulatedFM1 for the iOS Simulator and tests.
    • fm1probe, a Mac command-line tool that lists ports, identifies a connected FM-1 (repeatedly, with timings), monitors SysEx, watches devices come and go, inspects packages, and installs a firmware after asking to confirm (or rehearses one against SimulatedFM1).
    • Installs and resumes support task cancellation.
    • FM1Identity is Codable (as its text) and can be made from a model and version.
    • SimulatedFM1 refuses a package with the identity it's running, as the real device does.
    • UpdateEvent.checking(fraction:requests:) reports a loader reading the image a second time after writing, as Felucca's loader does. UpdateEvent.writing's fraction is the share of the firmware's application area read so far, in any order (V15's loader reads backwards), so both loaders seen reach 1. SimulatedFM1.Options can read backwards (writesBackwards) and check after writing (checksAfterWriting), as these loaders do.
    • FwscPackage checks the JieLi container inside a package before anything is sent: the header and table checksums, the chip (AC791N), each part's checksum, and that there's one firmware image and one update loader. New Failure cases say what's wrong (tooLarge, damagedHeader, unexpectedChip, damagedPart, missingPart). FwscPackage.loaderSHA256 identifies the loader a package brings, and OfficialFirmware.stockLoaderSHA256 is M-VAVE's. fm1probe inspect shows both.
    • FM1UpdateError.unexpectedFinish(phase:): the update stops if the device asks to finish the other half of the update.
    • M-VAVE's open firmware (FM-1_8000012 and builds of it, FM1Identity.isOpenFirmware): the updater asks it for its FM1Capability and arms it with the package's FM1BuildID before the upgrade command, and checks the build it reports afterwards. New errors: capabilityUnavailable, deviceBusy, armRefused(code:) and buildMismatch. OfficialFirmware gains buildID and recognises Open V12 (openV12). SimulatedFM1 simulates open firmware. Tested on one FM-1: installing Open V12 over V15, and arming it to install V15 back.
    • FM1Updater.check(_:buildID:) says what an install would do (InstallKind: an update, a switch to a different firmware, or a reinstall) without changing the device. install(_:options:events:) takes InstallOptions; switching firmware, which can erase projects and samples, throws switchNotConfirmed unless confirmsSwitchingFirmware is set, and reinstalling the running firmware throws alreadyInstalled before anything is sent.
    • Installs and resumes refuse to start while more than one FM-1 answers (multipleDevices). FM1Updater.findAll(where:) lists every device that answers.
    • The updater also probes ports named after other firmwares' loaders: SLOOP, Clipper and SINCO.
    • @_spi(Testing) FwscPackage.sampleFile(product:flashSize:loaderSize:seed:) builds a small valid package for apps' tests with SimulatedFM1. Never install one on a real FM-1.
    • FM1Identity reads the identity an armed open firmware's update loader reports, such as ota-FM1C_db1642b2b6fa5c2c (armedBuildPrefix), so the updater finds that loader; an install checks it names the build it was armed with.
    • FM1Updater.restoreFactoryVoices(from:progress:) rewrites V15's four voice banks with its 128 factory voices from the official V15 package, as M-VAVE's updater does after installing V15 (FwscPackage.hasFactoryVoices, FM1Identity.officialV15). New errors: factoryVoicesUnavailable, voiceRestoreNeedsV15, voiceRestoreRefused and voiceRestoreStalled. SimulatedFM1 simulates V15's voice banks (currentVoices), and fm1probe restore-voices runs a restore after asking to confirm. Tested on one FM-1.
    • Updates add no pauses, as M-VAVE's own updater doesn't: the device asks for each block when it's ready (UpdaterTiming.responseDelay and afterUpgradeCommand are zero by default).
    • After answering a finish request, the updater keeps the port open for UpdaterTiming.finishSettle (600 ms), answering any repeat, so the reply isn't cut off.
    • fm1probe install shows what the install would do and warns before switching firmware, and takes --build-id; fm1probe identify warns when more than one FM-1 answers.