diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83bf2bc..a84900d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,65 +13,68 @@ on: workflow_dispatch: jobs: - build-fw-hw-sw: - runs-on: ubuntu-latest + # build-fw-hw-sw: + # runs-on: ubuntu-latest - steps: - - name: Download SummerCart64 repository - uses: actions/checkout@v3 + # steps: + # - name: Download SummerCart64 repository + # uses: actions/checkout@v3 - - name: Set SC64 version - uses: frabert/replace-string-action@v2 - id: sc64version - with: - pattern: '\/' - string: '-${{ github.ref_name }}' - replace-with: '-' + # - name: Set SC64 version + # uses: frabert/replace-string-action@v2 + # id: sc64version + # with: + # pattern: '\/' + # string: '-${{ github.ref_name }}' + # replace-with: '-' - - name: Build everything - run: ./docker_build.sh release --force-clean - env: - SC64_VERSION: ${{ steps.sc64version.outputs.replaced }} + # - name: Build everything + # run: ./docker_build.sh release --force-clean + # env: + # SC64_VERSION: ${{ steps.sc64version.outputs.replaced }} - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: sc64-pkg${{ steps.sc64version.outputs.replaced }} - path: | - sc64-extra${{ steps.sc64version.outputs.replaced }}.zip - sc64-firmware${{ steps.sc64version.outputs.replaced }}.bin + # - name: Upload artifact + # uses: actions/upload-artifact@v3 + # with: + # name: sc64-pkg${{ steps.sc64version.outputs.replaced }} + # path: | + # sc64-extra${{ steps.sc64version.outputs.replaced }}.zip + # sc64-firmware${{ steps.sc64version.outputs.replaced }}.bin - - name: Upload release assets - if: github.event_name == 'release' && github.event.action == 'created' - uses: softprops/action-gh-release@v0.1.15 - with: - files: | - sc64-extra${{ steps.sc64version.outputs.replaced }}.zip - sc64-firmware${{ steps.sc64version.outputs.replaced }}.bin + # - name: Upload release assets + # if: github.event_name == 'release' && github.event.action == 'created' + # uses: softprops/action-gh-release@v0.1.15 + # with: + # files: | + # sc64-extra${{ steps.sc64version.outputs.replaced }}.zip + # sc64-firmware${{ steps.sc64version.outputs.replaced }}.bin - build-sc64-py: + build-deployer: strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] include: - os: windows-latest - pyinstaller-build-options: --target-arch=64bit - pyinstaller-options: --onefile --console --icon ../../assets/sc64_logo_256_256.png - package-name: sc64-windows - package-options: -c -a -f - package-extension: zip + # pyinstaller-build-options: --target-arch=64bit + # pyinstaller-options: --onefile --console --icon ../../assets/sc64_logo_256_256.png + executable-name: sc64deployer.exe + package-name: sc64deployer-windows + # package-options: -c -a -f + # package-extension: zip - os: ubuntu-latest - pyinstaller-options: --onefile - package-name: sc64-linux - package-options: -czf - package-extension: tar.gz + # pyinstaller-options: --onefile + executable-name: sc64deployer + package-name: sc64deployer-linux + # package-options: -czf + # package-extension: tar.gz - os: macos-latest - pyinstaller-options: --onedir --console --icon ../../assets/sc64_logo_256_256.png - package-name: sc64-macos - package-options: -czf - package-extension: tgz + # pyinstaller-options: --onedir --console --icon ../../assets/sc64_logo_256_256.png + executable-name: sc64deployer + package-name: sc64deployer-macos + # package-options: -czf + # package-extension: tgz runs-on: ${{ matrix.os }} @@ -87,53 +90,58 @@ jobs: string: '-${{ github.ref_name }}' replace-with: '-' - - name: Setup python - uses: actions/setup-python@v4 - with: - python-version: '3.11' + - name: Build deployer + run: cargo b -r + working-directory: sw/deployer - - name: Download pyinstaller repository - uses: actions/checkout@v3 - with: - repository: 'pyinstaller/pyinstaller' - ref: 'v5.8.0' - path: pyinstaller + # - name: Setup python + # uses: actions/setup-python@v4 + # with: + # python-version: '3.11' - - name: Compile and install pyinstaller - run: | - pip3 uninstall pyinstaller - pip3 install wheel - pushd bootloader - python3 ./waf all ${{ matrix.pyinstaller-build-options }} - popd - pip3 install . - working-directory: pyinstaller + # - name: Download pyinstaller repository + # uses: actions/checkout@v3 + # with: + # repository: 'pyinstaller/pyinstaller' + # ref: 'v5.8.0' + # path: pyinstaller - - name: Install sc64.py requirements - run: pip3 install -r requirements.txt - working-directory: sw/pc + # - name: Compile and install pyinstaller + # run: | + # pip3 uninstall pyinstaller + # pip3 install wheel + # pushd bootloader + # python3 ./waf all ${{ matrix.pyinstaller-build-options }} + # popd + # pip3 install . + # working-directory: pyinstaller - - name: Create sc64.py executable - run: python3 -m PyInstaller --clean ${{ matrix.pyinstaller-options }} sc64.py - working-directory: sw/pc + # - name: Install sc64.py requirements + # run: pip3 install -r requirements.txt + # working-directory: sw/pc - - name: Package executable - run: | - mkdir package - pushd dist - tar ${{ matrix.package-options }} ../package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }} * - popd - working-directory: sw/pc + # - name: Create sc64.py executable + # run: python3 -m PyInstaller --clean ${{ matrix.pyinstaller-options }} sc64.py + # working-directory: sw/pc + + # - name: Package executable + # run: | + # mkdir package + # pushd dist + # tar ${{ matrix.package-options }} ../package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }} * + # popd + # working-directory: sw/pc - name: Upload artifact uses: actions/upload-artifact@v3 with: name: ${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }} - path: sw/pc/package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }} + path: sw/deployer/target/release/${{ matrix.executable-name }} + # path: sw/pc/package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }} - - name: Upload release assets - if: github.event_name == 'release' && github.event.action == 'created' - uses: softprops/action-gh-release@v0.1.15 - with: - files: | - sw/pc/package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }} + # - name: Upload release assets + # if: github.event_name == 'release' && github.event.action == 'created' + # uses: softprops/action-gh-release@v0.1.15 + # with: + # files: | + # sw/pc/package/${{ matrix.package-name }}${{ steps.sc64version.outputs.replaced }}.${{ matrix.package-extension }} diff --git a/sw/deployer/.gitignore b/sw/deployer/.gitignore new file mode 100644 index 0000000..910096e --- /dev/null +++ b/sw/deployer/.gitignore @@ -0,0 +1,8 @@ +/target +*.bin +*.eep +*.fla +*.n64 +*.srm +*.v64 +*.z64 diff --git a/sw/deployer/Cargo.lock b/sw/deployer/Cargo.lock new file mode 100644 index 0000000..0cddc35 --- /dev/null +++ b/sw/deployer/Cargo.lock @@ -0,0 +1,803 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "CoreFoundation-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e9889e6db118d49d88d84728d0e964d973a5680befb5f85f55141beea5c20b" +dependencies = [ + "libc", + "mach", +] + +[[package]] +name = "IOKit-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99696c398cbaf669d2368076bdb3d627fb0ce51a26899d7c61228c5c0af3bf4a" +dependencies = [ + "CoreFoundation-sys", + "libc", + "mach", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "4.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap-num" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488557e97528174edaa2ee268b23a809e0c598213a4bbcb4f34575a46fda147e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "clap_derive" +version = "4.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ctrlc" +version = "3.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" +dependencies = [ + "nix", + "windows-sys", +] + +[[package]] +name = "cxx" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mach" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd13ee2dd61cc82833ba05ade5a30bb3d63f7ced605ef827063c63078302de9" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "panic-message" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384e52fd8fbd4cbe3c317e8216260c21a0f9134de108cea8a4dd4e7e152c472d" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "sc64loader" +version = "2.12.2" +dependencies = [ + "chrono", + "clap", + "clap-num", + "colored", + "crc32fast", + "ctrlc", + "encoding_rs", + "panic-message", + "serialport", +] + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "serialport" +version = "4.2.1-alpha.0" +source = "git+https://github.com/serialport/serialport-rs?branch=main#e1f46eef5af7df2430f0a595681243e46f721b2a" +dependencies = [ + "CoreFoundation-sys", + "IOKit-sys", + "bitflags", + "cfg-if", + "libudev", + "mach2", + "nix", + "regex", + "winapi", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/sw/deployer/Cargo.toml b/sw/deployer/Cargo.toml new file mode 100644 index 0000000..01115be --- /dev/null +++ b/sw/deployer/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sc64deployer" +version = "2.12.2" +edition = "2021" +authors = ["Polprzewodnikowy"] +description = "SC64 loader and control software" +documentation = "https://github.com/Polprzewodnikowy/SummerCart64" + +[dependencies] +chrono = "0.4.23" +clap = { version = "4.1.6", features = ["derive"] } +clap-num = "1.0.2" +colored = "2.0.0" +crc32fast = "1.3.2" +ctrlc = "3.2.5" +encoding_rs = "0.8.32" +panic-message = "0.3.0" +serialport = { git = "https://github.com/serialport/serialport-rs", branch = "main" } diff --git a/sw/deployer/src/debug.rs b/sw/deployer/src/debug.rs new file mode 100644 index 0000000..7caf292 --- /dev/null +++ b/sw/deployer/src/debug.rs @@ -0,0 +1,27 @@ +use crate::sc64::DebugPacket; + +pub fn handle_debug_packet(debug_packet: DebugPacket) { + let DebugPacket { datatype, data } = debug_packet; + match datatype { + 0x01 => handle_datatype_text(&data), + // 0x02 => handle_datatype_raw_binary(&data), + // 0x03 => handle_datatype_header(&data), + // 0x04 => handle_datatype_screenshot(&data), + // 0xDB => handle_datatype_gdb(&data), + _ => {} + } +} + +fn handle_datatype_text(data: &[u8]) { + if let Ok(message) = std::str::from_utf8(data) { + print!("{message}"); + } +} + +// fn handle_datatype_raw_binary(data: &[u8]) {} + +// fn handle_datatype_header(data: &[u8]) {} + +// fn handle_datatype_screenshot(data: &[u8]) {} + +// fn handle_datatype_gdb(data: &[u8]) {} diff --git a/sw/deployer/src/main.rs b/sw/deployer/src/main.rs new file mode 100644 index 0000000..2e3175b --- /dev/null +++ b/sw/deployer/src/main.rs @@ -0,0 +1,524 @@ +mod debug; +mod n64; +mod sc64; + +use chrono::Local; +use clap::{Args, Parser, Subcommand, ValueEnum}; +use clap_num::maybe_hex_range; +use colored::Colorize; +use debug::handle_debug_packet; +use panic_message::panic_message; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use std::{panic, process, thread}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, + + /// Use SC64 device matching provided serial number + #[arg(long)] + sn: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// List connected SC64 devices + List, + + /// Upload ROM (and save) to the SC64 + Upload(UploadArgs), + + /// Upload ROM, 64DD IPL and run disk server + _64DD(_64DDArgs), + + /// Enter debug mode + Debug(DebugArgs), + + /// Dump data from arbitrary location in SC64 memory space + Dump(DumpArgs), + + /// Print information about SC64 device + Info, + + /// Update persistent settings on SC64 device + Set { + #[command(subcommand)] + command: SetCommands, + }, + + /// Print firmware metadata / update or backup SC64 firmware + Firmware { + #[command(subcommand)] + command: FirmwareCommands, + }, +} + +#[derive(Args)] +struct UploadArgs { + /// Path to the ROM file + rom: PathBuf, + + /// Path to the save file + #[arg(short, long)] + save: Option, + + /// Override autodetected save type + #[arg(short = 't', long)] + save_type: Option, + + /// Use direct boot mode (skip bootloader) + #[arg(short, long)] + direct: bool, + + /// Do not put last 128 kiB of ROM inside flash memory (can corrupt non EEPROM saves) + #[arg(short, long)] + no_shadow: bool, + + /// Force TV type (ignored when used in conjunction with direct boot mode) + #[arg(long)] + tv: Option, +} + +#[derive(Args)] +struct _64DDArgs { + /// Path to the ROM file + #[arg(short, long)] + rom: Option, + + /// Path to the 64DD IPL file + ddipl: PathBuf, + + /// Path to the 64DD disk file (.ndd format, can be specified multiple times) + disk: Vec, + + /// Use direct boot mode (skip bootloader) + #[arg(short, long)] + direct: bool, +} + +#[derive(Args)] +struct DebugArgs { + /// Enable IS-Viewer64 and set listening address at ROM offset (in most cases it's fixed at 0x03FF0000) + #[arg(long, value_name = "offset", value_parser = |s: &str| maybe_hex_range::(s, 0x00000004, 0x03FF0000))] + isv: Option, + + /// Expose TCP socket port for GDB debugging + #[arg(long, value_name = "port", value_parser = clap::value_parser!(u16).range(1..))] + gdb: Option, +} + +#[derive(Args)] +struct DumpArgs { + /// Starting memory address + #[arg(value_parser = |s: &str| maybe_hex_range::(s, 0, sc64::MEMORY_LENGTH as u32))] + address: u32, + + /// Dump length + #[arg(value_parser = |s: &str| maybe_hex_range::(s, 1, sc64::MEMORY_LENGTH))] + length: usize, + + /// Path to the dump file + path: PathBuf, +} + +#[derive(Subcommand)] +enum SetCommands { + /// Synchronize real time clock (RTC) on the SC64 with local system time + Rtc, + + /// Enable LED I/O activity blinking + BlinkOn, + + /// Disable LED I/O activity blinking + BlinkOff, +} + +#[derive(Subcommand)] +enum FirmwareCommands { + /// Print metadata included inside SC64 firmware file + Info(FirmwareArgs), + + /// Download current SC64 firmware and save it to provided file + Backup(FirmwareArgs), + + /// Update SC64 firmware from provided file + Update(FirmwareArgs), +} + +#[derive(Args)] +struct FirmwareArgs { + /// Path to the firmware file + firmware: PathBuf, +} + +#[derive(Clone, Debug, ValueEnum)] +enum SaveType { + None, + Eeprom4k, + Eeprom16k, + Sram, + SramBanked, + Flashram, +} + +#[derive(Clone, Debug, ValueEnum)] +enum TvType { + PAL, + NTSC, + MPAL, +} + +fn main() { + let cli = Cli::parse(); + + // panic::set_hook(Box::new(|_| {})); + + match panic::catch_unwind(|| handle_command(&cli.command, cli.sn)) { + Ok(_) => {} + Err(payload) => { + eprintln!("{}", panic_message(&payload).red()); + process::exit(1); + } + } +} + +fn init_sc64(sn: Option, check_firmware: bool) -> Result { + let mut sc64 = sc64::new(sn)?; + + if check_firmware { + sc64.check_firmware_version()?; + } + + Ok(sc64) +} + +fn setup_exit_flag() -> Arc { + let exit_flag = Arc::new(AtomicBool::new(false)); + let handler_exit_flag = exit_flag.clone(); + + ctrlc::set_handler(move || { + handler_exit_flag.store(true, Ordering::Relaxed); + }) + .unwrap(); + + exit_flag +} + +fn handle_command(command: &Commands, sn: Option) { + let result = match command { + Commands::Upload(args) => handle_upload_command(sn, args), + Commands::_64DD(args) => handle_64dd_command(sn, args), + Commands::Dump(args) => handle_dump_command(sn, args), + Commands::Debug(args) => handle_debug_command(sn, args), + Commands::Info => handle_info_command(sn), + Commands::Set { command } => handle_set_command(sn, command), + Commands::Firmware { command } => handle_firmware_command(sn, command), + Commands::List => handle_list_command(), + }; + match result { + Ok(()) => {} + Err(error) => panic!("{error}"), + }; +} + +fn handle_upload_command(sn: Option, args: &UploadArgs) -> Result<(), sc64::Error> { + let mut sc64 = init_sc64(sn, true)?; + + sc64.reset_state()?; + + let rom_path = args.rom.to_str().unwrap(); + + print!( + "Uploading ROM [{}]... ", + args.rom.file_name().unwrap().to_str().unwrap() + ); + std::io::stdout().flush().unwrap(); + sc64.upload_rom(rom_path, args.no_shadow)?; + println!("done"); + + // TODO: autodetect save + + let args_save_type = args.save_type.as_ref().unwrap_or(&SaveType::None); + let save_type = match args_save_type { + SaveType::None => sc64::SaveType::None, + SaveType::Eeprom4k => sc64::SaveType::Eeprom4k, + SaveType::Eeprom16k => sc64::SaveType::Eeprom16k, + SaveType::Sram => sc64::SaveType::Sram, + SaveType::SramBanked => sc64::SaveType::SramBanked, + SaveType::Flashram => sc64::SaveType::Flashram, + }; + sc64.set_save_type(save_type)?; + + if args.save.is_some() { + let save = args.save.as_ref().unwrap(); + + print!( + "Uploading save [{}]... ", + save.file_name().unwrap().to_str().unwrap() + ); + std::io::stdout().flush().unwrap(); + sc64.upload_save(save.to_str().unwrap())?; + println!("done"); + } + + println!("Save type set to [{args_save_type:?}]"); + + let boot_mode = if args.direct { + sc64::BootMode::DirectRom + } else { + sc64::BootMode::Rom + }; + + sc64.set_boot_mode(boot_mode)?; + println!("Boot mode set to [{:?}]", boot_mode); + + if let Some(tv) = args.tv.as_ref() { + if args.direct { + println!("TV type ignored because direct boot mode is enabled"); + } else { + sc64.set_tv_type(match tv { + TvType::PAL => sc64::TvType::PAL, + TvType::NTSC => sc64::TvType::NTSC, + TvType::MPAL => sc64::TvType::MPAL, + })?; + println!("TV type set to [{tv:?}]"); + } + } + + sc64.calculate_cic_parameters()?; + + Ok(()) +} + +fn handle_64dd_command(sn: Option, args: &_64DDArgs) -> Result<(), sc64::Error> { + let mut sc64 = init_sc64(sn, true)?; + + // TODO: parse 64DD disk files + + sc64.reset_state()?; + + let ddipl_path = args.ddipl.to_str().unwrap(); + + print!( + "Uploading DDIPL [{}]... ", + args.ddipl.file_name().unwrap().to_str().unwrap() + ); + std::io::stdout().flush().unwrap(); + sc64.upload_ddipl(ddipl_path)?; + println!("done"); + + // TODO: upload other stuff + + // TODO: set boot mode + + sc64.calculate_cic_parameters()?; + + let exit = setup_exit_flag(); + while exit.load(Ordering::Relaxed) { + if let Some(data_packet) = sc64.receive_data_packet()? { + match data_packet { + sc64::DataPacket::Disk(_disk_packet) => { + // TODO: handle 64DD packet + } + _ => {} + } + } else { + thread::sleep(Duration::from_micros(1)); + } + } + + Ok(()) +} + +fn handle_dump_command(sn: Option, args: &DumpArgs) -> Result<(), sc64::Error> { + let dump_path = &args.path; + + let mut file = std::fs::File::create(dump_path)?; + + let mut sc64 = init_sc64(sn, true)?; + + print!( + "Dumping from [0x{:08X}] length [0x{:X}] to [{}]... ", + args.address, + args.length, + dump_path.file_name().unwrap().to_str().unwrap() + ); + std::io::stdout().flush().unwrap(); + let data = sc64.dump_memory(args.address, args.length)?; + + file.write(&data)?; + + println!("done"); + + Ok(()) +} + +fn handle_debug_command(sn: Option, args: &DebugArgs) -> Result<(), sc64::Error> { + let mut sc64 = init_sc64(sn, true)?; + + if args.isv.is_some() { + sc64.configure_isviewer64(args.isv)?; + } + + println!("{}", "Debug mode started".bold()); + + let exit = setup_exit_flag(); + while !exit.load(Ordering::Relaxed) { + if let Some(data_packet) = sc64.receive_data_packet()? { + match data_packet { + sc64::DataPacket::IsViewer(message) => print!("{}", message), + sc64::DataPacket::Debug(debug_packet) => handle_debug_packet(debug_packet), + _ => {} + } + } else { + thread::sleep(Duration::from_micros(1)); + } + } + + println!("{}", "Debug mode ended".bold()); + + if args.isv.is_some() { + sc64.configure_isviewer64(None)?; + } + + Ok(()) +} + +fn handle_info_command(sn: Option) -> Result<(), sc64::Error> { + let mut sc64 = init_sc64(sn, false)?; + + let (major, minor) = sc64.check_firmware_version()?; + let state = sc64.get_device_state()?; + let datetime = state.datetime.format("%Y-%m-%d %H:%M:%S %Z"); + + println!("{}", "SC64 information and current state:".bold()); + println!(" Firmware version: {major}.{minor}"); + println!(" RTC datetime: {}", datetime); + println!(" LED blink enabled: {}", state.led_enable); + println!(" Bootloader switch: {}", state.bootloader_switch); + println!(" ROM write enabled: {}", state.rom_write_enable); + println!(" ROM shadow enabled: {}", state.rom_shadow_enable); + println!(" ROM extended enabled: {}", state.rom_extended_enable); + println!(" Boot mode: {:?}", state.boot_mode); + println!(" Save type: {:?}", state.save_type); + println!(" CIC seed: {:?}", state.cic_seed); + println!(" TV type: {:?}", state.tv_type); + println!(" 64DD mode: {:?}", state.dd_mode); + println!(" 64DD SD card mode: {}", state.dd_sd_enable); + println!(" 64DD drive type: {:?}", state.dd_drive_type); + println!(" 64DD disk state: {:?}", state.dd_disk_state); + println!(" Button mode: {:?}", state.button_mode); + println!(" Button state: {}", state.button_state); + println!(" IS-Viewer 64 offset: 0x{:08X}", state.isv_address); + + Ok(()) +} + +fn handle_set_command(sn: Option, command: &SetCommands) -> Result<(), sc64::Error> { + let mut sc64 = init_sc64(sn, true)?; + + match command { + SetCommands::Rtc => { + let datetime = Local::now(); + sc64.set_datetime(datetime)?; + println!( + "SC64 RTC datetime synchronized to: {}", + datetime.format("%Y-%m-%d %H:%M:%S %Z").to_string().green() + ); + } + + SetCommands::BlinkOn => { + sc64.set_led_blink(true)?; + println!( + "SC64 LED I/O activity blinking set to {}", + "enabled".green() + ); + } + + SetCommands::BlinkOff => { + sc64.set_led_blink(false)?; + println!("SC64 LED I/O activity blinking set to {}", "disabled".red()); + } + } + + Ok(()) +} + +fn handle_firmware_command( + sn: Option, + command: &FirmwareCommands, +) -> Result<(), sc64::Error> { + match command { + FirmwareCommands::Info(args) => { + let firmware_path = &args.firmware; + + let mut file = std::fs::File::open(firmware_path)?; + let length = file.metadata()?.len(); + let mut buffer = vec![0u8; length as usize]; + file.read_exact(&mut buffer)?; + + // TODO: print firmware metadata + + Ok(()) + } + + FirmwareCommands::Backup(args) => { + let backup_path = &args.firmware; + + let mut file = std::fs::File::create(backup_path)?; + + let mut sc64 = init_sc64(sn, false)?; + + print!( + "Generating firmware backup, this might take a while [{}]... ", + backup_path.file_name().unwrap().to_str().unwrap() + ); + std::io::stdout().flush().unwrap(); + let data = sc64.backup_firmware()?; + file.write(&data)?; + println!("done"); + + Ok(()) + } + + FirmwareCommands::Update(args) => { + let update_path = &args.firmware; + + let mut file = std::fs::File::open(update_path)?; + let length = file.metadata()?.len(); + let mut buffer = vec![0u8; length as usize]; + file.read_exact(&mut buffer)?; + + // TODO: print firmware metadata + + let mut sc64 = init_sc64(sn, false)?; + + print!( + "Updating firmware, this might take a while [{}]... ", + update_path.file_name().unwrap().to_str().unwrap() + ); + std::io::stdout().flush().unwrap(); + sc64.update_firmware(&buffer)?; + println!("done"); + + Ok(()) + } + } +} + +fn handle_list_command() -> Result<(), sc64::Error> { + let devices = sc64::list_serial_devices()?; + + println!("{}", "Found devices:".bold()); + for (i, d) in devices.iter().enumerate() { + println!(" {i}: {}", d.sn); + } + + Ok(()) +} diff --git a/sw/deployer/src/n64.rs b/sw/deployer/src/n64.rs new file mode 100644 index 0000000..e69de29 diff --git a/sw/deployer/src/sc64/cic.rs b/sw/deployer/src/sc64/cic.rs new file mode 100644 index 0000000..b5a3a41 --- /dev/null +++ b/sw/deployer/src/sc64/cic.rs @@ -0,0 +1,180 @@ +use super::Error; +use crc32fast::Hasher; + +pub const IPL3_OFFSET: u32 = 0x40; +pub const IPL3_LENGTH: usize = 0xFC0; + +pub fn guess_ipl3_seed(ipl3: &[u8]) -> Result { + if ipl3.len() < IPL3_LENGTH { + return Err(Error::new("Invalid IPL3 length provided")); + } + + let mut hasher = Hasher::new(); + hasher.update(ipl3); + Ok(match hasher.finalize() { + 0x587BD543 => 0xAC, // 5101 + 0x6170A4A1 => 0x3F, // 6101 + 0x009E9EA3 => 0x3F, // 7102 + 0x90BB6CB5 => 0x3F, // 6102/7101 + 0x0B050EE0 => 0x78, // x103 + 0x98BC2C86 => 0x91, // x105 + 0xACC8580A => 0x85, // x106 + 0x0E018159 => 0xDD, // 5167 + 0x10C68B18 => 0xDD, // NDXJ0 + 0xBC605D0A => 0xDD, // NDDJ0 + 0x502C4466 => 0xDD, // NDDJ1 + 0x0C965795 => 0xDD, // NDDJ2 + 0x8FEBA21E => 0xDE, // NDDE0 + _ => 0x3F, + }) +} + +pub fn calculate_ipl3_checksum(ipl3: &[u8], seed: u8) -> Result<[u8; 6], Error> { + if ipl3.len() < IPL3_LENGTH { + return Err(Error::new("Invalid IPL3 length provided")); + } + + const MAGIC: u32 = 0x6C078965; + + let get = |offset: u32| { + let o: usize = offset as usize * 4; + return ((ipl3[o] as u32) << 24) + | ((ipl3[o + 1] as u32) << 16) + | ((ipl3[o + 2] as u32) << 8) + | (ipl3[o + 3] as u32); + }; + + let add = |a1: u32, a2: u32| u32::wrapping_add(a1, a2); + let sub = |a1: u32, a2: u32| u32::wrapping_sub(a1, a2); + let mul = |a1: u32, a2: u32| u32::wrapping_mul(a1, a2); + let lsh = |a: u32, s: u32| if s >= 32 { 0 } else { u32::wrapping_shl(a, s) }; + let rsh = |a: u32, s: u32| if s >= 32 { 0 } else { u32::wrapping_shr(a, s) }; + let checksum = |a0: u32, a1: u32, a2: u32| { + let prod = (a0 as u64).wrapping_mul(if a1 == 0 { a2 as u64 } else { a1 as u64 }); + let hi = ((prod >> 32) & 0xFFFFFFFF) as u32; + let lo = (prod & 0xFFFFFFFF) as u32; + let diff = hi.wrapping_sub(lo); + return if diff == 0 { a0 } else { diff }; + }; + + let init = add(mul(MAGIC, seed as u32), 1) ^ get(0); + + let mut buffer = vec![init; 16]; + + for i in 1..=1008 as u32 { + let data_prev = get(i.saturating_sub(2)); + let data_curr = get(i - 1); + + buffer[0] = add(buffer[0], checksum(sub(1007, i), data_curr, i)); + buffer[1] = checksum(buffer[1], data_curr, i); + buffer[2] = buffer[2] ^ data_curr; + buffer[3] = add(buffer[3], checksum(add(data_curr, 5), MAGIC, i)); + + let shift = data_prev & 0x1F; + let data_left = lsh(data_curr, 32 - shift); + let data_right = rsh(data_curr, shift); + let b4_shifted = data_left | data_right; + buffer[4] = add(buffer[4], b4_shifted); + + let shift = rsh(data_prev, 27); + let data_left = lsh(data_curr, shift); + let data_right = rsh(data_curr, 32 - shift); + let b5_shifted = data_left | data_right; + buffer[5] = add(buffer[5], b5_shifted); + + if data_curr < buffer[6] { + buffer[6] = add(buffer[3], buffer[6]) ^ add(data_curr, i); + } else { + buffer[6] = add(buffer[4], data_curr) ^ buffer[6]; + } + + let shift = data_prev & 0x1F; + let data_left = lsh(data_curr, shift); + let data_right = rsh(data_curr, 32 - shift); + buffer[7] = checksum(buffer[7], data_left | data_right, i); + + let shift = rsh(data_prev, 27); + let data_left = lsh(data_curr, 32 - shift); + let data_right = rsh(data_curr, shift); + buffer[8] = checksum(buffer[8], data_left | data_right, i); + + if data_prev < data_curr { + buffer[9] = checksum(buffer[9], data_curr, i) + } else { + buffer[9] = add(buffer[9], data_curr); + } + + if i == 1008 { + break; + } + + let data_next = get(i); + + buffer[10] = checksum(add(buffer[10], data_curr), data_next, i); + buffer[11] = checksum(buffer[11] ^ data_curr, data_next, i); + buffer[12] = add(buffer[12], buffer[8] ^ data_curr); + + let shift = data_curr & 0x1F; + let data_left = lsh(data_curr, 32 - shift); + let data_right = rsh(data_curr, shift); + let tmp = data_left | data_right; + let shift = data_next & 0x1F; + let data_left = lsh(data_next, 32 - shift); + let data_right = rsh(data_next, shift); + buffer[13] = add(buffer[13], add(tmp, data_left | data_right)); + + let shift = data_curr & 0x1F; + let data_left = lsh(data_next, 32 - shift); + let data_right = rsh(data_next, shift); + let sum = checksum(buffer[14], b4_shifted, i); + buffer[14] = checksum(sum, data_left | data_right, i); + + let shift = rsh(data_curr, 27); + let data_left = lsh(data_next, shift); + let data_right = rsh(data_next, 32 - shift); + let sum = checksum(buffer[15], b5_shifted, i); + buffer[15] = checksum(sum, data_left | data_right, i); + } + + let mut final_buffer = vec![buffer[0]; 4]; + + for i in 0..16 as u32 { + let data = buffer[i as usize]; + + let shift = data & 0x1F; + let data_left = lsh(data, 32 - shift); + let data_right = rsh(data, shift); + let b0_shifted = add(final_buffer[0], data_left | data_right); + final_buffer[0] = b0_shifted; + + if data < b0_shifted { + final_buffer[1] = add(final_buffer[1], data); + } else { + final_buffer[1] = checksum(final_buffer[1], data, i); + } + + if rsh(data & 0x02, 1) == (data & 0x01) { + final_buffer[2] = add(final_buffer[2], data); + } else { + final_buffer[2] = checksum(final_buffer[2], data, i); + } + + if (data & 0x01) == 0x01 { + final_buffer[3] = final_buffer[3] ^ data; + } else { + final_buffer[3] = checksum(final_buffer[3], data, i); + } + } + + let sum = checksum(final_buffer[0], final_buffer[1], 16); + let xor = final_buffer[3] ^ final_buffer[2]; + + Ok([ + (sum >> 8) as u8, + (sum >> 0) as u8, + (xor >> 24) as u8, + (xor >> 16) as u8, + (xor >> 8) as u8, + (xor >> 0) as u8, + ]) +} diff --git a/sw/deployer/src/sc64/error.rs b/sw/deployer/src/sc64/error.rs new file mode 100644 index 0000000..f96444f --- /dev/null +++ b/sw/deployer/src/sc64/error.rs @@ -0,0 +1,34 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug, Clone)] +pub struct Error { + description: String, +} + +impl Error { + pub fn new(description: &str) -> Self { + Error { + description: format!("{}", description), + } + } +} + +impl std::error::Error for Error {} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + write!(f, "SC64 error: {}", self.description.as_str()) + } +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Error::new(format!("IO error: {}", value).as_str()) + } +} + +impl From for Error { + fn from(value: serialport::Error) -> Self { + Error::new(format!("SerialPort error: {}", value.description).as_str()) + } +} diff --git a/sw/deployer/src/sc64/firmware.rs b/sw/deployer/src/sc64/firmware.rs new file mode 100644 index 0000000..e69de29 diff --git a/sw/deployer/src/sc64/link.rs b/sw/deployer/src/sc64/link.rs new file mode 100644 index 0000000..9c54013 --- /dev/null +++ b/sw/deployer/src/sc64/link.rs @@ -0,0 +1,231 @@ +use super::{error::Error, utils}; +use std::{collections::VecDeque, time::Duration}; + +#[derive(Clone, Debug)] +pub struct Device { + pub sn: String, + pub port: String, +} + +#[derive(Clone, Debug)] +pub struct Command { + pub id: u8, + pub args: [u32; 2], + pub data: Vec, +} + +#[derive(Clone, Debug)] +pub struct Response { + pub id: u8, + pub data: Vec, + pub error: bool, +} + +#[derive(Clone, Debug)] +pub struct Packet { + pub id: u8, + pub data: Vec, +} + +enum DataType { + Response, + Packet, +} + +pub trait Link { + fn execute_command(&mut self, command: &Command) -> Result, Error>; + fn execute_command_raw( + &mut self, + command: &Command, + timeout: Duration, + no_response: bool, + ignore_error: bool, + ) -> Result, Error>; + fn receive_packet(&mut self) -> Result, Error>; +} + +pub struct SerialLink { + serial: Box, + packets: VecDeque, +} + +const COMMAND_TIMEOUT: Duration = Duration::from_secs(5); +const PACKET_TIMEOUT: Duration = Duration::from_secs(5); + +impl SerialLink { + fn reset(&mut self) -> Result<(), Error> { + const WAIT_DURATION: Duration = Duration::from_millis(10); + const RETRY_COUNT: i32 = 100; + + self.serial.write_data_terminal_ready(true)?; + for n in 0..=RETRY_COUNT { + self.serial.clear(serialport::ClearBuffer::All)?; + std::thread::sleep(WAIT_DURATION); + if self.serial.read_data_set_ready()? { + break; + } + if n == RETRY_COUNT { + return Err(Error::new("Couldn't reset SC64 device (on)")); + } + } + + self.serial.write_data_terminal_ready(false)?; + for n in 0..=RETRY_COUNT { + std::thread::sleep(WAIT_DURATION); + if !self.serial.read_data_set_ready()? { + break; + } + if n == RETRY_COUNT { + return Err(Error::new("Couldn't reset SC64 device (off)")); + } + } + + Ok(()) + } + + fn serial_send_command(&mut self, command: &Command, timeout: Duration) -> Result<(), Error> { + let Command { id, args, mut data } = command.clone(); + + let mut packet: Vec = Vec::new(); + packet.append(&mut b"CMD".to_vec()); + packet.append(&mut [id].to_vec()); + packet.append(&mut args[0].to_be_bytes().to_vec()); + packet.append(&mut args[1].to_be_bytes().to_vec()); + packet.append(&mut data); + + self.serial.set_timeout(timeout)?; + self.serial.write_all(&packet)?; + self.serial.flush()?; + + Ok(()) + } + + fn serial_process_incoming_data( + &mut self, + data_type: DataType, + timeout: Duration, + ) -> Result, Error> { + const HEADER_SIZE: u32 = 8; + + let mut buffer = [0u8; HEADER_SIZE as usize]; + + self.serial.set_timeout(timeout)?; + + while matches!(data_type, DataType::Response) || self.serial.bytes_to_read()? >= HEADER_SIZE + { + self.serial.read_exact(&mut buffer)?; + + let (packet_token, error) = (match &buffer[0..3] { + b"CMP" => Ok((false, false)), + b"PKT" => Ok((true, false)), + b"ERR" => Ok((false, true)), + _ => Err(Error::new("Unknown response token")), + })?; + let id = buffer[3]; + let length = utils::u32_from_vec(&buffer[4..8])? as usize; + + let mut data = vec![0u8; length]; + self.serial.read_exact(&mut data)?; + + if packet_token { + self.packets.push_back(Packet { id, data }); + if matches!(data_type, DataType::Packet) { + return Ok(None); + } + } else { + return Ok(Some(Response { id, error, data })); + } + } + + Ok(None) + } + + fn send_command(&mut self, command: &Command) -> Result<(), Error> { + self.serial_send_command(command, COMMAND_TIMEOUT) + } + + fn receive_response(&mut self, timeout: Duration) -> Result { + if let Some(response) = self.serial_process_incoming_data(DataType::Response, timeout)? { + return Ok(response); + } + Err(Error::new("Command response timeout")) + } +} + +impl Link for SerialLink { + fn execute_command(&mut self, command: &Command) -> Result, Error> { + self.execute_command_raw(command, COMMAND_TIMEOUT, false, false) + } + + fn execute_command_raw( + &mut self, + command: &Command, + timeout: Duration, + no_response: bool, + ignore_error: bool, + ) -> Result, Error> { + self.send_command(command)?; + if no_response { + return Ok(vec![]); + } + let response = self.receive_response(timeout)?; + compare_id(&command, &response)?; + if !ignore_error && response.error { + return Err(Error::new("Command response error")); + } + Ok(response.data) + } + + fn receive_packet(&mut self) -> Result, Error> { + if self.packets.len() == 0 { + self.serial_process_incoming_data(DataType::Packet, PACKET_TIMEOUT)?; + } + Ok(self.packets.pop_front()) + } +} + +fn compare_id(command: &Command, response: &Response) -> Result<(), Error> { + if command.id != response.id { + return Err(Error::new("Command response ID didn't match")); + } + Ok(()) +} + +pub fn list_serial_devices() -> Result, Error> { + const SC64_VID: u16 = 0x0403; + const SC64_PID: u16 = 0x6014; + const SC64_SID: &str = "SC64"; + + let mut devices: Vec = Vec::new(); + + for device in serialport::available_ports()?.into_iter() { + if let serialport::SerialPortType::UsbPort(info) = device.port_type { + let sn = info.serial_number.unwrap_or("".to_string()); + if info.vid == SC64_VID && info.pid == SC64_PID && sn.starts_with(SC64_SID) { + devices.push(Device { + sn, + port: device.port_name, + }); + } + } + } + + if devices.len() == 0 { + return Err(Error::new("No SC64 devices found")); + } + + return Ok(devices); +} + +pub fn new_serial(port: &str) -> Result, Error> { + let mut link = SerialLink { + serial: serialport::new(port, 115_200) + .timeout(COMMAND_TIMEOUT) + .open()?, + packets: VecDeque::new(), + }; + + link.reset()?; + + Ok(Box::new(link)) +} diff --git a/sw/deployer/src/sc64/mod.rs b/sw/deployer/src/sc64/mod.rs new file mode 100644 index 0000000..c7d4e1b --- /dev/null +++ b/sw/deployer/src/sc64/mod.rs @@ -0,0 +1,672 @@ +mod cic; +mod error; +mod link; +mod types; +mod utils; + +use crate::sc64::cic::IPL3_OFFSET; + +use self::cic::{calculate_ipl3_checksum, guess_ipl3_seed, IPL3_LENGTH}; +pub use self::link::list_serial_devices; +pub use self::types::{ + BootMode, DataPacket, DdDiskState, DdDriveType, DdMode, DebugPacket, DiskPacket, + FirmwareStatus, SaveType, TvType, +}; +use self::types::{ButtonMode, CicSeed, UpdateStatus}; +use self::{ + link::{Command, Link}, + types::{get_config, get_setting, Config, ConfigId, Setting, SettingId}, + utils::{ + args_from_vec, datetime_from_vec, file_open_and_check_length, u32_from_vec, + vec_from_datetime, + }, +}; +use chrono::{DateTime, Local}; +pub use error::Error; +use std::io::{Read, Seek}; +use std::time::Instant; +use std::{cmp::min, time::Duration}; + +pub struct SC64 { + link: Box, +} + +#[derive(Debug)] +pub struct DeviceState { + pub bootloader_switch: bool, + pub rom_write_enable: bool, + pub rom_shadow_enable: bool, + pub dd_mode: DdMode, + pub isv_address: u32, + pub boot_mode: BootMode, + pub save_type: SaveType, + pub cic_seed: CicSeed, + pub tv_type: TvType, + pub dd_sd_enable: bool, + pub dd_drive_type: DdDriveType, + pub dd_disk_state: DdDiskState, + pub button_state: bool, + pub button_mode: ButtonMode, + pub rom_extended_enable: bool, + pub led_enable: bool, + pub datetime: DateTime, +} + +const SUPPORTED_MAJOR_VERSION: u16 = 2; +const SUPPORTED_MINOR_VERSION: u16 = 12; + +const SDRAM_ADDRESS: u32 = 0x0000_0000; +const SDRAM_LENGTH: usize = 64 * 1024 * 1024; + +const ROM_SHADOW_ADDRESS: u32 = 0x04FE_0000; +const ROM_SHADOW_LENGTH: usize = 128 * 1024; + +const ROM_EXTENDED_ADDRESS: u32 = 0x0400_0000; +const ROM_EXTENDED_LENGTH: usize = 14 * 1024 * 1024; + +const MAX_ROM_LENGTH: usize = 78 * 1024 * 1024; + +const DDIPL_ADDRESS: u32 = 0x03BC_0000; +const DDIPL_LENGTH: usize = 4 * 1024 * 1024; + +const SAVE_ADDRESS: u32 = 0x03FE_0000; +const EEPROM_ADDRESS: u32 = 0x0500_2000; + +const EEPROM_4K_LENGTH: usize = 512; +const EEPROM_16K_LENGTH: usize = 2 * 1024; +const SRAM_LENGTH: usize = 32 * 1024; +const SRAM_BANKED_LENGTH: usize = 3 * 32 * 1024; +const FLASHRAM_LENGTH: usize = 128 * 1024; + +const BOOTLOADER_ADDRESS: u32 = 0x04E0_0000; +const BOOTLOADER_LENGTH: usize = 1920 * 1024; + +const FIRMWARE_ADDRESS: u32 = 0x0010_0000; // Arbitrary offset in SDRAM memory +const FIRMWARE_COMMAND_TIMEOUT: Duration = Duration::from_secs(30); +const FIRMWARE_UPDATE_TIMEOUT: Duration = Duration::from_secs(90); + +const COMMAND_USB_WRITE_TIMEOUT: Duration = Duration::from_secs(5); + +const ISV_BUFFER_LENGTH: usize = 64 * 1024; + +pub const MEMORY_LENGTH: usize = 0x0500_2980; + +impl SC64 { + fn command_identifier_get(&mut self) -> Result, Error> { + let identifier = self.link.execute_command(&Command { + id: b'v', + args: [0, 0], + data: vec![], + })?; + Ok(identifier) + } + + fn command_version_get(&mut self) -> Result<(u16, u16), Error> { + let version = self.link.execute_command(&Command { + id: b'V', + args: [0, 0], + data: vec![], + })?; + let major = utils::u16_from_vec(&version[0..2])?; + let minor = utils::u16_from_vec(&version[2..4])?; + Ok((major, minor)) + } + + fn command_state_reset(&mut self) -> Result<(), Error> { + self.link.execute_command(&Command { + id: b'R', + args: [0, 0], + data: vec![], + })?; + Ok(()) + } + + fn command_cic_params_set( + &mut self, + disable: bool, + seed: u8, + checksum: &[u8; 6], + ) -> Result<(), Error> { + let mut params: Vec = vec![]; + params.append(&mut [(disable as u8) << 0, seed].to_vec()); + params.append(&mut checksum.to_vec()); + self.link.execute_command(&Command { + id: b'B', + args: args_from_vec(¶ms[0..8])?, + data: vec![], + })?; + Ok(()) + } + + fn command_config_get(&mut self, config_id: ConfigId) -> Result { + let data = self.link.execute_command(&Command { + id: b'c', + args: [config_id.into(), 0], + data: vec![], + })?; + let value = u32_from_vec(&data[0..4])?; + Ok((config_id, value).try_into()?) + } + + fn command_config_set(&mut self, config: Config) -> Result<(), Error> { + self.link.execute_command(&Command { + id: b'C', + args: config.into(), + data: vec![], + })?; + Ok(()) + } + + fn command_setting_get(&mut self, setting_id: SettingId) -> Result { + let data = self.link.execute_command(&Command { + id: b'a', + args: [setting_id.into(), 0], + data: vec![], + })?; + let value = u32_from_vec(&data[0..4])?; + Ok((setting_id, value).try_into()?) + } + + fn command_setting_set(&mut self, setting: Setting) -> Result<(), Error> { + self.link.execute_command(&Command { + id: b'A', + args: setting.into(), + data: vec![], + })?; + Ok(()) + } + + fn command_time_get(&mut self) -> Result, Error> { + let data = self.link.execute_command(&Command { + id: b't', + args: [0, 0], + data: vec![], + })?; + Ok(datetime_from_vec(&data[0..8])?) + } + + fn command_time_set(&mut self, datetime: DateTime) -> Result<(), Error> { + self.link.execute_command(&Command { + id: b'T', + args: args_from_vec(&vec_from_datetime(datetime)?[0..8])?, + data: vec![], + })?; + Ok(()) + } + + fn command_memory_read(&mut self, address: u32, length: usize) -> Result, Error> { + let data = self.link.execute_command(&Command { + id: b'm', + args: [address, length as u32], + data: vec![], + })?; + Ok(data) + } + + fn command_memory_write(&mut self, address: u32, data: &[u8]) -> Result<(), Error> { + self.link.execute_command(&Command { + id: b'M', + args: [address, data.len() as u32], + data: data.to_vec(), + })?; + Ok(()) + } + + fn command_usb_write(&mut self, datatype: u8, data: &[u8]) -> Result<(), Error> { + self.link.execute_command_raw( + &Command { + id: b'U', + args: [datatype as u32, data.len() as u32], + data: data.to_vec(), + }, + COMMAND_USB_WRITE_TIMEOUT, + true, + false, + )?; + Ok(()) + } + + fn command_dd_set_block_ready(&mut self, error: bool) -> Result<(), Error> { + self.link.execute_command(&Command { + id: b'D', + args: [error as u32, 0], + data: vec![], + })?; + Ok(()) + } + + fn command_flash_wait_busy(&mut self, wait: bool) -> Result { + let erase_block_size = self.link.execute_command(&Command { + id: b'p', + args: [wait as u32, 0], + data: vec![], + })?; + Ok(utils::u32_from_vec(&erase_block_size[0..4])?) + } + + fn command_flash_erase_block(&mut self, address: u32) -> Result<(), Error> { + self.link.execute_command(&Command { + id: b'P', + args: [address, 0], + data: vec![], + })?; + Ok(()) + } + + fn command_firmware_backup(&mut self, address: u32) -> Result<(FirmwareStatus, u32), Error> { + let data = self.link.execute_command_raw( + &Command { + id: b'f', + args: [address, 0], + data: vec![], + }, + FIRMWARE_COMMAND_TIMEOUT, + false, + true, + )?; + let status = FirmwareStatus::try_from(utils::u32_from_vec(&data[0..4])?)?; + let length = utils::u32_from_vec(&data[4..8])?; + Ok((status, length)) + } + + fn command_firmware_update( + &mut self, + address: u32, + length: usize, + ) -> Result { + let data = self.link.execute_command_raw( + &Command { + id: b'F', + args: [address, length as u32], + data: vec![], + }, + FIRMWARE_COMMAND_TIMEOUT, + false, + true, + )?; + Ok(FirmwareStatus::try_from(utils::u32_from_vec(&data[0..4])?)?) + } + + fn flash_erase(&mut self, address: u32, length: usize) -> Result<(), Error> { + let erase_block_size = self.command_flash_wait_busy(false)?; + for offset in (0..length as u32).step_by(erase_block_size as usize) { + self.command_flash_erase_block(address + offset)?; + } + Ok(()) + } + + fn flash_program(&mut self, address: u32, data: &[u8]) -> Result<(), Error> { + let current_data = self.command_memory_read(address, data.len())?; + if data == current_data { + return Ok(()); + } + self.flash_erase(address, data.len())?; + self.command_memory_write(address, data)?; + self.command_flash_wait_busy(true)?; + Ok(()) + } + + fn flash_program_shadow(&mut self, data: &[u8]) -> Result<(), Error> { + if data.len() > ROM_SHADOW_LENGTH { + return Err(Error::new( + "Invalid data length for program ROM shadow operation", + )); + } + self.flash_program(ROM_SHADOW_ADDRESS, data) + } + + fn flash_program_extended(&mut self, data: &[u8]) -> Result<(), Error> { + if data.len() > ROM_EXTENDED_LENGTH { + return Err(Error::new( + "Invalid data length for program ROM extended operation", + )); + } + self.flash_program(ROM_EXTENDED_ADDRESS, data) + } + + #[allow(dead_code)] + fn flash_program_bootloader(&mut self, data: &[u8]) -> Result<(), Error> { + if data.len() > BOOTLOADER_LENGTH { + return Err(Error::new( + "Invalid data length for program bootloader operation", + )); + } + self.flash_program(BOOTLOADER_ADDRESS, data) + } +} + +impl SC64 { + pub fn check_firmware_version(&mut self) -> Result<(u16, u16), Error> { + let (major, minor) = self + .command_version_get() + .map_err(|_| Error::new("Outdated SC64 firmware version, please update firmware"))?; + if major != SUPPORTED_MAJOR_VERSION || minor < SUPPORTED_MINOR_VERSION { + return Err(Error::new( + "Unsupported SC64 firmware version, please update firmware", + )); + } + Ok((major, minor)) + } + + pub fn reset_state(&mut self) -> Result<(), Error> { + self.command_state_reset()?; + Ok(()) + } + + pub fn upload_rom(&mut self, path: &str, no_shadow: bool) -> Result<(), Error> { + const BUFFER_SIZE: usize = 1 * 1024 * 1024; + + let (mut file, length) = file_open_and_check_length(path, MAX_ROM_LENGTH)?; + + let mut endian_check = vec![0u8; 4]; + file.read(&mut endian_check)?; + file.rewind()?; + + let endian_swapper = match u32_from_vec(&endian_check[0..4])? { + 0x37804012 => |b: &mut [u8]| b.chunks_exact_mut(2).for_each(|c| c.swap(0, 1)), + 0x40123780 => |b: &mut [u8]| { + b.chunks_exact_mut(4).for_each(|c| { + c.swap(0, 3); + c.swap(1, 2) + }) + }, + _ => |_b: &mut [u8]| {}, + }; + + let rom_shadow_enabled = !no_shadow && length > (SDRAM_LENGTH - ROM_SHADOW_LENGTH); + let rom_extended_enabled = length > SDRAM_LENGTH; + + let sdram_length = if rom_shadow_enabled { + min(length, SDRAM_LENGTH - ROM_SHADOW_LENGTH) + } else { + min(length, SDRAM_LENGTH) + }; + + let mut buffer = vec![0u8; BUFFER_SIZE]; + + for offset in (0..sdram_length as u32).step_by(buffer.len()) { + let chunk = file.read(&mut buffer)?; + endian_swapper(&mut buffer); + self.command_memory_write(SDRAM_ADDRESS + offset, &buffer[0..chunk])?; + } + + self.command_config_set(Config::RomShadowEnable(rom_shadow_enabled))?; + if rom_shadow_enabled { + let mut buffer = vec![0u8; ROM_SHADOW_LENGTH]; + let chunk = file.read(&mut buffer)?; + endian_swapper(&mut buffer); + self.flash_program_shadow(&buffer[0..chunk])?; + } + + self.command_config_set(Config::RomExtendedEnable(rom_extended_enabled))?; + if rom_extended_enabled { + let mut buffer = vec![0u8; ROM_EXTENDED_LENGTH]; + let chunk = file.read(&mut buffer)?; + endian_swapper(&mut buffer); + self.flash_program_extended(&buffer[0..chunk])?; + } + + Ok(()) + } + + pub fn upload_ddipl(&mut self, path: &str) -> Result<(), Error> { + let (mut file, length) = file_open_and_check_length(path, DDIPL_LENGTH)?; + + let mut buffer = vec![0u8; length]; + let chunk = file.read(&mut buffer)?; + + self.command_memory_write(DDIPL_ADDRESS, &buffer[0..chunk]) + } + + pub fn upload_save(&mut self, path: &str) -> Result<(), Error> { + let save_type = get_config!(self, SaveType)?; + + if matches!(save_type, SaveType::None) { + return Err(Error::new("No save type is enabled")); + } + + let address = match save_type { + SaveType::None => 0, + SaveType::Eeprom4k => EEPROM_ADDRESS, + SaveType::Eeprom16k => EEPROM_ADDRESS, + SaveType::Sram => SAVE_ADDRESS, + SaveType::SramBanked => SAVE_ADDRESS, + SaveType::Flashram => SAVE_ADDRESS, + }; + + let save_length = match save_type { + SaveType::None => 0, + SaveType::Eeprom4k => EEPROM_4K_LENGTH, + SaveType::Eeprom16k => EEPROM_16K_LENGTH, + SaveType::Sram => SRAM_LENGTH, + SaveType::SramBanked => SRAM_BANKED_LENGTH, + SaveType::Flashram => FLASHRAM_LENGTH, + }; + + let (mut file, length) = file_open_and_check_length(path, save_length)?; + + if length != save_length { + return Err(Error::new( + "Save file size did not match currently enabled save type", + )); + } + + let mut buffer = vec![0u8; length]; + file.read(&mut buffer)?; + + self.command_memory_write(address, &buffer) + } + + pub fn dump_memory(&mut self, address: u32, length: usize) -> Result, Error> { + if address + length as u32 > MEMORY_LENGTH as u32 { + return Err(Error::new("Invalid dump address or length")); + } + self.command_memory_read(address, length) + } + + pub fn calculate_cic_parameters(&mut self) -> Result<(), Error> { + let boot_mode = get_config!(self, BootMode)?; + let address = match boot_mode { + BootMode::DirectRom => SDRAM_ADDRESS, + BootMode::DirectDdIpl => DDIPL_ADDRESS, + _ => BOOTLOADER_ADDRESS, + }; + let ipl3 = self.command_memory_read(address + IPL3_OFFSET, IPL3_LENGTH)?; + let seed = guess_ipl3_seed(&ipl3)?; + let checksum = &calculate_ipl3_checksum(&ipl3, seed)?; + self.command_cic_params_set(false, seed, checksum) + } + + pub fn set_boot_mode(&mut self, boot_mode: BootMode) -> Result<(), Error> { + self.command_config_set(Config::BootMode(boot_mode)) + } + + pub fn set_save_type(&mut self, save_type: SaveType) -> Result<(), Error> { + self.command_config_set(Config::SaveType(save_type)) + } + + pub fn set_tv_type(&mut self, tv_type: TvType) -> Result<(), Error> { + self.command_config_set(Config::TvType(tv_type)) + } + + pub fn get_datetime(&mut self) -> Result, Error> { + self.command_time_get() + } + + pub fn set_datetime(&mut self, datetime: DateTime) -> Result<(), Error> { + self.command_time_set(datetime) + } + + pub fn set_led_blink(&mut self, enabled: bool) -> Result<(), Error> { + self.command_setting_set(Setting::LedEnable(enabled)) + } + + pub fn get_device_state(&mut self) -> Result { + Ok(DeviceState { + bootloader_switch: get_config!(self, BootloaderSwitch)?, + rom_write_enable: get_config!(self, RomWriteEnable)?, + rom_shadow_enable: get_config!(self, RomShadowEnable)?, + dd_mode: get_config!(self, DdMode)?, + isv_address: get_config!(self, IsvAddress)?, + boot_mode: get_config!(self, BootMode)?, + save_type: get_config!(self, SaveType)?, + cic_seed: get_config!(self, CicSeed)?, + tv_type: get_config!(self, TvType)?, + dd_sd_enable: get_config!(self, DdSdEnable)?, + dd_drive_type: get_config!(self, DdDriveType)?, + dd_disk_state: get_config!(self, DdDiskState)?, + button_state: get_config!(self, ButtonState)?, + button_mode: get_config!(self, ButtonMode)?, + rom_extended_enable: get_config!(self, RomExtendedEnable)?, + led_enable: get_setting!(self, LedEnable)?, + datetime: self.get_datetime()?, + }) + } + + pub fn configure_64dd( + &mut self, + dd_mode: DdMode, + drive_type: DdDriveType, + ) -> Result<(), Error> { + self.command_config_set(Config::DdMode(dd_mode))?; + self.command_config_set(Config::DdSdEnable(false))?; + self.command_config_set(Config::DdDriveType(drive_type))?; + self.command_config_set(Config::DdDiskState(DdDiskState::Ejected))?; + Ok(()) + } + + pub fn set_64dd_disk_state(&mut self, disk_state: DdDiskState) -> Result<(), Error> { + self.command_config_set(Config::DdDiskState(disk_state)) + } + + pub fn configure_isviewer64(&mut self, offset: Option) -> Result<(), Error> { + if let Some(off) = offset { + if get_config!(self, RomShadowEnable)? { + if off > (SAVE_ADDRESS - ISV_BUFFER_LENGTH as u32) { + return Err(Error::new( + format!( + "ROM shadow is enabled, IS-Viewer 64 at offset 0x{off:08X} won't work" + ) + .as_str(), + )); + } + } + self.command_config_set(Config::RomWriteEnable(true))?; + self.command_config_set(Config::IsvAddress(off))?; + } else { + self.command_config_set(Config::RomWriteEnable(false))?; + self.command_config_set(Config::IsvAddress(0))?; + } + Ok(()) + } + + pub fn receive_data_packet(&mut self) -> Result, Error> { + if let Some(packet) = self.link.receive_packet()? { + return Ok(Some(packet.try_into()?)); + } + Ok(None) + } + + pub fn reply_disk_packet(&mut self, disk_packet: Option) -> Result<(), Error> { + if let Some(packet) = disk_packet { + match packet { + DiskPacket::ReadBlock(disk_block) => { + self.command_memory_write(disk_block.address, &disk_block.data)?; + } + DiskPacket::WriteBlock(_) => {} + } + self.command_dd_set_block_ready(false)?; + } else { + self.command_dd_set_block_ready(true)?; + } + Ok(()) + } + + pub fn send_debug_packet(&mut self, debug_packet: DebugPacket) -> Result<(), Error> { + self.command_usb_write(debug_packet.datatype, &debug_packet.data) + } + + pub fn backup_firmware(&mut self) -> Result, Error> { + self.command_state_reset()?; + let (status, length) = self.command_firmware_backup(FIRMWARE_ADDRESS)?; + if !matches!(status, FirmwareStatus::Ok) { + return Err(Error::new( + format!("Firmware backup error: {:?}", status).as_str(), + )); + } + self.command_memory_read(FIRMWARE_ADDRESS, length as usize) + } + + pub fn update_firmware(&mut self, data: &[u8]) -> Result<(), Error> { + self.command_state_reset()?; + self.command_memory_write(FIRMWARE_ADDRESS, data)?; + let status = self.command_firmware_update(FIRMWARE_ADDRESS, data.len())?; + if !matches!(status, FirmwareStatus::Ok) { + return Err(Error::new( + format!("Firmware update verify error: {:?}", status).as_str(), + )); + } + let timeout = Instant::now(); + let mut last_update_status = UpdateStatus::Err; + loop { + if let Some(packet) = self.receive_data_packet()? { + if let DataPacket::UpdateStatus(status) = packet { + match status { + UpdateStatus::Done => { + std::thread::sleep(Duration::from_secs(2)); + return Ok(()); + } + UpdateStatus::Err => { + return Err(Error::new( + format!( + "Firmware update error on step {:?}, device is most likely bricked", + last_update_status + ) + .as_str(), + )) + } + current_update_status => last_update_status = current_update_status, + } + } + } + if timeout.elapsed() > FIRMWARE_UPDATE_TIMEOUT { + return Err(Error::new( + format!( + "Firmware update timeout, SC64 did not finish update in {} seconds", + FIRMWARE_UPDATE_TIMEOUT.as_secs() + ) + .as_str(), + )); + } + std::thread::sleep(Duration::from_millis(1)); + } + } +} + +pub fn new(sn: Option) -> Result { + let port = match sn { + Some(sn) => match list_serial_devices()?.iter().find(|d| d.sn == sn) { + Some(device) => device.port.clone(), + None => { + return Err(Error::new( + "No SC64 device found matching provided serial number", + )) + } + }, + None => list_serial_devices()?[0].port.clone(), + }; + + let mut sc64 = SC64 { + link: link::new_serial(&port)?, + }; + + let identifier = sc64 + .command_identifier_get() + .map_err(|_| Error::new("Couldn't get SC64 device identifier"))?; + + if identifier != b"SCv2" { + return Err(Error::new("Unknown identifier received, not a SC64 device")); + } + + Ok(sc64) +} diff --git a/sw/deployer/src/sc64/types.rs b/sw/deployer/src/sc64/types.rs new file mode 100644 index 0000000..4d46d82 --- /dev/null +++ b/sw/deployer/src/sc64/types.rs @@ -0,0 +1,553 @@ +use super::{link::Packet, utils::u32_from_vec, Error}; +use encoding_rs::EUC_JP; + +#[derive(Clone, Copy)] +pub enum ConfigId { + BootloaderSwitch, + RomWriteEnable, + RomShadowEnable, + DdMode, + IsvAddress, + BootMode, + SaveType, + CicSeed, + TvType, + DdSdEnable, + DdDriveType, + DdDiskState, + ButtonState, + ButtonMode, + RomExtendedEnable, +} + +pub enum Config { + BootloaderSwitch(bool), + RomWriteEnable(bool), + RomShadowEnable(bool), + DdMode(DdMode), + IsvAddress(u32), + BootMode(BootMode), + SaveType(SaveType), + CicSeed(CicSeed), + TvType(TvType), + DdSdEnable(bool), + DdDriveType(DdDriveType), + DdDiskState(DdDiskState), + ButtonState(bool), + ButtonMode(ButtonMode), + RomExtendedEnable(bool), +} + +impl From for u32 { + fn from(value: ConfigId) -> Self { + match value { + ConfigId::BootloaderSwitch => 0, + ConfigId::RomWriteEnable => 1, + ConfigId::RomShadowEnable => 2, + ConfigId::DdMode => 3, + ConfigId::IsvAddress => 4, + ConfigId::BootMode => 5, + ConfigId::SaveType => 6, + ConfigId::CicSeed => 7, + ConfigId::TvType => 8, + ConfigId::DdSdEnable => 9, + ConfigId::DdDriveType => 10, + ConfigId::DdDiskState => 11, + ConfigId::ButtonState => 12, + ConfigId::ButtonMode => 13, + ConfigId::RomExtendedEnable => 14, + } + } +} + +impl TryFrom<(ConfigId, u32)> for Config { + type Error = Error; + fn try_from(value: (ConfigId, u32)) -> Result { + let (id, config) = value; + Ok(match id { + ConfigId::BootloaderSwitch => Config::BootloaderSwitch(config != 0), + ConfigId::RomWriteEnable => Config::RomWriteEnable(config != 0), + ConfigId::RomShadowEnable => Config::RomShadowEnable(config != 0), + ConfigId::DdMode => Config::DdMode(config.try_into()?), + ConfigId::IsvAddress => Config::IsvAddress(config), + ConfigId::BootMode => Config::BootMode(config.try_into()?), + ConfigId::SaveType => Config::SaveType(config.try_into()?), + ConfigId::CicSeed => Config::CicSeed(config.try_into()?), + ConfigId::TvType => Config::TvType(config.try_into()?), + ConfigId::DdSdEnable => Config::DdSdEnable(config != 0), + ConfigId::DdDriveType => Config::DdDriveType(config.try_into()?), + ConfigId::DdDiskState => Config::DdDiskState(config.try_into()?), + ConfigId::ButtonState => Config::ButtonState(config != 0), + ConfigId::ButtonMode => Config::ButtonMode(config.try_into()?), + ConfigId::RomExtendedEnable => Config::RomExtendedEnable(config != 0), + }) + } +} + +impl From for [u32; 2] { + fn from(value: Config) -> Self { + match value { + Config::BootloaderSwitch(val) => [ConfigId::BootloaderSwitch.into(), val.into()], + Config::RomWriteEnable(val) => [ConfigId::RomWriteEnable.into(), val.into()], + Config::RomShadowEnable(val) => [ConfigId::RomShadowEnable.into(), val.into()], + Config::DdMode(val) => [ConfigId::DdMode.into(), val.into()], + Config::IsvAddress(val) => [ConfigId::IsvAddress.into(), val.into()], + Config::BootMode(val) => [ConfigId::BootMode.into(), val.into()], + Config::SaveType(val) => [ConfigId::SaveType.into(), val.into()], + Config::CicSeed(val) => [ConfigId::CicSeed.into(), val.into()], + Config::TvType(val) => [ConfigId::TvType.into(), val.into()], + Config::DdSdEnable(val) => [ConfigId::DdSdEnable.into(), val.into()], + Config::DdDriveType(val) => [ConfigId::DdDriveType.into(), val.into()], + Config::DdDiskState(val) => [ConfigId::DdDiskState.into(), val.into()], + Config::ButtonState(val) => [ConfigId::ButtonState.into(), val.into()], + Config::ButtonMode(val) => [ConfigId::ButtonMode.into(), val.into()], + Config::RomExtendedEnable(val) => [ConfigId::RomExtendedEnable.into(), val.into()], + } + } +} + +#[derive(Debug)] +pub enum DdMode { + None, + Regs, + DdIpl, + Full, +} + +impl TryFrom for DdMode { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => DdMode::None, + 1 => DdMode::Regs, + 2 => DdMode::DdIpl, + 3 => DdMode::Full, + _ => return Err(Error::new("Unknown 64DD mode code")), + }) + } +} + +impl From for u32 { + fn from(value: DdMode) -> Self { + match value { + DdMode::None => 0, + DdMode::Regs => 1, + DdMode::DdIpl => 2, + DdMode::Full => 3, + } + } +} + +#[derive(Copy, Clone, Debug)] +pub enum BootMode { + Menu, + Rom, + DdIpl, + DirectRom, + DirectDdIpl, +} + +impl TryFrom for BootMode { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => BootMode::Menu, + 1 => BootMode::Rom, + 2 => BootMode::DdIpl, + 3 => BootMode::DirectRom, + 4 => BootMode::DirectDdIpl, + _ => return Err(Error::new("Unknown boot mode code")), + }) + } +} + +impl From for u32 { + fn from(value: BootMode) -> Self { + match value { + BootMode::Menu => 0, + BootMode::Rom => 1, + BootMode::DdIpl => 2, + BootMode::DirectRom => 3, + BootMode::DirectDdIpl => 4, + } + } +} + +#[derive(Debug)] +pub enum SaveType { + None, + Eeprom4k, + Eeprom16k, + Sram, + Flashram, + SramBanked, +} + +impl TryFrom for SaveType { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => SaveType::None, + 1 => SaveType::Eeprom4k, + 2 => SaveType::Eeprom16k, + 3 => SaveType::Sram, + 4 => SaveType::Flashram, + 5 => SaveType::SramBanked, + _ => return Err(Error::new("Unknown save type code")), + }) + } +} + +impl From for u32 { + fn from(value: SaveType) -> Self { + match value { + SaveType::None => 0, + SaveType::Eeprom4k => 1, + SaveType::Eeprom16k => 2, + SaveType::Sram => 3, + SaveType::Flashram => 4, + SaveType::SramBanked => 5, + } + } +} + +#[derive(Debug)] +pub enum CicSeed { + Seed(u8), + Auto, +} + +impl TryFrom for CicSeed { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(if value <= 0xFF { + CicSeed::Seed(value as u8) + } else if value == 0xFFFF { + CicSeed::Auto + } else { + return Err(Error::new("Unknown CIC seed code")); + }) + } +} + +impl From for u32 { + fn from(value: CicSeed) -> Self { + match value { + CicSeed::Seed(seed) => seed.into(), + CicSeed::Auto => 0xFFFF, + } + } +} + +#[derive(Debug)] +pub enum TvType { + PAL, + NTSC, + MPAL, + Auto, +} + +impl TryFrom for TvType { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => TvType::PAL, + 1 => TvType::NTSC, + 2 => TvType::MPAL, + 3 => TvType::Auto, + _ => return Err(Error::new("Unknown TV type code")), + }) + } +} + +impl From for u32 { + fn from(value: TvType) -> Self { + match value { + TvType::PAL => 0, + TvType::NTSC => 1, + TvType::MPAL => 2, + TvType::Auto => 3, + } + } +} + +#[derive(Debug)] +pub enum DdDriveType { + Retail, + Development, +} + +impl TryFrom for DdDriveType { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => DdDriveType::Retail, + 1 => DdDriveType::Development, + _ => return Err(Error::new("Unknown 64DD drive type code")), + }) + } +} + +impl From for u32 { + fn from(value: DdDriveType) -> Self { + match value { + DdDriveType::Retail => 0, + DdDriveType::Development => 1, + } + } +} + +#[derive(Debug)] +pub enum DdDiskState { + Ejected, + Inserted, + Changed, +} + +impl TryFrom for DdDiskState { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => DdDiskState::Ejected, + 1 => DdDiskState::Inserted, + 2 => DdDiskState::Changed, + _ => return Err(Error::new("Unknown 64DD disk state code")), + }) + } +} + +impl From for u32 { + fn from(value: DdDiskState) -> Self { + match value { + DdDiskState::Ejected => 0, + DdDiskState::Inserted => 1, + DdDiskState::Changed => 2, + } + } +} + +#[derive(Debug)] +pub enum ButtonMode { + None, + N64Irq, + UsbPacket, + DdDiskSwap, +} + +impl TryFrom for ButtonMode { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => ButtonMode::None, + 1 => ButtonMode::N64Irq, + 2 => ButtonMode::UsbPacket, + 3 => ButtonMode::DdDiskSwap, + _ => return Err(Error::new("Unknown button mode code")), + }) + } +} + +impl From for u32 { + fn from(value: ButtonMode) -> Self { + match value { + ButtonMode::None => 0, + ButtonMode::N64Irq => 1, + ButtonMode::UsbPacket => 2, + ButtonMode::DdDiskSwap => 3, + } + } +} + +#[derive(Clone, Copy)] +pub enum SettingId { + LedEnable, +} + +pub enum Setting { + LedEnable(bool), +} + +impl From for u32 { + fn from(value: SettingId) -> Self { + match value { + SettingId::LedEnable => 0, + } + } +} + +impl TryFrom<(SettingId, u32)> for Setting { + type Error = Error; + fn try_from(value: (SettingId, u32)) -> Result { + let (id, setting) = value; + Ok(match id { + SettingId::LedEnable => Setting::LedEnable(setting != 0), + }) + } +} + +impl From for [u32; 2] { + fn from(value: Setting) -> Self { + match value { + Setting::LedEnable(val) => [SettingId::LedEnable.into(), val.into()], + } + } +} + +pub enum DataPacket { + Button, + Debug(DebugPacket), + Disk(DiskPacket), + IsViewer(String), + UpdateStatus(UpdateStatus), +} + +impl TryFrom for DataPacket { + type Error = Error; + fn try_from(value: Packet) -> Result { + Ok(match value.id { + b'B' => Self::Button, + b'U' => Self::Debug(value.data.try_into()?), + b'D' => Self::Disk(value.data.try_into()?), + b'I' => Self::IsViewer(EUC_JP.decode(&value.data).0.into()), + b'F' => Self::UpdateStatus(u32_from_vec(&value.data[0..4])?.try_into()?), + _ => return Err(Error::new("Unknown data packet code")), + }) + } +} + +pub struct DebugPacket { + pub datatype: u8, + pub data: Vec, +} + +impl TryFrom> for DebugPacket { + type Error = Error; + fn try_from(value: Vec) -> Result { + if value.len() < 4 { + return Err(Error::new("Couldn't extract header from debug packet")); + } + let header = u32_from_vec(&value[0..4])?; + let datatype = ((header >> 24) & 0xFF) as u8; + let length = (header & 0x00FFFFFF) as usize; + let data = value[4..].to_vec(); + if data.len() != length { + return Err(Error::new("Debug packet length did not match")); + } + Ok(Self { datatype, data }) + } +} + +pub enum DiskPacket { + ReadBlock(DiskBlock), + WriteBlock(DiskBlock), +} + +impl TryFrom> for DiskPacket { + type Error = Error; + fn try_from(value: Vec) -> Result { + if value.len() < 12 { + return Err(Error::new("Couldn't extract block info from disk packet")); + } + let command = u32_from_vec(&value[0..4])?; + let address = u32_from_vec(&value[4..8])?; + let track_head_block = u32_from_vec(&value[8..12])?; + let disk_block = DiskBlock { + address, + track: (track_head_block >> 2) & 0xFFF, + head: (track_head_block >> 1) & 0x01, + block: track_head_block & 0x01, + data: value[12..].to_vec(), + }; + Ok(match command { + 1 => Self::ReadBlock(disk_block), + 2 => Self::WriteBlock(disk_block), + _ => return Err(Error::new("Unknown disk packet command code")), + }) + } +} + +pub struct DiskBlock { + pub address: u32, + pub track: u32, + pub head: u32, + pub block: u32, + pub data: Vec, +} + +impl DiskBlock { + pub fn set_data(&mut self, data: &[u8]) { + self.data = data.to_vec(); + } +} + +#[derive(Debug)] +pub enum FirmwareStatus { + Ok, + ErrToken, + ErrChecksum, + ErrSize, + ErrUnknownChunk, + ErrRead, +} + +impl TryFrom for FirmwareStatus { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => FirmwareStatus::Ok, + 1 => FirmwareStatus::ErrToken, + 2 => FirmwareStatus::ErrChecksum, + 3 => FirmwareStatus::ErrSize, + 4 => FirmwareStatus::ErrUnknownChunk, + 5 => FirmwareStatus::ErrRead, + _ => return Err(Error::new("Unknown firmware status code")), + }) + } +} + +#[derive(Debug)] +pub enum UpdateStatus { + MCU, + FPGA, + Bootloader, + Done, + Err, +} + +impl TryFrom for UpdateStatus { + type Error = Error; + fn try_from(value: u32) -> Result { + Ok(match value { + 1 => UpdateStatus::MCU, + 2 => UpdateStatus::FPGA, + 3 => UpdateStatus::Bootloader, + 0x80 => UpdateStatus::Done, + 0xFF => UpdateStatus::Err, + _ => return Err(Error::new("Unknown update status code")), + }) + } +} + +macro_rules! get_config { + ($sc64:ident, $config:ident) => {{ + if let Config::$config(value) = $sc64.command_config_get(ConfigId::$config)? { + Ok(value) + } else { + Err(Error::new("Unexpected config type")) + } + }}; +} + +macro_rules! get_setting { + ($sc64:ident, $setting:ident) => {{ + #[allow(irrefutable_let_patterns)] // TODO: is there another way to ignore this warning? + if let Setting::$setting(value) = $sc64.command_setting_get(SettingId::$setting)? { + Ok(value) + } else { + Err(Error::new("Unexpected setting type")) + } + }}; +} + +pub(crate) use get_config; +pub(crate) use get_setting; diff --git a/sw/deployer/src/sc64/utils.rs b/sw/deployer/src/sc64/utils.rs new file mode 100644 index 0000000..91e7b86 --- /dev/null +++ b/sw/deployer/src/sc64/utils.rs @@ -0,0 +1,75 @@ +use super::Error; +use chrono::{DateTime, Datelike, Local, NaiveDateTime, TimeZone, Timelike}; + +pub fn u16_from_vec(data: &[u8]) -> Result { + if data.len() != 2 { + return Err(Error::new("Invalid slice length provided to u16_from_vec")); + } + let bytes = data[0..2] + .try_into() + .map_err(|_| Error::new("Couldn't convert from bytes to u16"))?; + Ok(u16::from_be_bytes(bytes)) +} + +pub fn u32_from_vec(data: &[u8]) -> Result { + if data.len() != 4 { + return Err(Error::new("Invalid slice length provided to u32_from_vec")); + } + let bytes = data[0..4] + .try_into() + .map_err(|_| Error::new("Couldn't convert from bytes to u32"))?; + Ok(u32::from_be_bytes(bytes)) +} + +pub fn args_from_vec(data: &[u8]) -> Result<[u32; 2], Error> { + if data.len() != 8 { + return Err(Error::new("Invalid slice length provided to args_from_vec")); + } + Ok([u32_from_vec(&data[0..4])?, u32_from_vec(&data[4..8])?]) +} + +pub fn u8_from_bcd(value: u8) -> u8 { + (((value & 0xF0) >> 4) * 10) + (value & 0x0F) +} + +pub fn bcd_from_u8(value: u8) -> u8 { + (((value / 10) & 0x0F) << 4) | ((value % 10) & 0x0F) +} + +pub fn datetime_from_vec(data: &[u8]) -> Result, Error> { + let hour = u8_from_bcd(data[1]); + let minute = u8_from_bcd(data[2]); + let second = u8_from_bcd(data[3]); + let year = u8_from_bcd(data[5]) as u32 + 2000; + let month = u8_from_bcd(data[6]); + let day = u8_from_bcd(data[7]); + let native = &NaiveDateTime::parse_from_str( + &format!("{year:02}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"), + "%Y-%m-%dT%H:%M:%S", + ) + .map_err(|_| Error::new("Couldn't convert from bytes to DateTime"))?; + Ok(Local.from_local_datetime(native).unwrap()) +} + +pub fn vec_from_datetime(datetime: DateTime) -> Result, Error> { + let weekday = bcd_from_u8((datetime.weekday() as u8) + 1); + let hour = bcd_from_u8(datetime.hour() as u8); + let minute = bcd_from_u8(datetime.minute() as u8); + let second = bcd_from_u8(datetime.second() as u8); + let year = bcd_from_u8((datetime.year() - 2000) as u8); + let month = bcd_from_u8(datetime.month() as u8); + let day = bcd_from_u8(datetime.day() as u8); + Ok(vec![weekday, hour, minute, second, 0, year, month, day]) +} + +pub fn file_open_and_check_length( + path: &str, + max_length: usize, +) -> Result<(std::fs::File, usize), Error> { + let file = std::fs::File::open(path)?; + let length = file.metadata()?.len() as usize; + if length > max_length { + return Err(Error::new("File size is too big")); + } + Ok((file, length)) +}