From 3cc003d961def33b209ab685aa2eb6b94ec9bb4a Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Mon, 10 Jun 2024 03:57:29 -0600 Subject: [PATCH 01/65] Starting WS connection --- fission/package-lock.json | 10 ++++ fission/package.json | 1 + fission/src/components/MainHUD.tsx | 6 +++ .../systems/simulation/SimulationSystem.ts | 3 ++ .../simulation/wpilib_brain/WPILibBrain.ts | 16 +++++++ .../wpilib_brain/WPILibConnector.ts | 48 +++++++++++++++++++ 6 files changed, 84 insertions(+) create mode 100644 fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts create mode 100644 fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts diff --git a/fission/package-lock.json b/fission/package-lock.json index a287e56772..adc9a867c7 100644 --- a/fission/package-lock.json +++ b/fission/package-lock.json @@ -11,6 +11,7 @@ "@barclah/jolt-physics": "^0.19.3", "@react-three/drei": "^9.96.5", "@react-three/fiber": "^8.15.15", + "async-mutex": "^0.5.0", "colord": "^2.9.3", "framer-motion": "^10.13.1", "react": "^18.2.0", @@ -2959,6 +2960,15 @@ "node": "*" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", diff --git a/fission/package.json b/fission/package.json index 76547f1c81..73a57cb2aa 100644 --- a/fission/package.json +++ b/fission/package.json @@ -19,6 +19,7 @@ "@barclah/jolt-physics": "^0.19.3", "@react-three/drei": "^9.96.5", "@react-three/fiber": "^8.15.15", + "async-mutex": "^0.5.0", "colord": "^2.9.3", "framer-motion": "^10.13.1", "react": "^18.2.0", diff --git a/fission/src/components/MainHUD.tsx b/fission/src/components/MainHUD.tsx index 48e20d215b..551c808834 100644 --- a/fission/src/components/MainHUD.tsx +++ b/fission/src/components/MainHUD.tsx @@ -12,6 +12,7 @@ import { motion } from "framer-motion" import logo from "../assets/autodesk_logo.png" import { ToastType, useToastContext } from "../ToastContext" import { Random } from "@/util/Random" +import WPILibConnector from "@/systems/simulation/wpilib_brain/WPILibConnector" type ButtonProps = { value: string @@ -150,6 +151,11 @@ const MainHUD: React.FC = () => { icon={} onClick={() => openModal("drivetrain")} /> + } + onClick={() => WPILibConnector.getInstance().then(_ => console.debug('WS connector loaded'))} + /> } diff --git a/fission/src/systems/simulation/SimulationSystem.ts b/fission/src/systems/simulation/SimulationSystem.ts index ce95d15d46..5c0c429786 100644 --- a/fission/src/systems/simulation/SimulationSystem.ts +++ b/fission/src/systems/simulation/SimulationSystem.ts @@ -11,6 +11,7 @@ import HingeStimulus from "./stimulus/HingeStimulus"; import WheelRotationStimulus from "./stimulus/WheelStimulus"; import SliderStimulus from "./stimulus/SliderStimulus"; import ChassisStimulus from "./stimulus/ChassisStimulus"; +// import WPILibConnector from "./wpilib_brain/WPILibConnector"; class SimulationSystem extends WorldSystem { @@ -20,6 +21,8 @@ class SimulationSystem extends WorldSystem { super(); this._simMechanisms = new Map(); + + // WPILibConnector.getInstance() } public RegisterMechanism(mechanism: Mechanism) { diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts new file mode 100644 index 0000000000..6521b8a990 --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -0,0 +1,16 @@ +import Mechanism from "@/systems/physics/Mechanism"; +import Brain from "../Brain"; + +class WPILibBrain extends Brain { + + constructor(mech: Mechanism) { + super(mech) + } + + public Update(deltaT: number): void { } + public Enable(): void { } + public Disable(): void { } + +} + +export default WPILibBrain \ No newline at end of file diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts b/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts new file mode 100644 index 0000000000..9cc31038a7 --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts @@ -0,0 +1,48 @@ +/* +WPILib Web Socket Simulation Spec +https://github.com/wpilibsuite/allwpilib/blob/main/simulation/halsim_ws_core/doc/hardware_ws_api.md +*/ + +import { Mutex } from 'async-mutex' + +const instanceMutex = new Mutex() + +class WPILibConnector { + + private static _connector: WPILibConnector | undefined + + private _socket: WebSocket + + private constructor() { + this._socket = new WebSocket('ws://localhost:3300/wpilibws') + + this._socket.addEventListener('open', () => { + this._socket.send('hello') + }) + + this._socket.addEventListener('message', (e) => { + console.debug(`WS MESSAGE: ${e.data}`) + }) + } + + private teardown() { + + } + + public static async getInstance(): Promise { + return instanceMutex.runExclusive(() => { + if (!this._connector) { + this._connector = new WPILibConnector() + } + return this._connector + }) + } + + public static killInstance() { + if (this._connector) { + this._connector.teardown() + } + } +} + +export default WPILibConnector \ No newline at end of file From 26d8c33978a67abefa1335f7a26d6563c2db7e7b Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Thu, 13 Jun 2024 16:55:38 -0600 Subject: [PATCH 02/65] Can now receive ws messages, need to parse --- fission/src/Synthesis.tsx | 7 ++ fission/src/components/MainHUD.tsx | 22 ++++-- fission/src/mirabuf/MirabufSceneObject.ts | 2 + .../simulation/wpilib_brain/WPILibBrain.ts | 67 ++++++++++++++++++- .../wpilib_brain/WPILibConnector.ts | 48 ------------- .../simulation/wpilib_brain/WPILibWSWorker.ts | 53 +++++++++++++++ 6 files changed, 144 insertions(+), 55 deletions(-) delete mode 100644 fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts create mode 100644 fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index b6746f0aff..06f0ad6cda 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -53,12 +53,16 @@ import ManageAssembliesModal from './modals/spawning/ManageAssembliesModal.tsx'; import World from './systems/World.ts'; import { AddRobotsModal, AddFieldsModal, SpawningModal } from './modals/spawning/SpawningModals.tsx'; +import WPILibWSWorker from '@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker' + const DEFAULT_MIRA_PATH = 'test_mira/Team_2471_(2018)_v7.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/Dozer_v2.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/PhysicsSpikeTest_v1.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/SliderTestFission_v2.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/HingeTestFission_v1.mira'; +export let worker: Worker | undefined = undefined + function Synthesis() { const { openModal, closeModal, getActiveModalElement } = useModalManager(initialModals) @@ -149,6 +153,9 @@ function Synthesis() { World.InitWorld(); + // worker = new Worker(new URL('systems/simulation/wpilib_brain/Worker.ts', import.meta.url)) + worker = new WPILibWSWorker() + let mira_path = DEFAULT_MIRA_PATH; const urlParams = new URLSearchParams(document.location.search); diff --git a/fission/src/components/MainHUD.tsx b/fission/src/components/MainHUD.tsx index 551c808834..e993ac2a13 100644 --- a/fission/src/components/MainHUD.tsx +++ b/fission/src/components/MainHUD.tsx @@ -12,7 +12,11 @@ import { motion } from "framer-motion" import logo from "../assets/autodesk_logo.png" import { ToastType, useToastContext } from "../ToastContext" import { Random } from "@/util/Random" -import WPILibConnector from "@/systems/simulation/wpilib_brain/WPILibConnector" +import { worker } from "@/Synthesis" +import World from "@/systems/World" +import SceneObject from "@/systems/scene/SceneObject" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import WPILibBrain from "@/systems/simulation/wpilib_brain/WPILibBrain" type ButtonProps = { value: string @@ -37,7 +41,7 @@ const MainHUDButton: React.FC = ({ {larger && icon} {!larger && ( {icon} @@ -48,7 +52,7 @@ const MainHUDButton: React.FC = ({ className={`px-2 ${larger ? "py-2" : "py-1 ml-6" } text-main-text cursor-pointer`} value={value} - onClick={onClick} + // onClick={onClick} /> ) @@ -154,7 +158,17 @@ const MainHUD: React.FC = () => { } - onClick={() => WPILibConnector.getInstance().then(_ => console.debug('WS connector loaded'))} + onClick={() => { + worker?.postMessage({ command: 'connect' }); + const miraObjs = [...World.SceneRenderer.sceneObjects.entries()] + .filter(x => x[1] instanceof MirabufSceneObject) + console.log(`Number of mirabuf scene objects: ${miraObjs.length}`) + if (miraObjs.length > 0) { + const mechanism = (miraObjs[0][1] as MirabufSceneObject).mechanism + const simLayer = World.SimulationSystem.GetSimulationLayer(mechanism) + simLayer?.SetBrain(new WPILibBrain(mechanism)) + } + }} /> = new Map() + +worker.addEventListener('message', (eventData: MessageEvent) => { + let data: any | undefined; + try { + data = JSON.parse(eventData.data) + } catch (e) { + console.warn(`Failed to parse data:\n${JSON.stringify(eventData.data)}`) + } + + if (!data) { + // console.log('No data, bailing out') + return + } + + switch (data.type.toLowerCase()) { + case 'solenoid': + if (!solenoids.has(data.device)) { + solenoids.set(data.device, new Solenoid(data.device)) + } + solenoids.get(data.device)?.Update(data.data) + break + case 'simdevice': + console.log(`SimDevice:\n${JSON.stringify(data, null, '\t')}`) + break + default: + // console.debug('Skipping Message') + break + } +}) + class WPILibBrain extends Brain { constructor(mech: Mechanism) { super(mech) } - public Update(deltaT: number): void { } - public Enable(): void { } - public Disable(): void { } + public Update(_: number): void { } + + public Enable(): void { + worker.postMessage({ command: 'connect' }) + } + + public Disable(): void { + worker.postMessage({ command: 'disconnect' }) + } } diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts b/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts deleted file mode 100644 index 9cc31038a7..0000000000 --- a/fission/src/systems/simulation/wpilib_brain/WPILibConnector.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* -WPILib Web Socket Simulation Spec -https://github.com/wpilibsuite/allwpilib/blob/main/simulation/halsim_ws_core/doc/hardware_ws_api.md -*/ - -import { Mutex } from 'async-mutex' - -const instanceMutex = new Mutex() - -class WPILibConnector { - - private static _connector: WPILibConnector | undefined - - private _socket: WebSocket - - private constructor() { - this._socket = new WebSocket('ws://localhost:3300/wpilibws') - - this._socket.addEventListener('open', () => { - this._socket.send('hello') - }) - - this._socket.addEventListener('message', (e) => { - console.debug(`WS MESSAGE: ${e.data}`) - }) - } - - private teardown() { - - } - - public static async getInstance(): Promise { - return instanceMutex.runExclusive(() => { - if (!this._connector) { - this._connector = new WPILibConnector() - } - return this._connector - }) - } - - public static killInstance() { - if (this._connector) { - this._connector.teardown() - } - } -} - -export default WPILibConnector \ No newline at end of file diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts new file mode 100644 index 0000000000..203e5c29a0 --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts @@ -0,0 +1,53 @@ +import { Mutex } from "async-mutex" + +let socket: WebSocket | undefined = undefined + +const connectMutex = new Mutex() + +async function tryConnect(port: number | undefined): Promise { + await connectMutex.runExclusive(() => { + if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { + socket?.close() + socket = undefined + } + + socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) + + socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }) }) + socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + + socket.addEventListener('message', onMessage) + }) +} + +async function tryDisconnect(): Promise { + await connectMutex.runExclusive(() => { + if (!socket) { + return + } + + socket.close() + socket = undefined + }) +} + +function onMessage(event: MessageEvent) { + // console.log(`${JSON.stringify(JSON.parse(event.data), null, '\t')}`) + self.postMessage(event.data) +} + +self.addEventListener('message', e => { + switch (e.data.command) { + case 'connect': + tryConnect(e.data.port) + break + case 'disconnect': + tryDisconnect() + break + default: + console.warn(`Unrecognized command '${e.data.command}'`) + break + } +}) + +console.log('Worker started') \ No newline at end of file From a491e3855b6f83f3a9130aa572e1f5578cc21cd5 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Fri, 21 Jun 2024 07:56:47 -0700 Subject: [PATCH 03/65] Non-deterministic behaviour because of course --- .../simulation/wpilib_brain/WPILibBrain.ts | 70 ++++++++++++++++++- .../simulation/wpilib_brain/WPILibWSWorker.ts | 33 +++++---- fission/src/ui/components/MainHUD.tsx | 2 +- 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index a942c63c79..a758549910 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -27,6 +27,64 @@ class Solenoid extends DeviceType { } const solenoids: Map = new Map() +class SimDevice extends DeviceType { + constructor(device: string) { + super(device) + } + + public Update(data: any): void { + + } +} +const simDevices: Map = new Map() + +class SparkMax extends SimDevice { + + private _sparkMaxId: number; + + constructor(device: string) { + super(device) + + console.debug('Spark Max Constructed') + + if (device.match(/spark max/i)?.length ?? 0 > 0) { + const endPos = device.indexOf(']') + const startPos = device.indexOf('[') + this._sparkMaxId = parseInt(device.substring(startPos + 1, endPos)) + } else { + throw new Error('Unrecognized Device ID') + } + } + + public Update(data: any): void { + super.Update(data) + + Object.entries(data).forEach(x => { + // if (x[0].startsWith('<')) { + // console.debug(`${x[0]} -> ${x[1]}`) + // } + + switch (x[0]) { + case '': + + break + default: + console.debug(`[${this._sparkMaxId}] ${x[0]} -> ${x[1]}`) + break + } + }) + } + + public SetPosition(val: number) { + worker.postMessage( + { + command: 'update', + data: { type: 'simdevice', device: this._device, data: { '>Position': val } } + } + ) + } +} + worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; try { @@ -48,10 +106,18 @@ worker.addEventListener('message', (eventData: MessageEvent) => { solenoids.get(data.device)?.Update(data.data) break case 'simdevice': - console.log(`SimDevice:\n${JSON.stringify(data, null, '\t')}`) + // console.debug(`SimDevice:\n${JSON.stringify(data, null, '\t')}`) + if (!simDevices.has(data.device)) { + if (data.device.match(/spark max/i)) { + simDevices.set(data.device, new SparkMax(data.device)) + } else { + simDevices.set(data.device, new SimDevice(data.device)) + } + } + simDevices.get(data.device)?.Update(data.data) break default: - // console.debug('Skipping Message') + // console.debug(`Unrecognized Message:\n${data}`) break } }) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts index 203e5c29a0..632eb8f99d 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts @@ -5,19 +5,21 @@ let socket: WebSocket | undefined = undefined const connectMutex = new Mutex() async function tryConnect(port: number | undefined): Promise { - await connectMutex.runExclusive(() => { - if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { - socket?.close() - socket = undefined - } - - socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) - - socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }) }) - socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) - - socket.addEventListener('message', onMessage) - }) + if (!connectMutex.isLocked()) { + await connectMutex.runExclusive(() => { + if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { + socket?.close() + socket = undefined + } + + socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) + + socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }); }) + socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + + socket.addEventListener('message', onMessage) + }) + } } async function tryDisconnect(): Promise { @@ -44,6 +46,11 @@ self.addEventListener('message', e => { case 'disconnect': tryDisconnect() break + case 'update': + if (socket) { + socket.send(JSON.stringify(e.data.data)) + } + break default: console.warn(`Unrecognized command '${e.data.command}'`) break diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index e1b42622f0..f81b3dde21 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -36,7 +36,7 @@ const MainHUDButton: React.FC = ({ value, icon, onClick, larger }) > {larger && icon} {!larger && ( - + {icon} )} From d769a9e4d8fbbba60b7390dcc6fbf092663a0111 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 28 Jun 2024 12:26:16 -0700 Subject: [PATCH 04/65] added docstring standard --- exporter/SynthesisFusionAddin/README.md | 59 +++++++++++++++++++++---- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/exporter/SynthesisFusionAddin/README.md b/exporter/SynthesisFusionAddin/README.md index 8f40acf4cd..4fc4933373 100644 --- a/exporter/SynthesisFusionAddin/README.md +++ b/exporter/SynthesisFusionAddin/README.md @@ -1,7 +1,9 @@ # Synthesis Exporter + This is a Addin for AutodeskĀ® Fusionā„¢ that will export a [Mirabuf](https://github.com/HiceS/mirabuf) usable by the Synthesis simulator. ## Features + - [x] Materials - [x] Apperances - [x] Instances @@ -30,16 +32,17 @@ We use `VSCode` Primarily, download it to interact with our code or use your own --- ### How to Build + Run + 1. See root [`README`](/README.md) on how to run `init` script 2. Open `Autodesk Fusion` 3. Select `UTILITIES` from the top bar 4. Click `ADD-INS` Button 5. Click `Add-Ins` tab at the top of Scripts and Add-Ins dialog -6. Press + Button under **My Add-Ins** +6. Press + Button under **My Add-Ins** 7. Navigate to the containing folder for this Addin and click open at bottom - _clone-directory_/synthesis/exporters/SynthesisFusionAddin 8. Synthesis should be an option - select it and click run at the bottom of the dialog 9. There should now be a button that says Synthesis in your utilities menu - - If there is no button there may be a problem - see below for [checking log file](#debug-non-start) + - If there is no button there may be a problem - see below for [checking log file](#debug-non-start) --- @@ -50,8 +53,8 @@ We use `VSCode` Primarily, download it to interact with our code or use your own Most of the runtime for the addin is saved under the `logs` directory in this folder - Open `logs/synthesis.log` - - If nothing appears something went very wrong (make a issue on this github) - - If something appears and you cannot solve it feel free to make an issue anyway and include the file + - If nothing appears something went very wrong (make a issue on this github) + - If something appears and you cannot solve it feel free to make an issue anyway and include the file #### General Debugging @@ -59,12 +62,12 @@ Most of the runtime for the addin is saved under the `logs` directory in this fo 2. Select `UTILITIES` from the top bar 3. Click `ADD-INS` Button 4. Click `Add-Ins` tab at the top of Scripts and Add-Ins dialog -5. Press + Button under **My Add-Ins** +5. Press + Button under **My Add-Ins** 6. Navigate to the containing folder for this Addin and click open at bottom - _clone-directory_/synthesis/exporters/SynthesisFusionAddin 7. Synthesis should be an option - select it and click `Debug` at the bottom of the dialog - - This is in a dropdown with the Run Button + - This is in a dropdown with the Run Button 8. This should open VSCode - Now run with `FN+5` - - Now you may add break points or debug at will + - Now you may add break points or debug at will --- @@ -84,4 +87,44 @@ We format using a Python formatter called `black` [![Code style: black](https:// - use `isort .` followed by `black .` to format all relevant exporter python files. - or, alternatively, run `python ./tools/format.py` to do this for you! -__Note: black will always ignore files in the proto/proto_out folder since google formats those__ +**Note: black will always ignore files in the proto/proto_out folder since google formats those** + +### Docstring standard + +This standard is inconsistently applied, and that's ok + +```python +def foo(bar: fizz="flower") -> Result[walrus, None]: + """ + Turns a fizz into a walrus + + Parameters: + bar - The fizz to be transformed (default = "flower") ; fizz standards are subject to change, old fizzes may no longer be valid + + Returns: + Success - She new walrus + Failure - None if the summoning fails ; the cause of failure will be printed, not returned + + Notes: + - Only works as expected if the bar arg isn't a palindrome or an anagram of coffee. Otherwise unexpected (but still valid) walruses may be returned + - Please do not name your fizz "rizz" either, it hurts the walrus's feelings + + TODO: Consult witch about inconsistent alchemical methods + """ + # More alchemical fizz -> walrus code + some_walrus = bar + "_coffee" + return some_walrus + +``` + +Note that not this much detail is necessary when writing function documentation, notes, defaults, and a differentiation between sucess and failure aren't always necessary. + +#### Where to list potential causes of failure? + +It depends on how many you can list + +- 1: In the failure return case +- 2-3: In the notes section +- 4+: In a dedicated "potential causes of failure section" between the "returns" and "notes" sections + +Additionally, printing the error instead of returning it is bad practice From 08d08ead763255b561eeae262ab9f642a45d8881 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 28 Jun 2024 12:27:14 -0700 Subject: [PATCH 05/65] initial exporter data management interface From c93deb4b0d5f85139924191509c6443f902181a1 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 3 Jul 2024 11:58:03 -0700 Subject: [PATCH 06/65] added docs to highest level upload file function --- fission/bun.lockb | Bin 305776 -> 307406 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/fission/bun.lockb b/fission/bun.lockb index e8c02486c5fdb040e68adeb96af72f2bad4b5df1..ca97e01c211453fc02dbe43b08f7a213e9aaeda5 100755 GIT binary patch delta 58746 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 PfDTpl!QAHhCl~)8pgdrB delta 57578 zcmeFadz?+>-~Ye&9-CRCiAfkqqMXXeIL&C-Lx>@1ikuRIVa8!FV;Gf&%3(;Ql`bkt z4v|#y5t5=(MyXVaq*5uAN>X(EJzr~GoB8zZ{`~Im<9FY`zgiEkd0+46bv~?Xt+i+N z^5%hazJKqWdzv)bG-g!pT~DTca{DjCmcRbW?ztO2Xq{8N-yOx3e}1`W!(C(V|8!-! zfOpL8UAuV7&mrIOema4`lu>{L?D(Ag>}+TjentHJ?5rW<@khr50%zfuVwJuR@v@yM zRkkbs+1Pg2IP9>3>G z3|8q6P+?u{c5DsoYHUsHT&(itV^v-@R!tr|reOS#f`Y)g=Xiecl%fiLAM>*E#^eqS z1hNYT7v_!{8u;kkK!7w;O0jDGIxPRDEV2A<>)(P^{0Pghx4a#;8oaUft75C;|4Cx; zZ?IJY-2zkg5Fl>Kt5{X|jP)PFDq#the^bWuqbha~R_VK972nGG7rR?Sm$iO|(bgzG zK)2N2=oA->ADWb%Hz9BvoS~btkU`ftt+eSoxs76K7tdr!6`Ng@JtT{T@-0J0@l!s> zGJsRYrNYb#p58z5aHfwxN(#Y&duUr2mtXi=St5&SSY9T#>RXG!L za);zler^FZVOKJREUfdU+;owhCR^Z;oUB~p1AW{BG1ZH^UF^pW&CVX1ls#@#JA4(F zjMchaL}ygZy;xN{zrJt1b)S@5Z~|ZH4_W@N2L5?6+xnBSTK6-^uXN*c^ClIHofMde zUpWvMG9{OQ5{yk6#R!ZKtZL*Z>~DD(n@IfkjI!ox<|Y2Byy8-S^7mPO*!o=?`{}mB zHO|KyyC0Ujp!laGfA}V4jT)6(Fg~!JXr0fGVQI&dUz_;Deiy!m|DMbIr85JoVL!jA zKOR%C8jpOenmv5fn86y&PRV{bWAcV*^aB&~v&PcRfX!cgx`5ogob3GE@dcc)qjHDm zjK7?W8ko^p!*hpZ6^yrM(Uc#X`3>3F+;51%>a4s4t5xr-Wujs&@ z9#?)N*b1%mwl@{wS0BwnlwK)-CbP1dhue?Y7FOLHMX~6b>5D}Y79%-+Eaoy zsE)!y?HYl=dlA14JFpt_XxrXR_w#RrEB(%Pe*WjIU4hkUKR9=2ZqkT?xPWtopJ27y zzC!JUm+*9!ufnSOMeeu?)oaY9FnQ;6vJpKw0$JgmH)bi`CO)u}y%=b3`kWmFi40T|eyEwLX z@qugnk&T|+qwpJ3Nsd}y7jaY0vekZtU$+*sMO*^cE-x#ZIorhJv3fL;x{Op1; zqb6hr5_|cVkh8Ix*#MToFP@TZ8_%XUY9#rO(;Lk)^&|}&l{GxtP=6aM=2FqmuQ=G> zuV7eTf4u&C1^l1+zo(&RQQ`ZPCtHfu$PK;0@5m>_s{>y%qURSA@NNLG-}mrWz!wAj zp1g~%9!4iVI)TxJ5nTw;1+szkHA(+G3rABjX^`LZA=qm8PL|)XY^Ncxbzxcah z{?Ztmo1evw6&QUJ)55WJxZlt5IoWx+!zKluwSIJi{WHgpADx?*J2tDJAiAOdb%Xwh zbgKTo5q|$>U^U=Vn0|QilyTr`Qyg; z{f?IN1`VqmV2ORn2&jeQu&Qv)SUWQX1=$4*;t;q7>>8|2n&{fEX7gQ%Rl_Ic`#npw z>FY2V;$t;@9P#)%36qFdhiWUGmRPi)5xL`Yl`yv;$s6UWg?<4QZH5q54IQ1GKRmk` zg;@SM=_l}PulO+R$h1p3c$l1rpoIwWVz$lRe> z`D`~shvw#w{fezfJs(H=wG^}RCXLP- NsV&FJLK2?0JT~b}_5*sxp&r9&^&3^oH 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 E16YI2!~g&Q From 8e1685e805fe4ed51f1cb2ead3831f05948485c3 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 5 Jul 2024 10:58:35 -0700 Subject: [PATCH 07/65] added exporter location --- .../src/Parser/ExporterOptions.py | 1 + .../src/UI/ConfigCommand.py | 101 ++++++++++-------- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 31ed4cd0c4..31b4381437 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -22,6 +22,7 @@ SignalType = Enum("SignalType", ["PWM", "CAN", "PASSIVE"]) ExportMode = Enum("ExportMode", ["ROBOT", "FIELD"]) # Dynamic / Static export PreferredUnits = Enum("PreferredUnits", ["METRIC", "IMPERIAL"]) +ExportLocation = Enum("ExportLocation", ["UPLOAD", "DOWNLOAD"]) @dataclass diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 6ea4e0b702..cde94b7d85 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -4,7 +4,7 @@ import logging import os -import platform +#import platform import traceback from enum import Enum @@ -24,6 +24,7 @@ SignalType, Wheel, WheelType, + ExportLocation ) from ..Parser.SynthesisParser.Parser import Parser from ..Parser.SynthesisParser.Utilities import guid_occurrence @@ -222,6 +223,17 @@ def notify(self, args): dropdownExportMode.tooltip = "Export Mode" dropdownExportMode.tooltipDescription = "
Does this object move dynamically?" + # ~~~~~~~~~~~~~~~~ EXPORT LOCATION ~~~~~~~~~~~~~~~~~~ + + dropdownExportLocation = inputs.addDropDownCommandInput("location", "Export Location", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle) + + upload: bool = exporterOptions.exportLocation == ExportLocation.UPLOAD + dropdownExportLocation.listItems.add("Upload", upload) + dropdownExportLocation.listItems.add("Download", not upload) + + dropdownExportLocation.tooltip = "Export Location" + dropdownExportLocation.tooltipDescription = "
Do you want to upload this mirabuf file to APS, or download it to your local machine?" + # ~~~~~~~~~~~~~~~~ WEIGHT CONFIGURATION ~~~~~~~~~~~~~~~~ """ Table for weight config. @@ -621,11 +633,11 @@ def notify(self, args): """ Creates the advanced tab, which is the parent container for internal command inputs """ - advancedSettings = INPUTS_ROOT.addTabCommandInput("advanced_settings", "Advanced") + advancedSettings: adsk.core.TabCommandInput = INPUTS_ROOT.addTabCommandInput("advanced_settings", "Advanced") advancedSettings.tooltip = ( "Additional Advanced Settings to change how your model will be translated into Unity." ) - a_input = advancedSettings.children + a_input: adsk.core.CommandInputs = advancedSettings.children # ~~~~~~~~~~~~~~~~ EXPORTER SETTINGS ~~~~~~~~~~~~~~~~ """ @@ -660,55 +672,36 @@ def notify(self, args): """ Physics settings group command """ - physicsSettings = a_input.addGroupCommandInput("physics_settings", "Physics Settings") + physicsSettings: adsk.core.GroupCommandInput = a_input.addGroupCommandInput("physics_settings", "Physics Settings") - physicsSettings.isExpanded = False + physicsSettings.isExpanded = True physicsSettings.isEnabled = True - physicsSettings.tooltip = "tooltip" # TODO: update tooltip - physics_settings = physicsSettings.children - - # AARD-1687 - # Should also be commented out / removed? - # This would cause problems elsewhere but I can't tell i f - # this is even being used. - frictionOverrideTable = self.createTableInput( - "friction_override_table", - "", - physics_settings, - 2, - "1:2", - 1, - columnSpacing=25, - ) - frictionOverrideTable.tablePresentationStyle = 2 - # frictionOverrideTable.isFullWidth = True + physicsSettings.tooltip = "Settings relating to the custom physics of the robot, like the wheel friction" + physics_settings: adsk.core.CommandInputs = physicsSettings.children - frictionOverride = self.createBooleanInput( + frictionOverrideInput = self.createBooleanInput( "friction_override", - "", + "Friction Override", physics_settings, - checked=False, + checked=exporterOptions.frictionOverride, # object is missing attribute tooltip="Manually override the default friction values on the bodies in the assembly.", enabled=True, isCheckBox=False, ) - frictionOverride.resourceFolder = IconPaths.stringIcons["friction_override-enabled"] - frictionOverride.isFullWidth = True + frictionOverrideInput.resourceFolder = IconPaths.stringIcons["friction_override-enabled"] + frictionOverrideInput.isFullWidth = True valueList = [1] for i in range(20): valueList.append(i / 20) - frictionCoeff = physics_settings.addFloatSliderListCommandInput( - "friction_coeff_override", "Friction Coefficient", "", valueList + frictionCoeffSlider: adsk.core.FloatSliderCommandInput = physics_settings.addFloatSliderListCommandInput( + "friction_override_coeff", "Friction Coefficient", "", valueList ) - frictionCoeff.isVisible = False - frictionCoeff.valueOne = 0.5 - frictionCoeff.tooltip = "Friction coefficient of field element." - frictionCoeff.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." - - frictionOverrideTable.addCommandInput(frictionOverride, 0, 0) - frictionOverrideTable.addCommandInput(frictionCoeff, 0, 1) + frictionCoeffSlider.isVisible = True + frictionCoeffSlider.valueOne = 0.5 + frictionCoeffSlider.tooltip = "Friction coefficient of field element." + frictionCoeffSlider.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." # ~~~~~~~~~~~~~~~~ JOINT SETTINGS ~~~~~~~~~~~~~~~~ """ @@ -999,12 +992,7 @@ def notify(self, args): self.log.error("Could not execute configuration due to failure") return - export_as_part_boolean = ( - eventArgs.command.commandInputs.itemById("advanced_settings") - .children.itemById("exporter_settings") - .children.itemById("export_as_part") - ).value - + processedFileName = gm.app.activeDocument.name.replace(" ", "_") dropdownExportMode = INPUTS_ROOT.itemById("mode") if dropdownExportMode.selectedItem.index == 0: @@ -1038,7 +1026,9 @@ def notify(self, args): _exportJoints = [] # all selected joints, formatted for parseOptions _exportGamepieces = [] # TODO work on the code to populate Gamepiece _robotWeight = float - _mode = ExportMode.ROBOT + _mode: ExportMode + _location: ExportLocation + """ Loops through all rows in the wheel table to extract all the input values @@ -1167,12 +1157,30 @@ def notify(self, args): elif dropdownExportMode.selectedItem.index == 1: _mode = ExportMode.FIELD + """ + Export Location + """ + dropdownExportLocation = INPUTS_ROOT.itemById("location") + if dropdownExportLocation.select.index == 0: + _location = ExportLocation.UPLOAD + elif dropdownExportLocation.select.index == 1: + _location = ExportLocation.DOWNLOAD + + """ + Advanced Settings + """ global compress compress = ( eventArgs.command.commandInputs.itemById("advanced_settings") .children.itemById("exporter_settings") .children.itemById("compress") ).value + + export_as_part_boolean = ( + eventArgs.command.commandInputs.itemById("advanced_settings") + .children.itemById("exporter_settings") + .children.itemById("export_as_part") + ).value exporterOptions = ExporterOptions( savepath, @@ -1185,11 +1193,12 @@ def notify(self, args): preferredUnits=selectedUnits, robotWeight=_robotWeight, exportMode=_mode, + exportLocation=_location, compressOutput=compress, exportAsPart=export_as_part_boolean, ) - Parser(exporterOptions).export() + _: bool = Parser(exporterOptions).export() exporterOptions.writeToDesign() except: if gm.ui: @@ -1628,7 +1637,7 @@ def notify(self, args): inputs = cmdInput.commandInputs onSelect = gm.handlers[3] - frictionCoeff = INPUTS_ROOT.itemById("friction_coeff_override") + frictionCoeff = INPUTS_ROOT.itemById("friction_override_coeff") wheelSelect = inputs.itemById("wheel_select") jointSelect = inputs.itemById("joint_select") From 0f5c97cb836a5ccdd06ff8fafa8993a9f4f4c6a8 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 5 Jul 2024 15:13:29 -0700 Subject: [PATCH 08/65] imported upload code to parser --- .../src/Parser/ExporterOptions.py | 2 ++ .../src/Parser/SynthesisParser/Parser.py | 17 +++++++++++++++-- .../src/UI/ConfigCommand.py | 6 +++--- .../SynthesisFusionAddin/src/general_imports.py | 3 +++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 31b4381437..df5d21c9c1 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -96,6 +96,8 @@ class ExporterOptions: compressOutput: bool = field(default=True) exportAsPart: bool = field(default=False) + exportLocation: ExportLocation = field(default=ExportLocation.UPLOAD) + hierarchy: ModelHierarchy = field(default=ModelHierarchy.FusionAssembly) visualQuality: TriangleMeshQualityOptions = field(default=TriangleMeshQualityOptions.LowQualityTriangleMesh) physicalDepth: PhysicalDepth = field(default=PhysicalDepth.AllOccurrence) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index a925f5b119..bf362dc0a6 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -9,9 +9,10 @@ from ...general_imports import * from ...UI.Camera import captureThumbnail, clearIconCache -from ..ExporterOptions import ExporterOptions, ExportMode +from ..ExporterOptions import ExporterOptions, ExportMode, ExportLocation from . import Components, JointHierarchy, Joints, Materials, PDMessage from .Utilities import * +from ...temp_file_upload import upload_mirabuf # This line causes everything to break class Parser: @@ -179,7 +180,19 @@ def export(self) -> bool: f.write(assembly_out.SerializeToString()) f.close() - progressDialog.hide() + # Upload Mirabuf File to APS + if self.exporterOptions.exportLocation == ExportLocation.UPLOAD: + self.logger.debug("Uploading file to APS") + project = app.data.dataProjects.item(0) + if not project.isValid: + return False # add throw later + project_id = project.id + folder_id = project.rootFolder.id + file_location = self.exporterOptions.fileLocation + if upload_mirabuf(project_id, folder_id, file_location).is_err(): + gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later + + _ = progressDialog.hide() if DEBUG: part_defs = assembly_out.data.parts.part_definitions diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index cde94b7d85..30010c9a33 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -683,7 +683,7 @@ def notify(self, args): "friction_override", "Friction Override", physics_settings, - checked=exporterOptions.frictionOverride, # object is missing attribute + checked=True, # object is missing attribute tooltip="Manually override the default friction values on the bodies in the assembly.", enabled=True, isCheckBox=False, @@ -1161,9 +1161,9 @@ def notify(self, args): Export Location """ dropdownExportLocation = INPUTS_ROOT.itemById("location") - if dropdownExportLocation.select.index == 0: + if dropdownExportLocation.selectedItem.index == 0: _location = ExportLocation.UPLOAD - elif dropdownExportLocation.select.index == 1: + elif dropdownExportLocation.selectedItem.index == 1: _location = ExportLocation.DOWNLOAD """ diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index 042e4618ea..87a6874c53 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -9,6 +9,9 @@ from time import time from types import FunctionType +from requests import get, post +from result import Ok, Err, is_err + import adsk.core import adsk.fusion From 1b59d9459d1fb96242e36bc6ecdbd042c4543c15 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Fri, 5 Jul 2024 19:06:32 -0600 Subject: [PATCH 09/65] Working on View panle --- fission/src/Synthesis.tsx | 2 + .../simulation/wpilib_brain/WPILibBrain.ts | 157 +++++++++--------- fission/src/ui/components/MainHUD.tsx | 7 +- fission/src/ui/panels/WSViewPanel.tsx | 42 +++++ 4 files changed, 129 insertions(+), 79 deletions(-) create mode 100644 fission/src/ui/panels/WSViewPanel.tsx diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index bc66c24396..982dc31708 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -55,6 +55,7 @@ import { AddRobotsModal, AddFieldsModal, SpawningModal } from '@/modals/spawning import ImportMirabufModal from '@/modals/mirabuf/ImportMirabufModal.tsx'; import WPILibWSWorker from '@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker' +import WSViewPanel from './ui/panels/WSViewPanel.tsx'; const DEFAULT_MIRA_PATH = '/api/mira/Robots/Team 2471 (2018)_v7.mira'; @@ -209,6 +210,7 @@ const initialPanels: ReactElement[] = [ , , , + , ] export default Synthesis diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index a758549910..7f7985b37a 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -6,84 +6,88 @@ import WPILibWSWorker from './WPILibWSWorker?worker' const worker = new WPILibWSWorker() -abstract class DeviceType { - protected _device: string +export const PWM_UPDATE_EVENT_KEY = "ws/pwm-update" + +// abstract class DeviceType { +// protected _device: string - constructor(device: string) { - this._device = device - } +// constructor(device: string) { +// this._device = device +// } - public abstract Update(data: any): void -} +// public abstract Update(data: any): void +// } -class Solenoid extends DeviceType { - constructor(device: string) { - super(device) - } +// class Solenoid extends DeviceType { +// constructor(device: string) { +// super(device) +// } - public Update(data: any): void { +// public Update(data: any): void { - } -} -const solenoids: Map = new Map() +// } +// } +// const solenoids: Map = new Map() -class SimDevice extends DeviceType { - constructor(device: string) { - super(device) - } +// class SimDevice extends DeviceType { +// constructor(device: string) { +// super(device) +// } - public Update(data: any): void { +// public Update(data: any): void { - } -} -const simDevices: Map = new Map() +// } +// } +// const simDevices: Map = new Map() -class SparkMax extends SimDevice { +// class SparkMax extends SimDevice { - private _sparkMaxId: number; +// private _sparkMaxId: number; - constructor(device: string) { - super(device) +// constructor(device: string) { +// super(device) - console.debug('Spark Max Constructed') +// console.debug('Spark Max Constructed') - if (device.match(/spark max/i)?.length ?? 0 > 0) { - const endPos = device.indexOf(']') - const startPos = device.indexOf('[') - this._sparkMaxId = parseInt(device.substring(startPos + 1, endPos)) - } else { - throw new Error('Unrecognized Device ID') - } - } - - public Update(data: any): void { - super.Update(data) - - Object.entries(data).forEach(x => { - // if (x[0].startsWith('<')) { - // console.debug(`${x[0]} -> ${x[1]}`) - // } - - switch (x[0]) { - case '': - - break - default: - console.debug(`[${this._sparkMaxId}] ${x[0]} -> ${x[1]}`) - break - } - }) - } - - public SetPosition(val: number) { - worker.postMessage( - { - command: 'update', - data: { type: 'simdevice', device: this._device, data: { '>Position': val } } - } - ) - } -} +// if (device.match(/spark max/i)?.length ?? 0 > 0) { +// const endPos = device.indexOf(']') +// const startPos = device.indexOf('[') +// this._sparkMaxId = parseInt(device.substring(startPos + 1, endPos)) +// } else { +// throw new Error('Unrecognized Device ID') +// } +// } + +// public Update(data: any): void { +// super.Update(data) + +// Object.entries(data).forEach(x => { +// // if (x[0].startsWith('<')) { +// // console.debug(`${x[0]} -> ${x[1]}`) +// // } + +// switch (x[0]) { +// case '': + +// break +// default: +// console.debug(`[${this._sparkMaxId}] ${x[0]} -> ${x[1]}`) +// break +// } +// }) +// } + +// public SetPosition(val: number) { +// worker.postMessage( +// { +// command: 'update', +// data: { type: 'simdevice', device: this._device, data: { '>Position': val } } +// } +// ) +// } +// } + +export const pwmMap = new Map() worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; @@ -98,23 +102,20 @@ worker.addEventListener('message', (eventData: MessageEvent) => { return } + const device = data.device + const updateData = data.data + switch (data.type.toLowerCase()) { + case 'pwm': { // ESLint wants curly brackets apparently. Doesn't like scoped variables with only colon? + const currentData = pwmMap.get(device) ?? {} + Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + pwmMap.set(device, currentData) + window.dispatchEvent(new Event(PWM_UPDATE_EVENT_KEY)) + break + } case 'solenoid': - if (!solenoids.has(data.device)) { - solenoids.set(data.device, new Solenoid(data.device)) - } - solenoids.get(data.device)?.Update(data.data) break case 'simdevice': - // console.debug(`SimDevice:\n${JSON.stringify(data, null, '\t')}`) - if (!simDevices.has(data.device)) { - if (data.device.match(/spark max/i)) { - simDevices.set(data.device, new SparkMax(data.device)) - } else { - simDevices.set(data.device, new SimDevice(data.device)) - } - } - simDevices.get(data.device)?.Update(data.data) break default: // console.debug(`Unrecognized Message:\n${data}`) diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index f81b3dde21..76da3a2248 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react" import { BsCodeSquare } from "react-icons/bs" import { FaCar, FaGear, FaMagnifyingGlass, FaPlus } from "react-icons/fa6" import { BiMenuAltLeft } from "react-icons/bi" -import { GrFormClose } from "react-icons/gr" +import { GrConnect, GrFormClose } from "react-icons/gr" import { GiSteeringWheel } from "react-icons/gi" import { HiDownload } from "react-icons/hi" import { IoGameControllerOutline, IoPeople } from "react-icons/io5" @@ -120,6 +120,11 @@ const MainHUD: React.FC = () => { icon={} onClick={() => openModal("import-mirabuf")} /> + } + onClick={() => openPanel("ws-view")} + />
= ({ panelId }) => { + + const onPwmUpdate = useCallback((_: Event) => { + + }, []) + + useEffect(() => { + window.addEventListener(PWM_UPDATE_EVENT_KEY, onPwmUpdate) + + return () => { + window.removeEventListener(PWM_UPDATE_EVENT_KEY, onPwmUpdate) + } + }, [onPwmUpdate]) + + return ( + } + panelId={panelId} + openLocation="right" + sidePadding={4} + > + + Sup + + + ) +} + +export default WSViewPanel From a4c45b68140f204d60a1013ca17cd060cc669c54 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Fri, 5 Jul 2024 19:23:52 -0600 Subject: [PATCH 10/65] Fixed dependency installation --- exporter/SynthesisFusionAddin/Synthesis.py | 4 +++- exporter/SynthesisFusionAddin/proto/deps.py | 25 +++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 335a2416ce..1f53f69e93 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -6,13 +6,15 @@ import adsk.core +from .proto.deps import installDependencies +installDependencies() + from .src.configure import setAnalytics, unload_config from .src.general_imports import APP_NAME, DESCRIPTION, INTERNAL_ID, gm, root_logger from .src.Types.OString import OString from .src.UI import HUI, Camera, ConfigCommand, Handlers, Helper, MarkingMenu from .src.UI.Toolbar import Toolbar - def run(_): """## Entry point to application from Fusion. diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index ea6da5a406..887649c1d6 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -10,7 +10,6 @@ system = platform.system() - def getPythonFolder() -> str: """Retreives the folder that contains the Autodesk python executable @@ -166,14 +165,22 @@ def _checkDeps() -> bool: except ImportError: return False +""" +Checks for, and installs if need be, the dependencies needed by the Synthesis Exporter. Will error if it cannot install the dependencies +correctly. This should crash the exporter, since most of the exporter needs these dependencies to function in +the first place. +""" +def installDependencies(): + try: + import logging.handlers -try: - import logging.handlers + import google.protobuf + import pkg_resources - import google.protobuf - import pkg_resources + from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 - from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 -except ImportError or ModuleNotFoundError: - installCross(["protobuf==4.23.3"]) - from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 + from requests import get, post + from result import Ok, Err, is_err + except ImportError or ModuleNotFoundError: + installCross(["protobuf==4.23.3", "requests==2.32.3", "result==0.17.0"]) + from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 From 52e5a162aad976a13f11ba2ed802f0ac4b1142c6 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 10:35:30 -0600 Subject: [PATCH 11/65] Discovering some short comings with the WPILib WS sim --- .../simulation/wpilib_brain/WPILibBrain.ts | 39 ++++++++-- .../simulation/wpilib_brain/WPILibWSWorker.ts | 25 +++---- fission/src/ui/components/MainHUD.tsx | 2 +- fission/src/ui/panels/WSViewPanel.tsx | 75 ++++++++++++++++--- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index 7f7985b37a..f7141aa8e8 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -6,7 +6,7 @@ import WPILibWSWorker from './WPILibWSWorker?worker' const worker = new WPILibWSWorker() -export const PWM_UPDATE_EVENT_KEY = "ws/pwm-update" +export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" // abstract class DeviceType { // protected _device: string @@ -88,35 +88,62 @@ export const PWM_UPDATE_EVENT_KEY = "ws/pwm-update" // } export const pwmMap = new Map() +export const simDeviceMap = new Map() +export const canMotorMap = new Map() worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; try { - data = JSON.parse(eventData.data) + if (typeof(eventData.data) == 'object') { + data = eventData.data + } else { + data = JSON.parse(eventData.data) + } } catch (e) { console.warn(`Failed to parse data:\n${JSON.stringify(eventData.data)}`) } - if (!data) { - // console.log('No data, bailing out') + if (!data || !data.type) { + console.log('No data, bailing out') return } + // console.debug(data) + const device = data.device const updateData = data.data switch (data.type.toLowerCase()) { case 'pwm': { // ESLint wants curly brackets apparently. Doesn't like scoped variables with only colon? + console.debug('pwm') const currentData = pwmMap.get(device) ?? {} Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) pwmMap.set(device, currentData) - window.dispatchEvent(new Event(PWM_UPDATE_EVENT_KEY)) + + window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) break } case 'solenoid': + console.debug('solenoid') + break + case 'simdevice': { + console.debug('simdevice') + const currentData = simDeviceMap.get(device) ?? {} + Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + simDeviceMap.set(device, currentData) + + window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) break - case 'simdevice': + } + case 'canmotor': { + console.debug('canmotor') + const currentData = canMotorMap.get(device) ?? {} + Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + canMotorMap.set(device, currentData) + + window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) break + } default: // console.debug(`Unrecognized Message:\n${data}`) break diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts index 632eb8f99d..784db32306 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts @@ -5,21 +5,18 @@ let socket: WebSocket | undefined = undefined const connectMutex = new Mutex() async function tryConnect(port: number | undefined): Promise { - if (!connectMutex.isLocked()) { - await connectMutex.runExclusive(() => { - if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { - socket?.close() - socket = undefined - } - - socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) + await connectMutex.runExclusive(() => { + if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { + return + } - socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }); }) - socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) - socket.addEventListener('message', onMessage) - }) - } + socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }); }) + socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + + socket.addEventListener('message', onMessage) + }).then(() => console.debug('Mutex released')) } async function tryDisconnect(): Promise { @@ -28,7 +25,7 @@ async function tryDisconnect(): Promise { return } - socket.close() + socket?.close() socket = undefined }) } diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index 76da3a2248..fab368a401 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -143,7 +143,7 @@ const MainHUD: React.FC = () => { value={"WS Test"} icon={} onClick={() => { - worker?.postMessage({ command: 'connect' }); + // worker?.postMessage({ command: 'connect' }); const miraObjs = [...World.SceneRenderer.sceneObjects.entries()] .filter(x => x[1] instanceof MirabufSceneObject) console.log(`Number of mirabuf scene objects: ${miraObjs.length}`) diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index fc87e15965..7c77c18eed 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -1,23 +1,65 @@ import Panel, { PanelPropsImpl } from "@/components/Panel" -import { PWM_UPDATE_EVENT_KEY } from "@/systems/simulation/wpilib_brain/WPILibBrain" -import { Typography } from "@mui/material" -import { useCallback, useEffect } from "react" +import { SIM_MAP_UPDATE_EVENT, canMotorMap, pwmMap, simDeviceMap } from "@/systems/simulation/wpilib_brain/WPILibBrain" +import { styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" +import { useCallback, useEffect, useState } from "react" import { GrConnect } from "react-icons/gr" import { IoPeople } from "react-icons/io5" +const TypoStyled = styled(Typography)({ + fontFamily: "Artifakt Legend", + fontWeight: 300, + color: "white" +}) + +function generateTableBody() { + return ( + + {[...pwmMap.entries()].filter(x => x[1][" { + return ( + + PWM + {x[0]} + {JSON.stringify(x[1])} + + ) + })} + {[...simDeviceMap.entries()].map(x => { + return ( + + SimDevice + {x[0]} + {JSON.stringify(x[1])} + + ) + })} + {[...canMotorMap.entries()].map(x => { + return ( + + CAN Motor + {x[0]} + {JSON.stringify(x[1])} + + ) + })} + + ) +} + const WSViewPanel: React.FC = ({ panelId }) => { - const onPwmUpdate = useCallback((_: Event) => { + const [tb, setTb] = useState(generateTableBody()) + const onSimMapUpdate = useCallback((_: Event) => { + setTb(generateTableBody()) }, []) useEffect(() => { - window.addEventListener(PWM_UPDATE_EVENT_KEY, onPwmUpdate) + window.addEventListener(SIM_MAP_UPDATE_EVENT, onSimMapUpdate) return () => { - window.removeEventListener(PWM_UPDATE_EVENT_KEY, onPwmUpdate) + window.removeEventListener(SIM_MAP_UPDATE_EVENT, onSimMapUpdate) } - }, [onPwmUpdate]) + }, [onSimMapUpdate]) return ( = ({ panelId }) => { openLocation="right" sidePadding={4} > - - Sup - + + + + Type + Device + Data + + + {tb} +
+
) } From dd28d7aa5791f62dec57a5a73df446c6d1ba8cc2 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 11:10:41 -0600 Subject: [PATCH 12/65] CAN Motors and Encoders giving ok ish data. SparkMaxes aren't supported, however --- .../simulation/wpilib_brain/WPILibBrain.ts | 60 +++++++++++-------- fission/src/ui/panels/WSViewPanel.tsx | 24 +++++--- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index f7141aa8e8..e699509752 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -8,6 +8,13 @@ const worker = new WPILibWSWorker() export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" +export type SimType = + | 'pwm' + | 'canmotor' + | 'solenoid' + | 'simdevice' + | 'canencoder' + // abstract class DeviceType { // protected _device: string @@ -87,9 +94,7 @@ export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" // } // } -export const pwmMap = new Map() -export const simDeviceMap = new Map() -export const canMotorMap = new Map() +export const simMap = new Map>() worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; @@ -114,42 +119,49 @@ worker.addEventListener('message', (eventData: MessageEvent) => { const updateData = data.data switch (data.type.toLowerCase()) { - case 'pwm': { // ESLint wants curly brackets apparently. Doesn't like scoped variables with only colon? + case 'pwm': console.debug('pwm') - const currentData = pwmMap.get(device) ?? {} - Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) - pwmMap.set(device, currentData) - - window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) + UpdateSimMap('pwm', device, updateData) break - } case 'solenoid': console.debug('solenoid') + UpdateSimMap('solenoid', device, updateData) break - case 'simdevice': { + case 'simdevice': console.debug('simdevice') - const currentData = simDeviceMap.get(device) ?? {} - Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) - simDeviceMap.set(device, currentData) - - window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) + UpdateSimMap('simdevice', device, updateData) break - } - case 'canmotor': { + case 'canmotor': console.debug('canmotor') - const currentData = canMotorMap.get(device) ?? {} - Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) - canMotorMap.set(device, currentData) - - window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) + UpdateSimMap('canmotor', device, updateData) + break + case 'canencoder': + console.debug('canencoder') + UpdateSimMap('canencoder', device, updateData) break - } default: // console.debug(`Unrecognized Message:\n${data}`) break } }) +function UpdateSimMap(type: SimType, device: string, updateData: any) { + let typeMap = simMap.get(type) + if (!typeMap) { + typeMap = new Map() + simMap.set(type, typeMap) + } + + let currentData = typeMap.get(device) + if (!currentData) { + currentData = {} + typeMap.set(device, currentData) + } + Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + + window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) +} + class WPILibBrain extends Brain { constructor(mech: Mechanism) { diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index 7c77c18eed..36356243de 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -1,9 +1,8 @@ import Panel, { PanelPropsImpl } from "@/components/Panel" -import { SIM_MAP_UPDATE_EVENT, canMotorMap, pwmMap, simDeviceMap } from "@/systems/simulation/wpilib_brain/WPILibBrain" +import { SIM_MAP_UPDATE_EVENT, simMap } from "@/systems/simulation/wpilib_brain/WPILibBrain" import { styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" import { useCallback, useEffect, useState } from "react" import { GrConnect } from "react-icons/gr" -import { IoPeople } from "react-icons/io5" const TypoStyled = styled(Typography)({ fontFamily: "Artifakt Legend", @@ -14,7 +13,7 @@ const TypoStyled = styled(Typography)({ function generateTableBody() { return ( - {[...pwmMap.entries()].filter(x => x[1][" { + {simMap.has('pwm') ? [...simMap.get('pwm')!.entries()].filter(x => x[1][" { return ( PWM @@ -22,8 +21,8 @@ function generateTableBody() { {JSON.stringify(x[1])} ) - })} - {[...simDeviceMap.entries()].map(x => { + }) : <>} + {simMap.has('simdevice') ? [...simMap.get('simdevice')!.entries()].map(x => { return ( SimDevice @@ -31,8 +30,8 @@ function generateTableBody() { {JSON.stringify(x[1])} ) - })} - {[...canMotorMap.entries()].map(x => { + }): <>} + {simMap.has('canmotor') ? [...simMap.get('canmotor')!.entries()].map(x => { return ( CAN Motor @@ -40,7 +39,16 @@ function generateTableBody() { {JSON.stringify(x[1])} ) - })} + }) : <>} + {simMap.has('canencoder') ? [...simMap.get('canencoder')!.entries()].map(x => { + return ( + + CAN Encoder + {x[0]} + {JSON.stringify(x[1])} + + ) + }) : <>} ) } From 63648371344077649057c40cad0bcbdeb93561bb Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Sat, 6 Jul 2024 10:32:12 -0700 Subject: [PATCH 13/65] fix folder ids --- .../SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index bf362dc0a6..dfab55eb5e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -185,10 +185,12 @@ def export(self) -> bool: self.logger.debug("Uploading file to APS") project = app.data.dataProjects.item(0) if not project.isValid: + gm.ui.messageBox("Project is invalid", "") return False # add throw later project_id = project.id folder_id = project.rootFolder.id file_location = self.exporterOptions.fileLocation + gm.ui.messageBox(f"project: {project_id}\nfolder: {folder_id}\nfile: {file_location}", "ARGS:") if upload_mirabuf(project_id, folder_id, file_location).is_err(): gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later From eb0277737fa2320bc7c63828e4eb4389b2c89d2c Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 12:39:10 -0600 Subject: [PATCH 14/65] Slowly discovering this may not be the solution --- .../simulation/wpilib_brain/WPILibBrain.ts | 214 +++++++++++------- fission/src/ui/components/MainHUD.tsx | 1 - fission/src/ui/panels/WSViewPanel.tsx | 8 +- 3 files changed, 133 insertions(+), 90 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index e699509752..ed5d8bd1d5 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -8,93 +8,133 @@ const worker = new WPILibWSWorker() export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" +const PWM_SPEED = "" ? FieldType.Both : FieldType.Read + case '>': + return FieldType.Write + default: + return FieldType.Unknown + } +} -// public abstract Update(data: any): void -// } +export const simMap = new Map>() -// class Solenoid extends DeviceType { -// constructor(device: string) { -// super(device) -// } +class SimGeneric { + private constructor() { } -// public Update(data: any): void { + public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { + const fieldType = GetFieldType(field) + if (fieldType != FieldType.Read && fieldType != FieldType.Both) { + console.warn(`Field '${field}' is not a read or both field type`) + return undefined + } -// } -// } -// const solenoids: Map = new Map() + const map = simMap.get(simType) + if (!map) { + console.warn(`No '${simType}' devices found`) + return undefined + } + + const data = map.get(device) + if (!data) { + console.warn(`No '${simType}' device '${device}' found`) + return undefined + } + + return data[field] as (T | undefined) ?? defaultValue + } + + public static Set(simType: SimType, device: string, field: string, value: T): boolean { + const fieldType = GetFieldType(field) + if (fieldType != FieldType.Write && fieldType != FieldType.Both) { + console.warn(`Field '${field}' is not a write or both field type`) + return false + } -// class SimDevice extends DeviceType { -// constructor(device: string) { -// super(device) -// } + const map = simMap.get(simType) + if (!map) { + console.warn(`No '${simType}' devices found`) + return false + } -// public Update(data: any): void { + const data = map.get(device) + if (!data) { + console.warn(`No '${simType}' device '${device}' found`) + return false + } -// } -// } -// const simDevices: Map = new Map() + const selectedData: any = {} + selectedData[field] = value + + data[field] = value + worker.postMessage({ + command: 'update', + data: { + type: simType, + device: device, + data: selectedData + } + }) + return true + } +} -// class SparkMax extends SimDevice { +class SimPWM { + private constructor() { } -// private _sparkMaxId: number; + public static GetSpeed(device: string): number | undefined { + return SimGeneric.Get('PWM', device, PWM_SPEED, 0.0) + } -// constructor(device: string) { -// super(device) + public static GetPosition(device: string): number | undefined { + return SimGeneric.Get('PWM', device, PWM_POSITION, 0.0) + } +} -// console.debug('Spark Max Constructed') - -// if (device.match(/spark max/i)?.length ?? 0 > 0) { -// const endPos = device.indexOf(']') -// const startPos = device.indexOf('[') -// this._sparkMaxId = parseInt(device.substring(startPos + 1, endPos)) -// } else { -// throw new Error('Unrecognized Device ID') -// } -// } - -// public Update(data: any): void { -// super.Update(data) - -// Object.entries(data).forEach(x => { -// // if (x[0].startsWith('<')) { -// // console.debug(`${x[0]} -> ${x[1]}`) -// // } - -// switch (x[0]) { -// case '': - -// break -// default: -// console.debug(`[${this._sparkMaxId}] ${x[0]} -> ${x[1]}`) -// break -// } -// }) -// } - -// public SetPosition(val: number) { -// worker.postMessage( -// { -// command: 'update', -// data: { type: 'simdevice', device: this._device, data: { '>Position': val } } -// } -// ) -// } -// } +class SimCANMotor { + private constructor() { } -export const simMap = new Map>() + public static GetDutyCycle(device: string): number | undefined { + return SimGeneric.Get('CANMotor', device, CANMOTOR_DUTY_CYCLE, 0.0) + } + + public static SetSupplyVoltage(device: string, voltage: number): boolean { + return SimGeneric.Set('CANMotor', device, CANMOTOR_SUPPLY_VOLTAGE, voltage) + } +} + +class SimCANEncoder { + private constructor() { } + + public static SetRawInputPosition(device: string, rawInputPosition: number): boolean { + return SimGeneric.Set('CANEncoder', device, CANENCODER_RAW_INPUT_POSITION, rawInputPosition) + } +} + +let flag = false worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; @@ -118,26 +158,30 @@ worker.addEventListener('message', (eventData: MessageEvent) => { const device = data.device const updateData = data.data - switch (data.type.toLowerCase()) { - case 'pwm': + switch (data.type) { + case 'PWM': console.debug('pwm') - UpdateSimMap('pwm', device, updateData) + UpdateSimMap('PWM', device, updateData) break - case 'solenoid': + case 'Solenoid': console.debug('solenoid') - UpdateSimMap('solenoid', device, updateData) + UpdateSimMap('Solenoid', device, updateData) break - case 'simdevice': + case 'SimDevice': console.debug('simdevice') - UpdateSimMap('simdevice', device, updateData) + UpdateSimMap('SimDevice', device, updateData) + if (!flag) { + flag = true + SimGeneric.Set('SimDevice', device, '>init', true) + } break - case 'canmotor': + case 'CANMotor': console.debug('canmotor') - UpdateSimMap('canmotor', device, updateData) + UpdateSimMap('CANMotor', device, updateData) break - case 'canencoder': + case 'CANEncoder': console.debug('canencoder') - UpdateSimMap('canencoder', device, updateData) + UpdateSimMap('CANEncoder', device, updateData) break default: // console.debug(`Unrecognized Message:\n${data}`) diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index fab368a401..196d794a2e 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -12,7 +12,6 @@ import { motion } from "framer-motion" import logo from "@/assets/autodesk_logo.png" import { ToastType, useToastContext } from "@/ui/ToastContext" import { Random } from "@/util/Random" -import { worker } from "@/Synthesis" import World from "@/systems/World" import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" import WPILibBrain from "@/systems/simulation/wpilib_brain/WPILibBrain" diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index 36356243de..693581488d 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -13,7 +13,7 @@ const TypoStyled = styled(Typography)({ function generateTableBody() { return ( - {simMap.has('pwm') ? [...simMap.get('pwm')!.entries()].filter(x => x[1][" { + {simMap.has('PWM') ? [...simMap.get('PWM')!.entries()].filter(x => x[1][" { return ( PWM @@ -22,7 +22,7 @@ function generateTableBody() { ) }) : <>} - {simMap.has('simdevice') ? [...simMap.get('simdevice')!.entries()].map(x => { + {simMap.has('SimDevice') ? [...simMap.get('SimDevice')!.entries()].map(x => { return ( SimDevice @@ -31,7 +31,7 @@ function generateTableBody() { ) }): <>} - {simMap.has('canmotor') ? [...simMap.get('canmotor')!.entries()].map(x => { + {simMap.has('CANMotor') ? [...simMap.get('CANMotor')!.entries()].map(x => { return ( CAN Motor @@ -40,7 +40,7 @@ function generateTableBody() { ) }) : <>} - {simMap.has('canencoder') ? [...simMap.get('canencoder')!.entries()].map(x => { + {simMap.has('CANEncoder') ? [...simMap.get('CANEncoder')!.entries()].map(x => { return ( CAN Encoder From c0d38ad46329c74d682c81cf0d9f786103bd0f39 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 14:10:23 -0600 Subject: [PATCH 15/65] Added modification --- .../simulation/wpilib_brain/WPILibBrain.ts | 43 +++++---- fission/src/ui/components/Panel.tsx | 2 +- fission/src/ui/panels/WSViewPanel.tsx | 94 +++++++++++++++++-- 3 files changed, 116 insertions(+), 23 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index ed5d8bd1d5..b00d4aabec 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -6,13 +6,11 @@ import WPILibWSWorker from './WPILibWSWorker?worker' const worker = new WPILibWSWorker() -export const SIM_MAP_UPDATE_EVENT = "ws/sim-map-update" - const PWM_SPEED = ">() -class SimGeneric { +export class SimGeneric { private constructor() { } - public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { + public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { const fieldType = GetFieldType(field) if (fieldType != FieldType.Read && fieldType != FieldType.Both) { console.warn(`Field '${field}' is not a read or both field type`) @@ -67,7 +65,7 @@ class SimGeneric { return data[field] as (T | undefined) ?? defaultValue } - public static Set(simType: SimType, device: string, field: string, value: T): boolean { + public static Set(simType: SimType, device: string, field: string, value: T): boolean { const fieldType = GetFieldType(field) if (fieldType != FieldType.Write && fieldType != FieldType.Both) { console.warn(`Field '${field}' is not a write or both field type`) @@ -98,11 +96,13 @@ class SimGeneric { data: selectedData } }) + + window.dispatchEvent(new SimMapUpdateEvent(true)) return true } } -class SimPWM { +export class SimPWM { private constructor() { } public static GetSpeed(device: string): number | undefined { @@ -114,7 +114,7 @@ class SimPWM { } } -class SimCANMotor { +export class SimCANMotor { private constructor() { } public static GetDutyCycle(device: string): number | undefined { @@ -126,7 +126,7 @@ class SimCANMotor { } } -class SimCANEncoder { +export class SimCANEncoder { private constructor() { } public static SetRawInputPosition(device: string, rawInputPosition: number): boolean { @@ -134,8 +134,6 @@ class SimCANEncoder { } } -let flag = false - worker.addEventListener('message', (eventData: MessageEvent) => { let data: any | undefined; try { @@ -170,10 +168,6 @@ worker.addEventListener('message', (eventData: MessageEvent) => { case 'SimDevice': console.debug('simdevice') UpdateSimMap('SimDevice', device, updateData) - if (!flag) { - flag = true - SimGeneric.Set('SimDevice', device, '>init', true) - } break case 'CANMotor': console.debug('canmotor') @@ -203,7 +197,7 @@ function UpdateSimMap(type: SimType, device: string, updateData: any) { } Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) - window.dispatchEvent(new Event(SIM_MAP_UPDATE_EVENT)) + window.dispatchEvent(new SimMapUpdateEvent(false)) } class WPILibBrain extends Brain { @@ -224,4 +218,21 @@ class WPILibBrain extends Brain { } +export class SimMapUpdateEvent extends Event { + + public static readonly TYPE: string = "ws/sim-map-update" + + private _internalUpdate: boolean; + + public get internalUpdate(): boolean { + return this._internalUpdate + } + + public constructor(internalUpdate: boolean) { + super(SimMapUpdateEvent.TYPE) + + this._internalUpdate = internalUpdate + } +} + export default WPILibBrain \ No newline at end of file diff --git a/fission/src/ui/components/Panel.tsx b/fission/src/ui/components/Panel.tsx index bf0703ffce..2edcc18a02 100644 --- a/fission/src/ui/components/Panel.tsx +++ b/fission/src/ui/components/Panel.tsx @@ -125,7 +125,7 @@ const Panel: React.FC = ({ return (
diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index 693581488d..4df0226f52 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -1,13 +1,15 @@ import Panel, { PanelPropsImpl } from "@/components/Panel" -import { SIM_MAP_UPDATE_EVENT, simMap } from "@/systems/simulation/wpilib_brain/WPILibBrain" -import { styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" -import { useCallback, useEffect, useState } from "react" +import { SimMapUpdateEvent, SimGeneric, simMap, SimType } from "@/systems/simulation/wpilib_brain/WPILibBrain" +import { Button, Dropdown } from "@mui/base" +import { Box, MenuItem, Select, Stack, styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography } from "@mui/material" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { GrConnect } from "react-icons/gr" +type ValueType = "string" | "number" | "object" | "boolean" + const TypoStyled = styled(Typography)({ fontFamily: "Artifakt Legend", fontWeight: 300, - color: "white" }) function generateTableBody() { @@ -53,19 +55,62 @@ function generateTableBody() { ) } +function setGeneric(simType: SimType, device: string, field: string, value: string, valueType: ValueType) { + switch (valueType) { + case "number": + SimGeneric.Set(simType, device, field, parseFloat(value)) + break + case "object": + SimGeneric.Set(simType, device, field, JSON.parse(value)) + break + case "boolean": + SimGeneric.Set(simType, device, field, value.toLowerCase() == "true") + break + default: + SimGeneric.Set(simType, device, field, value) + break + } +} + const WSViewPanel: React.FC = ({ panelId }) => { const [tb, setTb] = useState(generateTableBody()) + const [selectedType, setSelectedType] = useState() + const [selectedDevice, setSelectedDevice] = useState() + const [field, setField] = useState("") + const [value, setValue] = useState("") + const [selectedValueType, setSelectedValueType] = useState("string") + + const deviceSelect = useMemo(() => { + if (!selectedType || !simMap.has(selectedType)) { + return (<>) + } + + return ( + + ) + }, [selectedType]) + + useEffect(() => { + setSelectedDevice(undefined) + }, [selectedType]) + const onSimMapUpdate = useCallback((_: Event) => { setTb(generateTableBody()) }, []) useEffect(() => { - window.addEventListener(SIM_MAP_UPDATE_EVENT, onSimMapUpdate) + window.addEventListener(SimMapUpdateEvent.TYPE, onSimMapUpdate) return () => { - window.removeEventListener(SIM_MAP_UPDATE_EVENT, onSimMapUpdate) + window.removeEventListener(SimMapUpdateEvent.TYPE, onSimMapUpdate) } }, [onSimMapUpdate]) @@ -94,6 +139,43 @@ const WSViewPanel: React.FC = ({ panelId }) => { {tb} + + + {deviceSelect} + {selectedDevice + ? + setField(x.target.value as string)} + > + + setValue(x.target.value as string)} + > + + + + + : <> + } + ) } From 5d61696a8c721d88abadea5fe6e6aef0c985f5c8 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Sat, 6 Jul 2024 19:54:59 -0600 Subject: [PATCH 16/65] Added sample --- simulation/JavaSample/.gitignore | 8 + simulation/JavaSample/WPILib-License.md | 24 ++ simulation/JavaSample/build.gradle | 109 ++++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + simulation/JavaSample/gradlew | 249 +++++++++++++ simulation/JavaSample/gradlew.bat | 92 +++++ simulation/JavaSample/settings.gradle | 30 ++ .../JavaSample/src/main/deploy/example.txt | 3 + .../src/main/java/frc/robot/Main.java | 25 ++ .../src/main/java/frc/robot/Robot.java | 128 +++++++ simulation/JavaSample/vendordeps/NavX.json | 40 +++ .../JavaSample/vendordeps/Phoenix6.json | 339 ++++++++++++++++++ simulation/JavaSample/vendordeps/REVLib.json | 74 ++++ .../vendordeps/WPILibNewCommands.json | 38 ++ 15 files changed, 1166 insertions(+) create mode 100644 simulation/JavaSample/.gitignore create mode 100644 simulation/JavaSample/WPILib-License.md create mode 100644 simulation/JavaSample/build.gradle create mode 100644 simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar create mode 100644 simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties create mode 100755 simulation/JavaSample/gradlew create mode 100644 simulation/JavaSample/gradlew.bat create mode 100644 simulation/JavaSample/settings.gradle create mode 100644 simulation/JavaSample/src/main/deploy/example.txt create mode 100644 simulation/JavaSample/src/main/java/frc/robot/Main.java create mode 100644 simulation/JavaSample/src/main/java/frc/robot/Robot.java create mode 100644 simulation/JavaSample/vendordeps/NavX.json create mode 100644 simulation/JavaSample/vendordeps/Phoenix6.json create mode 100644 simulation/JavaSample/vendordeps/REVLib.json create mode 100644 simulation/JavaSample/vendordeps/WPILibNewCommands.json diff --git a/simulation/JavaSample/.gitignore b/simulation/JavaSample/.gitignore new file mode 100644 index 0000000000..0a7cba8930 --- /dev/null +++ b/simulation/JavaSample/.gitignore @@ -0,0 +1,8 @@ +*.json + +.gradle/ +.vscode/ +.wpilib/ +build/ +ctre_sim/ +bin/ diff --git a/simulation/JavaSample/WPILib-License.md b/simulation/JavaSample/WPILib-License.md new file mode 100644 index 0000000000..645e54253a --- /dev/null +++ b/simulation/JavaSample/WPILib-License.md @@ -0,0 +1,24 @@ +Copyright (c) 2009-2024 FIRST and other WPILib contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of FIRST, WPILib, nor the names of other WPILib + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/simulation/JavaSample/build.gradle b/simulation/JavaSample/build.gradle new file mode 100644 index 0000000000..f0c731e2d9 --- /dev/null +++ b/simulation/JavaSample/build.gradle @@ -0,0 +1,109 @@ +plugins { + id "java" + // id "edu.wpi.first.GradleRIO" version "2024.3.2" +} + +// wpi.maven.useLocal = false +// wpi.maven.useFrcMavenLocalDevelopment = true +// wpi.versions.wpilibVersion = '2024.424242.+' +// wpi.versions.wpimathVersion = '2024.424242.+' + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +def ROBOT_MAIN_CLASS = "frc.robot.Main" + +// Define my targets (RoboRIO) and artifacts (deployable files) +// This is added by GradleRIO's backing project DeployUtils. +// deploy { +// targets { +// roborio(getTargetTypeClass('RoboRIO')) { +// // Team number is loaded either from the .wpilib/wpilib_preferences.json +// // or from command line. If not found an exception will be thrown. +// // You can use getTeamOrDefault(team) instead of getTeamNumber if you +// // want to store a team number in this file. +// team = project.frc.getTeamNumber() +// debug = project.frc.getDebugOrDefault(false) + +// artifacts { +// // First part is artifact name, 2nd is artifact type +// // getTargetTypeClass is a shortcut to get the class type using a string + +// frcJava(getArtifactTypeClass('FRCJavaArtifact')) { +// } + +// // Static files artifact +// frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { +// files = project.fileTree('src/main/deploy') +// directory = '/home/lvuser/deploy' +// } +// } +// } +// } +// } + +// def deployArtifact = deploy.targets.roborio.artifacts.frcJava + +// Set to true to use debug for JNI. +wpi.java.debugJni = false + +// Set this to true to enable desktop support. +def includeDesktopSupport = true + +// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. +// Also defines JUnit 5. +dependencies { + implementation wpi.java.deps.wpilib() + implementation wpi.java.vendor.java() + + roborioDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.roborio) + roborioDebug wpi.java.vendor.jniDebug(wpi.platforms.roborio) + + roborioRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.roborio) + roborioRelease wpi.java.vendor.jniRelease(wpi.platforms.roborio) + + nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) + nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) + simulationDebug wpi.sim.enableDebug() + + nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) + nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) + simulationRelease wpi.sim.enableRelease() + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() + systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' +} + +// Simulation configuration (e.g. environment variables). +wpi.sim.addGui().defaultEnabled = true +wpi.sim.addDriverstation() + +wpi.sim.envVar("HALSIMWS_HOST", "127.0.0.1") +wpi.sim.addWebsocketsServer().defaultEnabled = true + +// Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar') +// in order to make them all available at runtime. Also adding the manifest so WPILib +// knows where to look for our Robot Class. +jar { + from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } + from sourceSets.main.allSource + manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +// Configure jar and deploy tasks +deployArtifact.jarTask = jar +wpi.java.configureExecutableTasks(jar) +wpi.java.configureTestTasks(test) + +// Configure string concat to always inline compile +tasks.withType(JavaCompile) { + options.compilerArgs.add '-XDstringConcat=inline' +} diff --git a/simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar b/simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties b/simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5e82d67b9f --- /dev/null +++ b/simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=permwrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=permwrapper/dists diff --git a/simulation/JavaSample/gradlew b/simulation/JavaSample/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/simulation/JavaSample/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright Ā© 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions Ā«$varĀ», Ā«${var}Ā», Ā«${var:-default}Ā», Ā«${var+SET}Ā», +# Ā«${var#prefix}Ā», Ā«${var%suffix}Ā», and Ā«$( cmd )Ā»; +# * compound commands having a testable exit status, especially Ā«caseĀ»; +# * various built-in commands including Ā«commandĀ», Ā«setĀ», and Ā«ulimitĀ». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/simulation/JavaSample/gradlew.bat b/simulation/JavaSample/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/simulation/JavaSample/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/simulation/JavaSample/settings.gradle b/simulation/JavaSample/settings.gradle new file mode 100644 index 0000000000..d94f73c635 --- /dev/null +++ b/simulation/JavaSample/settings.gradle @@ -0,0 +1,30 @@ +import org.gradle.internal.os.OperatingSystem + +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + String frcYear = '2024' + File frcHome + if (OperatingSystem.current().isWindows()) { + String publicFolder = System.getenv('PUBLIC') + if (publicFolder == null) { + publicFolder = "C:\\Users\\Public" + } + def homeRoot = new File(publicFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } else { + def userFolder = System.getProperty("user.home") + def homeRoot = new File(userFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } + def frcHomeMaven = new File(frcHome, 'maven') + maven { + name 'frcHome' + url frcHomeMaven + } + } +} + +Properties props = System.getProperties(); +props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true"); diff --git a/simulation/JavaSample/src/main/deploy/example.txt b/simulation/JavaSample/src/main/deploy/example.txt new file mode 100644 index 0000000000..bb82515dad --- /dev/null +++ b/simulation/JavaSample/src/main/deploy/example.txt @@ -0,0 +1,3 @@ +Files placed in this directory will be deployed to the RoboRIO into the +'deploy' directory in the home folder. Use the 'Filesystem.getDeployDirectory' wpilib function +to get a proper path relative to the deploy directory. \ No newline at end of file diff --git a/simulation/JavaSample/src/main/java/frc/robot/Main.java b/simulation/JavaSample/src/main/java/frc/robot/Main.java new file mode 100644 index 0000000000..8776e5dda7 --- /dev/null +++ b/simulation/JavaSample/src/main/java/frc/robot/Main.java @@ -0,0 +1,25 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot; + +import edu.wpi.first.wpilibj.RobotBase; + +/** + * Do NOT add any static variables to this class, or any initialization at all. Unless you know what + * you are doing, do not modify this file except to change the parameter class to the startRobot + * call. + */ +public final class Main { + private Main() {} + + /** + * Main initialization function. Do not perform any initialization here. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/simulation/JavaSample/src/main/java/frc/robot/Robot.java b/simulation/JavaSample/src/main/java/frc/robot/Robot.java new file mode 100644 index 0000000000..3922566c6e --- /dev/null +++ b/simulation/JavaSample/src/main/java/frc/robot/Robot.java @@ -0,0 +1,128 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot; + +import com.ctre.phoenix6.hardware.TalonFX; +import com.revrobotics.CANSparkMax; +import com.revrobotics.CANSparkLowLevel.MotorType; + +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.motorcontrol.Spark; +import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; + +/** + * The VM is configured to automatically run this class, and to call the functions corresponding to + * each mode, as described in the TimedRobot documentation. If you change the name of this class or + * the package after creating this project, you must also update the build.gradle file in the + * project. + */ +public class Robot extends TimedRobot { + private static final String kDefaultAuto = "Default"; + private static final String kCustomAuto = "My Auto"; + private String m_autoSelected; + private final SendableChooser m_chooser = new SendableChooser<>(); + + private Spark m_Spark = new Spark(0); + private CANSparkMax m_SparkMax = new CANSparkMax(1, MotorType.kBrushless); + private TalonFX m_Talon = new TalonFX(2); + + /** + * This function is run when the robot is first started up and should be used for any + * initialization code. + */ + @Override + public void robotInit() { + m_chooser.setDefaultOption("Default Auto", kDefaultAuto); + m_chooser.addOption("My Auto", kCustomAuto); + SmartDashboard.putData("Auto choices", m_chooser); + } + + /** + * This function is called every 20 ms, no matter the mode. Use this for items like diagnostics + * that you want ran during disabled, autonomous, teleoperated and test. + * + *

This runs after the mode specific periodic functions, but before LiveWindow and + * SmartDashboard integrated updating. + */ + @Override + public void robotPeriodic() {} + + /** + * This autonomous (along with the chooser code above) shows how to select between different + * autonomous modes using the dashboard. The sendable chooser code works with the Java + * SmartDashboard. If you prefer the LabVIEW Dashboard, remove all of the chooser code and + * uncomment the getString line to get the auto name from the text box below the Gyro + * + *

You can add additional auto modes by adding additional comparisons to the switch structure + * below with additional strings. If using the SendableChooser make sure to add them to the + * chooser code above as well. + */ + @Override + public void autonomousInit() { + m_autoSelected = m_chooser.getSelected(); + // m_autoSelected = SmartDashboard.getString("Auto Selector", kDefaultAuto); + System.out.println("Auto selected: " + m_autoSelected); + } + + /** This function is called periodically during autonomous. */ + @Override + public void autonomousPeriodic() { + + m_Spark.set(0.5); + m_SparkMax.set(1.0); + m_Talon.set(-1.0); + + switch (m_autoSelected) { + case kCustomAuto: + // Put custom auto code here + break; + case kDefaultAuto: + default: + // Put default auto code here + break; + } + } + + /** This function is called once when teleop is enabled. */ + @Override + public void teleopInit() {} + + /** This function is called periodically during operator control. */ + @Override + public void teleopPeriodic() { + m_Spark.set(0.25); + m_SparkMax.set(0.75); + m_Talon.set(-0.5); + } + + /** This function is called once when the robot is disabled. */ + @Override + public void disabledInit() { + m_Spark.set(0.0); + m_SparkMax.set(0.0); + m_Talon.set(0.0); + } + + /** This function is called periodically when disabled. */ + @Override + public void disabledPeriodic() {} + + /** This function is called once when test mode is enabled. */ + @Override + public void testInit() {} + + /** This function is called periodically during test mode. */ + @Override + public void testPeriodic() {} + + /** This function is called once when the robot is first started up. */ + @Override + public void simulationInit() {} + + /** This function is called periodically whilst in simulation. */ + @Override + public void simulationPeriodic() {} +} diff --git a/simulation/JavaSample/vendordeps/NavX.json b/simulation/JavaSample/vendordeps/NavX.json new file mode 100644 index 0000000000..e978a5f745 --- /dev/null +++ b/simulation/JavaSample/vendordeps/NavX.json @@ -0,0 +1,40 @@ +{ + "fileName": "NavX.json", + "name": "NavX", + "version": "2024.1.0", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2024", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2024/" + ], + "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", + "javaDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-java", + "version": "2024.1.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-cpp", + "version": "2024.1.0", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": false, + "libName": "navx_frc", + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxraspbian", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/Phoenix6.json b/simulation/JavaSample/vendordeps/Phoenix6.json new file mode 100644 index 0000000000..032238505f --- /dev/null +++ b/simulation/JavaSample/vendordeps/Phoenix6.json @@ -0,0 +1,339 @@ +{ + "fileName": "Phoenix6.json", + "name": "CTRE-Phoenix (v6)", + "version": "24.3.0", + "frcYear": 2024, + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "mavenUrls": [ + "https://maven.ctr-electronics.com/release/" + ], + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", + "conflictsWith": [ + { + "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", + "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", + "offlineFileName": "Phoenix6And5.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "24.3.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "libName": "CTRE_SimCANCoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/REVLib.json b/simulation/JavaSample/vendordeps/REVLib.json new file mode 100644 index 0000000000..f85acd4054 --- /dev/null +++ b/simulation/JavaSample/vendordeps/REVLib.json @@ -0,0 +1,74 @@ +{ + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.2.4", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.2.4" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.2.4", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/WPILibNewCommands.json b/simulation/JavaSample/vendordeps/WPILibNewCommands.json new file mode 100644 index 0000000000..67bf3898d5 --- /dev/null +++ b/simulation/JavaSample/vendordeps/WPILibNewCommands.json @@ -0,0 +1,38 @@ +{ + "fileName": "WPILibNewCommands.json", + "name": "WPILib-New-Commands", + "version": "1.0.0", + "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", + "frcYear": "2024", + "mavenUrls": [], + "jsonUrl": "", + "javaDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-java", + "version": "wpilib" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-cpp", + "version": "wpilib", + "libName": "wpilibNewCommands", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "windowsx86-64", + "windowsx86", + "linuxx86-64", + "osxuniversal" + ] + } + ] +} From d6070d6e903fe3f30f9a4369e7e2f11e7d34c16d Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Sun, 7 Jul 2024 03:51:09 -0600 Subject: [PATCH 17/65] This actually appears to be a plausible path --- simulation/JavaSample/build.gradle | 58 +++++++++---------- .../synthesis/revrobotics/CANSparkMax.java | 45 ++++++++++++++ .../src/main/java/frc/robot/Robot.java | 5 +- 3 files changed, 78 insertions(+), 30 deletions(-) create mode 100644 simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java diff --git a/simulation/JavaSample/build.gradle b/simulation/JavaSample/build.gradle index f0c731e2d9..7735bda8e6 100644 --- a/simulation/JavaSample/build.gradle +++ b/simulation/JavaSample/build.gradle @@ -1,6 +1,6 @@ plugins { id "java" - // id "edu.wpi.first.GradleRIO" version "2024.3.2" + id "edu.wpi.first.GradleRIO" version "2024.3.2" } // wpi.maven.useLocal = false @@ -17,34 +17,34 @@ def ROBOT_MAIN_CLASS = "frc.robot.Main" // Define my targets (RoboRIO) and artifacts (deployable files) // This is added by GradleRIO's backing project DeployUtils. -// deploy { -// targets { -// roborio(getTargetTypeClass('RoboRIO')) { -// // Team number is loaded either from the .wpilib/wpilib_preferences.json -// // or from command line. If not found an exception will be thrown. -// // You can use getTeamOrDefault(team) instead of getTeamNumber if you -// // want to store a team number in this file. -// team = project.frc.getTeamNumber() -// debug = project.frc.getDebugOrDefault(false) - -// artifacts { -// // First part is artifact name, 2nd is artifact type -// // getTargetTypeClass is a shortcut to get the class type using a string - -// frcJava(getArtifactTypeClass('FRCJavaArtifact')) { -// } - -// // Static files artifact -// frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { -// files = project.fileTree('src/main/deploy') -// directory = '/home/lvuser/deploy' -// } -// } -// } -// } -// } - -// def deployArtifact = deploy.targets.roborio.artifacts.frcJava +deploy { + targets { + roborio(getTargetTypeClass('RoboRIO')) { + // Team number is loaded either from the .wpilib/wpilib_preferences.json + // or from command line. If not found an exception will be thrown. + // You can use getTeamOrDefault(team) instead of getTeamNumber if you + // want to store a team number in this file. + team = project.frc.getTeamOrDefault(997) + debug = project.frc.getDebugOrDefault(false) + + artifacts { + // First part is artifact name, 2nd is artifact type + // getTargetTypeClass is a shortcut to get the class type using a string + + frcJava(getArtifactTypeClass('FRCJavaArtifact')) { + } + + // Static files artifact + frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree('src/main/deploy') + directory = '/home/lvuser/deploy' + } + } + } + } +} + +def deployArtifact = deploy.targets.roborio.artifacts.frcJava // Set to true to use debug for JNI. wpi.java.debugJni = false diff --git a/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java b/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java new file mode 100644 index 0000000000..bb63a11238 --- /dev/null +++ b/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java @@ -0,0 +1,45 @@ +package com.autodesk.synthesis.revrobotics; + +import com.revrobotics.REVLibError; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimDevice.Direction; + +public class CANSparkMax extends com.revrobotics.CANSparkMax { + + private SimDevice m_device; + private SimDouble m_percentOutput; + private SimBoolean m_brakeMode; + private SimDouble m_neutralDeadband; + + public CANSparkMax(int deviceId, MotorType motorType) { + super(deviceId, motorType); + + m_device = SimDevice.create("SYN CANSparkMax", deviceId); + m_percentOutput = m_device.createDouble("percentOutput", Direction.kOutput, 0.0); + m_brakeMode = m_device.createBoolean("brakeMode", Direction.kOutput, false); + } + + @Override + public void set(double percent) { + if (Double.isNaN(percent) || Double.isInfinite(percent)) { + percent = 0.0; + } + + m_percentOutput.set(Math.min(1.0, Math.max(-1.0, percent))); + + super.set(percent); + } + + @Override + public REVLibError setIdleMode(com.revrobotics.CANSparkBase.IdleMode mode) { + if (mode != null) { + m_brakeMode.set(mode.equals(com.revrobotics.CANSparkBase.IdleMode.kBrake)); + } + + return super.setIdleMode(mode); + } + +} diff --git a/simulation/JavaSample/src/main/java/frc/robot/Robot.java b/simulation/JavaSample/src/main/java/frc/robot/Robot.java index 3922566c6e..8a9aa5024b 100644 --- a/simulation/JavaSample/src/main/java/frc/robot/Robot.java +++ b/simulation/JavaSample/src/main/java/frc/robot/Robot.java @@ -5,7 +5,8 @@ package frc.robot; import com.ctre.phoenix6.hardware.TalonFX; -import com.revrobotics.CANSparkMax; +import com.revrobotics.CANSparkBase.IdleMode; +// import com.revrobotics.CANSparkMax; import com.revrobotics.CANSparkLowLevel.MotorType; import edu.wpi.first.wpilibj.TimedRobot; @@ -13,6 +14,8 @@ import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import com.autodesk.synthesis.revrobotics.CANSparkMax; + /** * The VM is configured to automatically run this class, and to call the functions corresponding to * each mode, as described in the TimedRobot documentation. If you change the name of this class or From 71de3f9f3277afceb35d962ea146e5edb00376c0 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Sun, 7 Jul 2024 12:21:41 -0600 Subject: [PATCH 18/65] Added javadocs to SyntheSimJava, READMEs all around, and now JavaSample is making use of SyntheSimJava --- .../synthesis/revrobotics/CANSparkMax.java | 45 --- simulation/JavaSample/vendordeps/NavX.json | 40 --- .../JavaSample/vendordeps/Phoenix6.json | 339 ------------------ simulation/JavaSample/vendordeps/REVLib.json | 74 ---- .../vendordeps/WPILibNewCommands.json | 38 -- simulation/README.md | 12 + simulation/SyntheSimJava/.gitattributes | 9 + simulation/SyntheSimJava/.gitignore | 6 + simulation/SyntheSimJava/README.md | 86 +++++ simulation/SyntheSimJava/build.gradle | 76 ++++ simulation/SyntheSimJava/gradle.properties | 6 + .../SyntheSimJava/gradle/libs.versions.toml | 10 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + simulation/SyntheSimJava/gradlew | 249 +++++++++++++ simulation/SyntheSimJava/gradlew.bat | 92 +++++ simulation/SyntheSimJava/settings.gradle | 15 + .../com/autodesk/synthesis/CANEncoder.java | 50 +++ .../java/com/autodesk/synthesis/CANMotor.java | 108 ++++++ .../com/autodesk/synthesis/ctre/TalonFX.java | 13 + .../synthesis/revrobotics/CANSparkMax.java | 43 +++ .../{ => samples}/JavaSample/.gitignore | 0 .../JavaSample/WPILib-License.md | 0 .../{ => samples}/JavaSample/build.gradle | 6 + .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 simulation/{ => samples}/JavaSample/gradlew | 0 .../{ => samples}/JavaSample/gradlew.bat | 0 .../{ => samples}/JavaSample/settings.gradle | 0 .../JavaSample/src/main/deploy/example.txt | 0 .../src/main/java/frc/robot/Main.java | 0 .../src/main/java/frc/robot/Robot.java | 0 32 files changed, 788 insertions(+), 536 deletions(-) delete mode 100644 simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java delete mode 100644 simulation/JavaSample/vendordeps/NavX.json delete mode 100644 simulation/JavaSample/vendordeps/Phoenix6.json delete mode 100644 simulation/JavaSample/vendordeps/REVLib.json delete mode 100644 simulation/JavaSample/vendordeps/WPILibNewCommands.json create mode 100644 simulation/README.md create mode 100644 simulation/SyntheSimJava/.gitattributes create mode 100644 simulation/SyntheSimJava/.gitignore create mode 100644 simulation/SyntheSimJava/README.md create mode 100644 simulation/SyntheSimJava/build.gradle create mode 100644 simulation/SyntheSimJava/gradle.properties create mode 100644 simulation/SyntheSimJava/gradle/libs.versions.toml create mode 100644 simulation/SyntheSimJava/gradle/wrapper/gradle-wrapper.jar create mode 100644 simulation/SyntheSimJava/gradle/wrapper/gradle-wrapper.properties create mode 100644 simulation/SyntheSimJava/gradlew create mode 100644 simulation/SyntheSimJava/gradlew.bat create mode 100644 simulation/SyntheSimJava/settings.gradle create mode 100644 simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java create mode 100644 simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java create mode 100644 simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java create mode 100644 simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java rename simulation/{ => samples}/JavaSample/.gitignore (100%) rename simulation/{ => samples}/JavaSample/WPILib-License.md (100%) rename simulation/{ => samples}/JavaSample/build.gradle (97%) rename simulation/{ => samples}/JavaSample/gradle/wrapper/gradle-wrapper.jar (100%) rename simulation/{ => samples}/JavaSample/gradle/wrapper/gradle-wrapper.properties (100%) rename simulation/{ => samples}/JavaSample/gradlew (100%) mode change 100755 => 100644 rename simulation/{ => samples}/JavaSample/gradlew.bat (100%) rename simulation/{ => samples}/JavaSample/settings.gradle (100%) rename simulation/{ => samples}/JavaSample/src/main/deploy/example.txt (100%) rename simulation/{ => samples}/JavaSample/src/main/java/frc/robot/Main.java (100%) rename simulation/{ => samples}/JavaSample/src/main/java/frc/robot/Robot.java (100%) diff --git a/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java b/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java deleted file mode 100644 index bb63a11238..0000000000 --- a/simulation/JavaSample/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.autodesk.synthesis.revrobotics; - -import com.revrobotics.REVLibError; - -import edu.wpi.first.hal.SimBoolean; -import edu.wpi.first.hal.SimDevice; -import edu.wpi.first.hal.SimDouble; -import edu.wpi.first.hal.SimDevice.Direction; - -public class CANSparkMax extends com.revrobotics.CANSparkMax { - - private SimDevice m_device; - private SimDouble m_percentOutput; - private SimBoolean m_brakeMode; - private SimDouble m_neutralDeadband; - - public CANSparkMax(int deviceId, MotorType motorType) { - super(deviceId, motorType); - - m_device = SimDevice.create("SYN CANSparkMax", deviceId); - m_percentOutput = m_device.createDouble("percentOutput", Direction.kOutput, 0.0); - m_brakeMode = m_device.createBoolean("brakeMode", Direction.kOutput, false); - } - - @Override - public void set(double percent) { - if (Double.isNaN(percent) || Double.isInfinite(percent)) { - percent = 0.0; - } - - m_percentOutput.set(Math.min(1.0, Math.max(-1.0, percent))); - - super.set(percent); - } - - @Override - public REVLibError setIdleMode(com.revrobotics.CANSparkBase.IdleMode mode) { - if (mode != null) { - m_brakeMode.set(mode.equals(com.revrobotics.CANSparkBase.IdleMode.kBrake)); - } - - return super.setIdleMode(mode); - } - -} diff --git a/simulation/JavaSample/vendordeps/NavX.json b/simulation/JavaSample/vendordeps/NavX.json deleted file mode 100644 index e978a5f745..0000000000 --- a/simulation/JavaSample/vendordeps/NavX.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "fileName": "NavX.json", - "name": "NavX", - "version": "2024.1.0", - "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", - "frcYear": "2024", - "mavenUrls": [ - "https://dev.studica.com/maven/release/2024/" - ], - "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", - "javaDependencies": [ - { - "groupId": "com.kauailabs.navx.frc", - "artifactId": "navx-frc-java", - "version": "2024.1.0" - } - ], - "jniDependencies": [], - "cppDependencies": [ - { - "groupId": "com.kauailabs.navx.frc", - "artifactId": "navx-frc-cpp", - "version": "2024.1.0", - "headerClassifier": "headers", - "sourcesClassifier": "sources", - "sharedLibrary": false, - "libName": "navx_frc", - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "linuxathena", - "linuxraspbian", - "linuxarm32", - "linuxarm64", - "linuxx86-64", - "osxuniversal", - "windowsx86-64" - ] - } - ] -} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/Phoenix6.json b/simulation/JavaSample/vendordeps/Phoenix6.json deleted file mode 100644 index 032238505f..0000000000 --- a/simulation/JavaSample/vendordeps/Phoenix6.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "fileName": "Phoenix6.json", - "name": "CTRE-Phoenix (v6)", - "version": "24.3.0", - "frcYear": 2024, - "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", - "mavenUrls": [ - "https://maven.ctr-electronics.com/release/" - ], - "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", - "conflictsWith": [ - { - "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", - "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", - "offlineFileName": "Phoenix6And5.json" - } - ], - "javaDependencies": [ - { - "groupId": "com.ctre.phoenix6", - "artifactId": "wpiapi-java", - "version": "24.3.0" - } - ], - "jniDependencies": [ - { - "groupId": "com.ctre.phoenix6", - "artifactId": "tools", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "tools-sim", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonSRX", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonFX", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simVictorSPX", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simPigeonIMU", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simCANCoder", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProTalonFX", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProCANcoder", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProPigeon2", - "version": "24.3.0", - "isJar": false, - "skipInvalidPlatforms": true, - "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - } - ], - "cppDependencies": [ - { - "groupId": "com.ctre.phoenix6", - "artifactId": "wpiapi-cpp", - "version": "24.3.0", - "libName": "CTRE_Phoenix6_WPI", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix6", - "artifactId": "tools", - "version": "24.3.0", - "libName": "CTRE_PhoenixTools", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxathena" - ], - "simMode": "hwsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "wpiapi-cpp-sim", - "version": "24.3.0", - "libName": "CTRE_Phoenix6_WPISim", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "tools-sim", - "version": "24.3.0", - "libName": "CTRE_PhoenixTools_Sim", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonSRX", - "version": "24.3.0", - "libName": "CTRE_SimTalonSRX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonFX", - "version": "24.3.0", - "libName": "CTRE_SimTalonFX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simVictorSPX", - "version": "24.3.0", - "libName": "CTRE_SimVictorSPX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simPigeonIMU", - "version": "24.3.0", - "libName": "CTRE_SimPigeonIMU", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simCANCoder", - "version": "24.3.0", - "libName": "CTRE_SimCANCoder", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProTalonFX", - "version": "24.3.0", - "libName": "CTRE_SimProTalonFX", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProCANcoder", - "version": "24.3.0", - "libName": "CTRE_SimProCANcoder", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - }, - { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simProPigeon2", - "version": "24.3.0", - "libName": "CTRE_SimProPigeon2", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "osxuniversal" - ], - "simMode": "swsim" - } - ] -} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/REVLib.json b/simulation/JavaSample/vendordeps/REVLib.json deleted file mode 100644 index f85acd4054..0000000000 --- a/simulation/JavaSample/vendordeps/REVLib.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "fileName": "REVLib.json", - "name": "REVLib", - "version": "2024.2.4", - "frcYear": "2024", - "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", - "mavenUrls": [ - "https://maven.revrobotics.com/" - ], - "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", - "javaDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-java", - "version": "2024.2.4" - } - ], - "jniDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-driver", - "version": "2024.2.4", - "skipInvalidPlatforms": true, - "isJar": false, - "validPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - } - ], - "cppDependencies": [ - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-cpp", - "version": "2024.2.4", - "libName": "REVLib", - "headerClassifier": "headers", - "sharedLibrary": false, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - }, - { - "groupId": "com.revrobotics.frc", - "artifactId": "REVLib-driver", - "version": "2024.2.4", - "libName": "REVLibDriver", - "headerClassifier": "headers", - "sharedLibrary": false, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "windowsx86", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] - } - ] -} \ No newline at end of file diff --git a/simulation/JavaSample/vendordeps/WPILibNewCommands.json b/simulation/JavaSample/vendordeps/WPILibNewCommands.json deleted file mode 100644 index 67bf3898d5..0000000000 --- a/simulation/JavaSample/vendordeps/WPILibNewCommands.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "fileName": "WPILibNewCommands.json", - "name": "WPILib-New-Commands", - "version": "1.0.0", - "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", - "frcYear": "2024", - "mavenUrls": [], - "jsonUrl": "", - "javaDependencies": [ - { - "groupId": "edu.wpi.first.wpilibNewCommands", - "artifactId": "wpilibNewCommands-java", - "version": "wpilib" - } - ], - "jniDependencies": [], - "cppDependencies": [ - { - "groupId": "edu.wpi.first.wpilibNewCommands", - "artifactId": "wpilibNewCommands-cpp", - "version": "wpilib", - "libName": "wpilibNewCommands", - "headerClassifier": "headers", - "sourcesClassifier": "sources", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "linuxathena", - "linuxarm32", - "linuxarm64", - "windowsx86-64", - "windowsx86", - "linuxx86-64", - "osxuniversal" - ] - } - ] -} diff --git a/simulation/README.md b/simulation/README.md new file mode 100644 index 0000000000..4f33881b3b --- /dev/null +++ b/simulation/README.md @@ -0,0 +1,12 @@ +# Synthesis Simulation + +A collection of simulation tools and samples to help enchance the use of code simulation inside of Synthesis. + +## SyntheSim + +SyntheSim is a utility used to bolster the simulation capabilities of WPILib compatible libraries commonly used in FRC today. + +## Samples + +This directory includes sample projects that are simulation compatible with Synthesis. These sample projects give +users an idea of how to configure their own projects to best take advantage of simulation. diff --git a/simulation/SyntheSimJava/.gitattributes b/simulation/SyntheSimJava/.gitattributes new file mode 100644 index 0000000000..097f9f98d9 --- /dev/null +++ b/simulation/SyntheSimJava/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/simulation/SyntheSimJava/.gitignore b/simulation/SyntheSimJava/.gitignore new file mode 100644 index 0000000000..3b2f6ee393 --- /dev/null +++ b/simulation/SyntheSimJava/.gitignore @@ -0,0 +1,6 @@ +# Ignore Gradle project-specific cache directory +.gradle +.vscode + +# Ignore Gradle build output directory +build diff --git a/simulation/SyntheSimJava/README.md b/simulation/SyntheSimJava/README.md new file mode 100644 index 0000000000..3e10a613a3 --- /dev/null +++ b/simulation/SyntheSimJava/README.md @@ -0,0 +1,86 @@ +# SyntheSim - Java + +This is the SyntheSim Java utility library. FRC users can add this to their project to enhance the simulation capabilities of commonly used libraries + +## Current 3rd-Party Support + +This is a list of the following 3rd-Party libraries that SyntheSim - Java improves, as well as the level of capability currently offered. + +### REVRobotics +- [ ] CANSparkMax + - [x] Basic motor control + - [x] Basic internal encoder data + - [ ] Motor following + - [ ] Full encoder support + +### CTRE Phoenix +- [ ] TalonFX + - [ ] Basic motor control + - [ ] Basic internal encoder data + - [ ] Motor following + - [ ] Full encoder support + +## Building + +To build the project, run the `build` task: + +

+ Example + + Windows: + ```sh + $ gradlew.bat build + ``` + + MacOS/Linux: + ```sh + $ ./gradlew build + ``` +
+ +## Usage + +Currently, SyntheSimJava is only available as a local repository. This means it will need to be published and accessed locally. + +### Publish (Local) + +To publish the project locally, run the `publishToMavenLocal` task: + +
+ Example + + Windows: + ```sh + $ gradlew.bat publishToMavenLocal + ``` + + MacOS/Linux: + ```sh + $ ./gradlew publishToMavenLocal + ``` +
+ +### Adding to project locally + +In order to add the project locally, you must include the the `mavenLocal()` repository to your projects: + +```groovy +repositories { + mavenLocal() + ... +} +``` + +Then, add the implementation to your dependencies: + +```groovy +dependencies { + ... + implementation "com.autodesk.synthesis:SyntheSimJava:1.0.0" + ... +} +``` + +### Swapping Imports + +SyntheSimJava creates alternative classes that wrap the original ones. Everything that we intercept is passed on to the original class, making it so these classes can (although not recommended) be used when running your robot code on original hardware. Be sure to switch over any and all CAN devices that this project supports in order to effectively simulate your code inside of Synthesis, or with any HALSim, WebSocket supported simulation/device. \ No newline at end of file diff --git a/simulation/SyntheSimJava/build.gradle b/simulation/SyntheSimJava/build.gradle new file mode 100644 index 0000000000..421980fb3d --- /dev/null +++ b/simulation/SyntheSimJava/build.gradle @@ -0,0 +1,76 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java library project to get you started. + * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.8/userguide/building_java_projects.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + // Apply the java-library plugin for API and implementation separation. + id 'java-library' + id 'maven-publish' +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() + + // WPILib + maven { + url "https://frcmaven.wpi.edu/artifactory/release/" + } + + // CTRE + maven { + url "https://maven.ctr-electronics.com/release/" + } + + // REV + maven { + url "https://maven.revrobotics.com/" + } +} + +def WPI_Version = '2024.3.2' +def REV_Version = '2024.2.4' +def CTRE_Version = '24.3.0' + +dependencies { + // This dependency is exported to consumers, that is to say found on their compile classpath. + api libs.commons.math3 + + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + implementation libs.guava + + // WPILib + implementation "edu.wpi.first.wpilibj:wpilibj-java:$WPI_Version" + implementation "edu.wpi.first.wpiutil:wpiutil-java:$WPI_Version" + implementation "edu.wpi.first.hal:hal-java:$WPI_Version" + + // REVRobotics + implementation "com.revrobotics.frc:REVLib-java:$REV_Version" + + // CTRE + implementation "com.ctre.phoenix6:wpiapi-java:$CTRE_Version" +} + +java { + withJavadocJar() + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +publishing() { + publications { + maven(MavenPublication) { + group = 'com.autodesk.synthesis' + artifactId = 'SyntheSimJava' + version = '1.0.0' + + from components.java + } + } +} diff --git a/simulation/SyntheSimJava/gradle.properties b/simulation/SyntheSimJava/gradle.properties new file mode 100644 index 0000000000..18f452c73f --- /dev/null +++ b/simulation/SyntheSimJava/gradle.properties @@ -0,0 +1,6 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/simulation/SyntheSimJava/gradle/libs.versions.toml b/simulation/SyntheSimJava/gradle/libs.versions.toml new file mode 100644 index 0000000000..db11383cd4 --- /dev/null +++ b/simulation/SyntheSimJava/gradle/libs.versions.toml @@ -0,0 +1,10 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +commons-math3 = "3.6.1" +guava = "33.0.0-jre" + +[libraries] +commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } diff --git a/simulation/SyntheSimJava/gradle/wrapper/gradle-wrapper.jar b/simulation/SyntheSimJava/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vSTxF-Vi3+ZOI=Thq2} zyQgjYY1_7^ZQHh{?P))4+qUiQJLi1&{yE>h?~jU%tjdV0h|FENbM3X(KnJdPKc?~k zh=^Ixv*+smUll!DTWH!jrV*wSh*(mx0o6}1@JExzF(#9FXgmTXVoU+>kDe68N)dkQ zH#_98Zv$}lQwjKL@yBd;U(UD0UCl322=pav<=6g>03{O_3oKTq;9bLFX1ia*lw;#K zOiYDcBJf)82->83N_Y(J7Kr_3lE)hAu;)Q(nUVydv+l+nQ$?|%MWTy`t>{havFSQloHwiIkGK9YZ79^9?AZo0ZyQlVR#}lF%dn5n%xYksXf8gnBm=wO7g_^! zauQ-bH1Dc@3ItZ-9D_*pH}p!IG7j8A_o94#~>$LR|TFq zZ-b00*nuw|-5C2lJDCw&8p5N~Z1J&TrcyErds&!l3$eSz%`(*izc;-?HAFD9AHb-| z>)id`QCrzRws^9(#&=pIx9OEf2rmlob8sK&xPCWS+nD~qzU|qG6KwA{zbikcfQrdH z+ zQg>O<`K4L8rN7`GJB0*3<3`z({lWe#K!4AZLsI{%z#ja^OpfjU{!{)x0ZH~RB0W5X zTwN^w=|nA!4PEU2=LR05x~}|B&ZP?#pNgDMwD*ajI6oJqv!L81gu=KpqH22avXf0w zX3HjbCI!n9>l046)5rr5&v5ja!xkKK42zmqHzPx$9Nn_MZk`gLeSLgC=LFf;H1O#B zn=8|^1iRrujHfbgA+8i<9jaXc;CQBAmQvMGQPhFec2H1knCK2x!T`e6soyrqCamX% zTQ4dX_E*8so)E*TB$*io{$c6X)~{aWfaqdTh=xEeGvOAN9H&-t5tEE-qso<+C!2>+ zskX51H-H}#X{A75wqFe-J{?o8Bx|>fTBtl&tcbdR|132Ztqu5X0i-pisB-z8n71%q%>EF}yy5?z=Ve`}hVh{Drv1YWL zW=%ug_&chF11gDv3D6B)Tz5g54H0mDHNjuKZ+)CKFk4Z|$RD zfRuKLW`1B>B?*RUfVd0+u8h3r-{@fZ{k)c!93t1b0+Q9vOaRnEn1*IL>5Z4E4dZ!7 ztp4GP-^1d>8~LMeb}bW!(aAnB1tM_*la=Xx)q(I0Y@__Zd$!KYb8T2VBRw%e$iSdZ zkwdMwd}eV9q*;YvrBFTv1>1+}{H!JK2M*C|TNe$ZSA>UHKk);wz$(F$rXVc|sI^lD zV^?_J!3cLM;GJuBMbftbaRUs$;F}HDEDtIeHQ)^EJJ1F9FKJTGH<(Jj`phE6OuvE) zqK^K`;3S{Y#1M@8yRQwH`?kHMq4tHX#rJ>5lY3DM#o@or4&^_xtBC(|JpGTfrbGkA z2Tu+AyT^pHannww!4^!$5?@5v`LYy~T`qs7SYt$JgrY(w%C+IWA;ZkwEF)u5sDvOK zGk;G>Mh&elvXDcV69J_h02l&O;!{$({fng9Rlc3ID#tmB^FIG^w{HLUpF+iB`|
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/simulation/SyntheSimJava/gradlew.bat b/simulation/SyntheSimJava/gradlew.bat new file mode 100644 index 0000000000..25da30dbde --- /dev/null +++ b/simulation/SyntheSimJava/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/simulation/SyntheSimJava/settings.gradle b/simulation/SyntheSimJava/settings.gradle new file mode 100644 index 0000000000..9bc6b745d3 --- /dev/null +++ b/simulation/SyntheSimJava/settings.gradle @@ -0,0 +1,15 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.8/userguide/multi_project_builds.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +// rootProject.name = 'SyntheSimJava' +// include('lib') diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java new file mode 100644 index 0000000000..fa74dbbe0f --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java @@ -0,0 +1,50 @@ +package com.autodesk.synthesis; + +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimDevice.Direction; + +/** + * CANEncoder class for easy implementation of documentation-compliant simulation data. + * + * See https://github.com/wpilibsuite/allwpilib/blob/6478ba6e3fa317ee041b8a41e562d925602b6ea4/simulation/halsim_ws_core/doc/hardware_ws_api.md + * for documentation on the WebSocket API Specification. + */ +public class CANEncoder { + + private SimDevice m_device; + + private SimDouble m_position; + private SimDouble m_velocity; + + /** + * Creates an Encoder accessed by the CANBus. + * + * @param name Name of the CAN Motor. This is generally the class name of the originating encoder, prefixed with something (ie. "SYN CANSparkMax/Encoder"). + * @param deviceId CAN Device ID. + */ + public CANEncoder(String name, int deviceId) { + m_device = SimDevice.create(name, deviceId); + + m_position = m_device.createDouble("position", Direction.kInput, 0.0); + m_velocity = m_device.createDouble("velocity", Direction.kInput, 0.0); + } + + /** + * Gets the current position of the encoder, simulated. + * + * @return Current Position. + */ + public double getPosition() { + return m_position.get(); + } + + /** + * Gets the current velocity of the encoder, simulated. + * + * @return Current Velocity. + */ + public double getVelocity() { + return m_velocity.get(); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java new file mode 100644 index 0000000000..eeca38ed7b --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java @@ -0,0 +1,108 @@ +package com.autodesk.synthesis; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDevice.Direction; +import edu.wpi.first.hal.SimDouble; + +/** + * CANMotor class for easy implementation of documentation-compliant simulation data. + * + * See https://github.com/wpilibsuite/allwpilib/blob/6478ba6e3fa317ee041b8a41e562d925602b6ea4/simulation/halsim_ws_core/doc/hardware_ws_api.md + * for documentation on the WebSocket API Specification. + */ +public class CANMotor { + + private SimDevice m_device; + + private SimDouble m_percentOutput; + private SimBoolean m_brakeMode; + private SimDouble m_neutralDeadband; + + private SimDouble m_supplyCurrent; + private SimDouble m_motorCurrent; + private SimDouble m_busVoltage; + + /** + * Creates a CANMotor sim device in accordance with the WebSocket API Specification. + * + * @param name Name of the CAN Motor. This is generally the class name of the originating motor, prefixed with something (ie. "SYN CANSparkMax"). + * @param deviceId CAN Device ID. + * @param defaultPercentOutput Default PercentOutput value. [-1.0, 1.0] + * @param defaultBrakeMode Default BrakeMode value. (true/false) + * @param defaultNeutralDeadband Default Neutral Deadband value. This is used to determine when braking should be enabled. [0.0, 1.0] + */ + public CANMotor(String name, int deviceId, double defaultPercentOutput, boolean defaultBrakeMode, double defaultNeutralDeadband) { + m_device = SimDevice.create(name, deviceId); + + m_percentOutput = m_device.createDouble("percentOutput", Direction.kOutput, 0.0); + m_brakeMode = m_device.createBoolean("brakeMode", Direction.kOutput, false); + m_neutralDeadband = m_device.createDouble("neutralDeadband", Direction.kOutput, deviceId); + + m_supplyCurrent = m_device.createDouble("supplyCurrent", Direction.kInput, 120.0); + m_motorCurrent = m_device.createDouble("motorCurrent", Direction.kInput, 120.0); + m_busVoltage = m_device.createDouble("busVoltage", Direction.kInput, 12.0); + } + + /** + * Set the PercentOutput of the motor. + * + * @param percent [-1.0, 1.0] + */ + public void setPercentOutput(double percent) { + if (Double.isNaN(percent) || Double.isInfinite(percent)) { + percent = 0.0; + } + + m_percentOutput.set(Math.min(1.0, Math.max(-1.0, percent))); + } + + /** + * Set the BrakeMode of the motor. + * + * @param brake True to enable braking. False to not. + */ + public void setBrakeMode(boolean brake) { + m_brakeMode.set(brake); + } + + /** + * Set the neutral deadband of the motor. Essentially when to enable brake mode. + * + * @param deadband [0.0, 1.0] + */ + public void setNeutralDeadband(double deadband) { + if (Double.isNaN(deadband) || Double.isInfinite(deadband)) { + deadband = 0.0; + } + + m_neutralDeadband.set(Math.min(1.0, Math.max(0.0, deadband))); + } + + /** + * Get the supply current, simulated. + * + * @return Supply Current. + */ + public double getSupplyCurrent() { + return m_supplyCurrent.get(); + } + + /** + * Get the motor current, simulated. + * + * @return Motor Current. + */ + public double getMotorCurrent() { + return m_motorCurrent.get(); + } + + /** + * Get the Bus Voltage, simulated. + * + * @return Bus Voltage + */ + public double getBusVoltage() { + return m_busVoltage.get(); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java new file mode 100644 index 0000000000..63ed5b1e9c --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java @@ -0,0 +1,13 @@ +package com.autodesk.synthesis.ctre; + +import com.autodesk.synthesis.CANMotor; + +public class TalonFX extends com.ctre.phoenix6.hardware.TalonFX { + private CANMotor m_motor; + + public TalonFX(int deviceId) { + super(deviceId); + + m_motor = new CANMotor("SYN TalonFX", deviceId, 0.0, false, 0.3); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java new file mode 100644 index 0000000000..da2087e7ab --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java @@ -0,0 +1,43 @@ +package com.autodesk.synthesis.revrobotics; + +import com.autodesk.synthesis.CANEncoder; +import com.autodesk.synthesis.CANMotor; +import com.revrobotics.REVLibError; + +/** + * CANSparkMax wrapper to add proper WPILib HALSim support. + */ +public class CANSparkMax extends com.revrobotics.CANSparkMax { + + private CANMotor m_motor; + private CANEncoder m_encoder; + + /** + * Creates a new CANSparkMax, wrapped with simulation support. + * + * @param deviceId CAN Device ID. + * @param motorType Motortype. For Simulation purposes, this is discarded at the moment. + */ + public CANSparkMax(int deviceId, MotorType motorType) { + super(deviceId, motorType); + + m_motor = new CANMotor("SYN CANSparkMax", deviceId, 0.0, false, 0.3); + m_encoder = new CANEncoder("SYN CANSparkMax/Encoder", deviceId); + } + + @Override + public void set(double percent) { + super.set(percent); + m_motor.setPercentOutput(percent); + } + + @Override + public REVLibError setIdleMode(com.revrobotics.CANSparkBase.IdleMode mode) { + if (mode != null) { + m_motor.setBrakeMode(mode.equals(com.revrobotics.CANSparkBase.IdleMode.kBrake)); + } + + return super.setIdleMode(mode); + } + +} diff --git a/simulation/JavaSample/.gitignore b/simulation/samples/JavaSample/.gitignore similarity index 100% rename from simulation/JavaSample/.gitignore rename to simulation/samples/JavaSample/.gitignore diff --git a/simulation/JavaSample/WPILib-License.md b/simulation/samples/JavaSample/WPILib-License.md similarity index 100% rename from simulation/JavaSample/WPILib-License.md rename to simulation/samples/JavaSample/WPILib-License.md diff --git a/simulation/JavaSample/build.gradle b/simulation/samples/JavaSample/build.gradle similarity index 97% rename from simulation/JavaSample/build.gradle rename to simulation/samples/JavaSample/build.gradle index 7735bda8e6..5acbea004b 100644 --- a/simulation/JavaSample/build.gradle +++ b/simulation/samples/JavaSample/build.gradle @@ -3,6 +3,10 @@ plugins { id "edu.wpi.first.GradleRIO" version "2024.3.2" } +repositories { + mavenLocal() +} + // wpi.maven.useLocal = false // wpi.maven.useFrcMavenLocalDevelopment = true // wpi.versions.wpilibVersion = '2024.424242.+' @@ -74,6 +78,8 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation "com.autodesk.synthesis:SyntheSimJava:1.0.0" } test { diff --git a/simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar b/simulation/samples/JavaSample/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from simulation/JavaSample/gradle/wrapper/gradle-wrapper.jar rename to simulation/samples/JavaSample/gradle/wrapper/gradle-wrapper.jar diff --git a/simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties b/simulation/samples/JavaSample/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from simulation/JavaSample/gradle/wrapper/gradle-wrapper.properties rename to simulation/samples/JavaSample/gradle/wrapper/gradle-wrapper.properties diff --git a/simulation/JavaSample/gradlew b/simulation/samples/JavaSample/gradlew old mode 100755 new mode 100644 similarity index 100% rename from simulation/JavaSample/gradlew rename to simulation/samples/JavaSample/gradlew diff --git a/simulation/JavaSample/gradlew.bat b/simulation/samples/JavaSample/gradlew.bat similarity index 100% rename from simulation/JavaSample/gradlew.bat rename to simulation/samples/JavaSample/gradlew.bat diff --git a/simulation/JavaSample/settings.gradle b/simulation/samples/JavaSample/settings.gradle similarity index 100% rename from simulation/JavaSample/settings.gradle rename to simulation/samples/JavaSample/settings.gradle diff --git a/simulation/JavaSample/src/main/deploy/example.txt b/simulation/samples/JavaSample/src/main/deploy/example.txt similarity index 100% rename from simulation/JavaSample/src/main/deploy/example.txt rename to simulation/samples/JavaSample/src/main/deploy/example.txt diff --git a/simulation/JavaSample/src/main/java/frc/robot/Main.java b/simulation/samples/JavaSample/src/main/java/frc/robot/Main.java similarity index 100% rename from simulation/JavaSample/src/main/java/frc/robot/Main.java rename to simulation/samples/JavaSample/src/main/java/frc/robot/Main.java diff --git a/simulation/JavaSample/src/main/java/frc/robot/Robot.java b/simulation/samples/JavaSample/src/main/java/frc/robot/Robot.java similarity index 100% rename from simulation/JavaSample/src/main/java/frc/robot/Robot.java rename to simulation/samples/JavaSample/src/main/java/frc/robot/Robot.java From b9db0946aff58929f2c2bd460f06d68a5a0ce928 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Mon, 8 Jul 2024 11:07:11 -0600 Subject: [PATCH 19/65] Added Cpp sampel --- simulation/samples/CppSample/.gitignore | 4 + .../samples/CppSample/WPILib-License.md | 24 ++ simulation/samples/CppSample/build.gradle | 99 +++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + simulation/samples/CppSample/gradlew | 249 +++++++++++++ simulation/samples/CppSample/gradlew.bat | 92 +++++ simulation/samples/CppSample/settings.gradle | 30 ++ .../samples/CppSample/src/main/cpp/Robot.cpp | 78 ++++ .../CppSample/src/main/deploy/example.txt | 4 + .../CppSample/src/main/include/Robot.h | 32 ++ .../samples/CppSample/src/test/cpp/main.cpp | 10 + .../samples/CppSample/vendordeps/NavX.json | 40 +++ .../CppSample/vendordeps/Phoenix6.json | 339 ++++++++++++++++++ .../samples/CppSample/vendordeps/REVLib.json | 74 ++++ .../vendordeps/WPILibNewCommands.json | 38 ++ 16 files changed, 1120 insertions(+) create mode 100644 simulation/samples/CppSample/.gitignore create mode 100644 simulation/samples/CppSample/WPILib-License.md create mode 100644 simulation/samples/CppSample/build.gradle create mode 100644 simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.jar create mode 100644 simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.properties create mode 100644 simulation/samples/CppSample/gradlew create mode 100644 simulation/samples/CppSample/gradlew.bat create mode 100644 simulation/samples/CppSample/settings.gradle create mode 100644 simulation/samples/CppSample/src/main/cpp/Robot.cpp create mode 100644 simulation/samples/CppSample/src/main/deploy/example.txt create mode 100644 simulation/samples/CppSample/src/main/include/Robot.h create mode 100644 simulation/samples/CppSample/src/test/cpp/main.cpp create mode 100644 simulation/samples/CppSample/vendordeps/NavX.json create mode 100644 simulation/samples/CppSample/vendordeps/Phoenix6.json create mode 100644 simulation/samples/CppSample/vendordeps/REVLib.json create mode 100644 simulation/samples/CppSample/vendordeps/WPILibNewCommands.json diff --git a/simulation/samples/CppSample/.gitignore b/simulation/samples/CppSample/.gitignore new file mode 100644 index 0000000000..11c9fdd738 --- /dev/null +++ b/simulation/samples/CppSample/.gitignore @@ -0,0 +1,4 @@ +.gradle/ +.vscode/ +.wpilib/ +build/ diff --git a/simulation/samples/CppSample/WPILib-License.md b/simulation/samples/CppSample/WPILib-License.md new file mode 100644 index 0000000000..645e54253a --- /dev/null +++ b/simulation/samples/CppSample/WPILib-License.md @@ -0,0 +1,24 @@ +Copyright (c) 2009-2024 FIRST and other WPILib contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of FIRST, WPILib, nor the names of other WPILib + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/simulation/samples/CppSample/build.gradle b/simulation/samples/CppSample/build.gradle new file mode 100644 index 0000000000..5692953ce6 --- /dev/null +++ b/simulation/samples/CppSample/build.gradle @@ -0,0 +1,99 @@ +plugins { + id "cpp" + id "google-test-test-suite" + id "edu.wpi.first.GradleRIO" version "2024.3.2" +} + +// Define my targets (RoboRIO) and artifacts (deployable files) +// This is added by GradleRIO's backing project DeployUtils. +deploy { + targets { + roborio(getTargetTypeClass('RoboRIO')) { + // Team number is loaded either from the .wpilib/wpilib_preferences.json + // or from command line. If not found an exception will be thrown. + // You can use getTeamOrDefault(team) instead of getTeamNumber if you + // want to store a team number in this file. + team = project.frc.getTeamOrDefault(997) + debug = project.frc.getDebugOrDefault(false) + + artifacts { + // First part is artifact name, 2nd is artifact type + // getTargetTypeClass is a shortcut to get the class type using a string + + frcCpp(getArtifactTypeClass('FRCNativeArtifact')) { + } + + // Static files artifact + frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree('src/main/deploy') + directory = '/home/lvuser/deploy' + } + } + } + } +} + +def deployArtifact = deploy.targets.roborio.artifacts.frcCpp + +// Set this to true to enable desktop support. +def includeDesktopSupport = true + +// Set to true to run simulation in debug mode +wpi.cpp.debugSimulation = false + +// Default enable simgui +wpi.sim.addGui().defaultEnabled = true +// Enable DS but not by default +wpi.sim.addDriverstation() + +model { + components { + frcUserProgram(NativeExecutableSpec) { + targetPlatform wpi.platforms.roborio + if (includeDesktopSupport) { + targetPlatform wpi.platforms.desktop + } + + sources.cpp { + source { + srcDir 'src/main/cpp' + include '**/*.cpp', '**/*.cc' + } + exportedHeaders { + srcDir 'src/main/include' + } + } + + // Set deploy task to deploy this component + deployArtifact.component = it + + // Enable run tasks for this component + wpi.cpp.enableExternalTasks(it) + + // Enable simulation for this component + wpi.sim.enable(it) + // Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. + wpi.cpp.vendor.cpp(it) + wpi.cpp.deps.wpilib(it) + } + } + testSuites { + frcUserProgramTest(GoogleTestTestSuiteSpec) { + testing $.components.frcUserProgram + + sources.cpp { + source { + srcDir 'src/test/cpp' + include '**/*.cpp' + } + } + + // Enable run tasks for this component + wpi.cpp.enableExternalTasks(it) + + wpi.cpp.vendor.cpp(it) + wpi.cpp.deps.wpilib(it) + wpi.cpp.deps.googleTest(it) + } + } +} diff --git a/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.jar b/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.properties b/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5e82d67b9f --- /dev/null +++ b/simulation/samples/CppSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=permwrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=permwrapper/dists diff --git a/simulation/samples/CppSample/gradlew b/simulation/samples/CppSample/gradlew new file mode 100644 index 0000000000..1aa94a4269 --- /dev/null +++ b/simulation/samples/CppSample/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright Ā© 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions Ā«$varĀ», Ā«${var}Ā», Ā«${var:-default}Ā», Ā«${var+SET}Ā», +# Ā«${var#prefix}Ā», Ā«${var%suffix}Ā», and Ā«$( cmd )Ā»; +# * compound commands having a testable exit status, especially Ā«caseĀ»; +# * various built-in commands including Ā«commandĀ», Ā«setĀ», and Ā«ulimitĀ». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/simulation/samples/CppSample/gradlew.bat b/simulation/samples/CppSample/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/simulation/samples/CppSample/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/simulation/samples/CppSample/settings.gradle b/simulation/samples/CppSample/settings.gradle new file mode 100644 index 0000000000..d94f73c635 --- /dev/null +++ b/simulation/samples/CppSample/settings.gradle @@ -0,0 +1,30 @@ +import org.gradle.internal.os.OperatingSystem + +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + String frcYear = '2024' + File frcHome + if (OperatingSystem.current().isWindows()) { + String publicFolder = System.getenv('PUBLIC') + if (publicFolder == null) { + publicFolder = "C:\\Users\\Public" + } + def homeRoot = new File(publicFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } else { + def userFolder = System.getProperty("user.home") + def homeRoot = new File(userFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } + def frcHomeMaven = new File(frcHome, 'maven') + maven { + name 'frcHome' + url frcHomeMaven + } + } +} + +Properties props = System.getProperties(); +props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true"); diff --git a/simulation/samples/CppSample/src/main/cpp/Robot.cpp b/simulation/samples/CppSample/src/main/cpp/Robot.cpp new file mode 100644 index 0000000000..f74262bef9 --- /dev/null +++ b/simulation/samples/CppSample/src/main/cpp/Robot.cpp @@ -0,0 +1,78 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "Robot.h" + +#include +#include + +void Robot::RobotInit() { + m_chooser.SetDefaultOption(kAutoNameDefault, kAutoNameDefault); + m_chooser.AddOption(kAutoNameCustom, kAutoNameCustom); + frc::SmartDashboard::PutData("Auto Modes", &m_chooser); +} + +/** + * This function is called every 20 ms, no matter the mode. Use + * this for items like diagnostics that you want ran during disabled, + * autonomous, teleoperated and test. + * + *

This runs after the mode specific periodic functions, but before + * LiveWindow and SmartDashboard integrated updating. + */ +void Robot::RobotPeriodic() {} + +/** + * This autonomous (along with the chooser code above) shows how to select + * between different autonomous modes using the dashboard. The sendable chooser + * code works with the Java SmartDashboard. If you prefer the LabVIEW Dashboard, + * remove all of the chooser code and uncomment the GetString line to get the + * auto name from the text box below the Gyro. + * + * You can add additional auto modes by adding additional comparisons to the + * if-else structure below with additional strings. If using the SendableChooser + * make sure to add them to the chooser code above as well. + */ +void Robot::AutonomousInit() { + m_autoSelected = m_chooser.GetSelected(); + // m_autoSelected = SmartDashboard::GetString("Auto Selector", + // kAutoNameDefault); + fmt::print("Auto selected: {}\n", m_autoSelected); + + if (m_autoSelected == kAutoNameCustom) { + // Custom Auto goes here + } else { + // Default Auto goes here + } +} + +void Robot::AutonomousPeriodic() { + if (m_autoSelected == kAutoNameCustom) { + // Custom Auto goes here + } else { + // Default Auto goes here + } +} + +void Robot::TeleopInit() {} + +void Robot::TeleopPeriodic() {} + +void Robot::DisabledInit() {} + +void Robot::DisabledPeriodic() {} + +void Robot::TestInit() {} + +void Robot::TestPeriodic() {} + +void Robot::SimulationInit() {} + +void Robot::SimulationPeriodic() {} + +#ifndef RUNNING_FRC_TESTS +int main() { + return frc::StartRobot(); +} +#endif diff --git a/simulation/samples/CppSample/src/main/deploy/example.txt b/simulation/samples/CppSample/src/main/deploy/example.txt new file mode 100644 index 0000000000..683953917e --- /dev/null +++ b/simulation/samples/CppSample/src/main/deploy/example.txt @@ -0,0 +1,4 @@ +Files placed in this directory will be deployed to the RoboRIO into the + 'deploy' directory in the home folder. Use the 'frc::filesystem::GetDeployDirectory' + function from the 'frc/Filesystem.h' header to get a proper path relative to the deploy + directory. \ No newline at end of file diff --git a/simulation/samples/CppSample/src/main/include/Robot.h b/simulation/samples/CppSample/src/main/include/Robot.h new file mode 100644 index 0000000000..5677a88fe5 --- /dev/null +++ b/simulation/samples/CppSample/src/main/include/Robot.h @@ -0,0 +1,32 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include +#include + +class Robot : public frc::TimedRobot { + public: + void RobotInit() override; + void RobotPeriodic() override; + void AutonomousInit() override; + void AutonomousPeriodic() override; + void TeleopInit() override; + void TeleopPeriodic() override; + void DisabledInit() override; + void DisabledPeriodic() override; + void TestInit() override; + void TestPeriodic() override; + void SimulationInit() override; + void SimulationPeriodic() override; + + private: + frc::SendableChooser m_chooser; + const std::string kAutoNameDefault = "Default"; + const std::string kAutoNameCustom = "My Auto"; + std::string m_autoSelected; +}; diff --git a/simulation/samples/CppSample/src/test/cpp/main.cpp b/simulation/samples/CppSample/src/test/cpp/main.cpp new file mode 100644 index 0000000000..b8b23d2382 --- /dev/null +++ b/simulation/samples/CppSample/src/test/cpp/main.cpp @@ -0,0 +1,10 @@ +#include + +#include "gtest/gtest.h" + +int main(int argc, char** argv) { + HAL_Initialize(500, 0); + ::testing::InitGoogleTest(&argc, argv); + int ret = RUN_ALL_TESTS(); + return ret; +} diff --git a/simulation/samples/CppSample/vendordeps/NavX.json b/simulation/samples/CppSample/vendordeps/NavX.json new file mode 100644 index 0000000000..e978a5f745 --- /dev/null +++ b/simulation/samples/CppSample/vendordeps/NavX.json @@ -0,0 +1,40 @@ +{ + "fileName": "NavX.json", + "name": "NavX", + "version": "2024.1.0", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2024", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2024/" + ], + "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", + "javaDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-java", + "version": "2024.1.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-cpp", + "version": "2024.1.0", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": false, + "libName": "navx_frc", + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxraspbian", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/samples/CppSample/vendordeps/Phoenix6.json b/simulation/samples/CppSample/vendordeps/Phoenix6.json new file mode 100644 index 0000000000..032238505f --- /dev/null +++ b/simulation/samples/CppSample/vendordeps/Phoenix6.json @@ -0,0 +1,339 @@ +{ + "fileName": "Phoenix6.json", + "name": "CTRE-Phoenix (v6)", + "version": "24.3.0", + "frcYear": 2024, + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "mavenUrls": [ + "https://maven.ctr-electronics.com/release/" + ], + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", + "conflictsWith": [ + { + "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", + "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", + "offlineFileName": "Phoenix6And5.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "24.3.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "libName": "CTRE_SimCANCoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} \ No newline at end of file diff --git a/simulation/samples/CppSample/vendordeps/REVLib.json b/simulation/samples/CppSample/vendordeps/REVLib.json new file mode 100644 index 0000000000..f85acd4054 --- /dev/null +++ b/simulation/samples/CppSample/vendordeps/REVLib.json @@ -0,0 +1,74 @@ +{ + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.2.4", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.2.4" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.2.4", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/samples/CppSample/vendordeps/WPILibNewCommands.json b/simulation/samples/CppSample/vendordeps/WPILibNewCommands.json new file mode 100644 index 0000000000..67bf3898d5 --- /dev/null +++ b/simulation/samples/CppSample/vendordeps/WPILibNewCommands.json @@ -0,0 +1,38 @@ +{ + "fileName": "WPILibNewCommands.json", + "name": "WPILib-New-Commands", + "version": "1.0.0", + "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", + "frcYear": "2024", + "mavenUrls": [], + "jsonUrl": "", + "javaDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-java", + "version": "wpilib" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "edu.wpi.first.wpilibNewCommands", + "artifactId": "wpilibNewCommands-cpp", + "version": "wpilib", + "libName": "wpilibNewCommands", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxarm32", + "linuxarm64", + "windowsx86-64", + "windowsx86", + "linuxx86-64", + "osxuniversal" + ] + } + ] +} From 7b85e627d21b970c9f8ad27f50953fbb21759161 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 8 Jul 2024 14:16:31 -0700 Subject: [PATCH 20/65] fixed auth token issues --- exporter/SynthesisFusionAddin/.gitignore | 4 +++- exporter/SynthesisFusionAddin/proto/deps.py | 3 +-- fission/src/aps/APS.ts | 2 +- fission/src/systems/physics/PhysicsSystem.ts | 2 -- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/.gitignore b/exporter/SynthesisFusionAddin/.gitignore index eca33a87a8..fd821a7742 100644 --- a/exporter/SynthesisFusionAddin/.gitignore +++ b/exporter/SynthesisFusionAddin/.gitignore @@ -107,4 +107,6 @@ site-packages # env files **/.env -proto/proto_out \ No newline at end of file +proto/proto_out + +secrets diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 887649c1d6..80887337df 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -5,8 +5,7 @@ import adsk.core import adsk.fusion - -from src.general_imports import INTERNAL_ID +from ..src.general_imports import INTERNAL_ID system = platform.system() diff --git a/fission/src/aps/APS.ts b/fission/src/aps/APS.ts index f3a9854fe9..60fca2fdb2 100644 --- a/fission/src/aps/APS.ts +++ b/fission/src/aps/APS.ts @@ -84,7 +84,7 @@ class APS { ["response_type", "code"], ["client_id", CLIENT_ID], ["redirect_uri", callbackUrl], - ["scope", "data:read"], + ["scope", "data:create"], ["nonce", Date.now().toString()], ["prompt", "login"], ["code_challenge", codeChallenge], diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index dd87a1c79d..b208c6484b 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -723,8 +723,6 @@ class PhysicsSystem extends WorldSystem { let substeps = Math.max(1, Math.floor((lastDeltaT / STANDARD_SIMULATION_PERIOD) * STANDARD_SUB_STEPS)) substeps = Math.min(MAX_SUBSTEPS, Math.max(MIN_SUBSTEPS, substeps)) - console.log(`DeltaT: ${lastDeltaT.toFixed(5)}, Substeps: ${substeps}`) - this._joltInterface.Step(lastDeltaT, substeps) } From 8791fc508a6ff012e04f59d8999a43c1b176593f Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 9 Jul 2024 17:03:24 -0700 Subject: [PATCH 21/65] moved temp file upload into aps file --- exporter/SynthesisFusionAddin/src/APS/APS.py | 557 +++++++++++++++++- .../src/Parser/SynthesisParser/Parser.py | 2 +- 2 files changed, 557 insertions(+), 2 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index e31964e561..c6fec2f73f 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -7,6 +7,9 @@ import urllib.parse import urllib.request from dataclasses import dataclass +from typing import Any +from result import Ok, Err, Result, is_err +import requests from ..general_imports import ( APP_NAME, @@ -124,7 +127,7 @@ def refreshAuthToken(): "client_id": CLIENT_ID, "grant_type": "refresh_token", "refresh_token": APS_AUTH.refresh_token, - "scope": "data:read", + "scope": "data:create", } ).encode("utf-8") req = urllib.request.Request("https://developer.api.autodesk.com/authentication/v2/token", data=body) @@ -182,3 +185,555 @@ def getUserInfo() -> APSUserInfo | None: if APS_USER_INFO is not None: return APS_USER_INFO return loadUserInfo() + +def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_display_name: str) -> Result[str, None]: + """ + creates a folder on an APS project + + params: + auth - auth token + project - project blueprint; might be changed to just the project id + folder - the blueprint for the new folder + + returns: + success - the href of the new folder ; might be changed to the id in the future + failure - none if the API request fails ; the failure text will be printed + """ + headers = { + "Authorization": f"Bearer {auth}", + "Content-Type": "application/vnd.api+json" + + } + data: dict[str, Any] = { + "jsonapi": { + "version": "1.0" + }, + "data": { + "type": "folders", + "attributes": { + "name": folder_display_name, + "extension": { + "type": "folders:autodesk.core:Folder", + "version": "1.0" + } + }, + "relationships": { + "parent": { + "data": { + "type": "folders", + "id": f"{parent_folder_id}" + } + } + } + } + } + + res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders", headers=headers, data=data) + if not res.ok: + gm.ui.messageBox("", f"Failed to create folder: {res.text}") + return Err(None) + json: dict[str, Any] = res.json() + href: str = json["links"]["self"]["href"] + return Ok(href) + + +def file_path_to_file_name(file_path: str) -> str: + return file_path.split("/").pop() + +def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[str | None, None]: + """ + uploads mirabuf file to a specific folder in an APS project + the folder and project must be created and valid + if the file has already been created, it will use the APS versioning API to upload it as a new file version + + parameters: + project - the project reference object, used for it's id ; may be changed to project_id in the future + folder - the folder reference object, used for it's id ; may be changed to folder_id in the future + file_path - the path to the file on your machine, to be uploaded to APS + + returns: + success - if the file already exists, the new version id, otherwise, None + failure - none ; the cause of the failure will be printed + + potential causes of failure: + - invalid auth + - incorrectly formatted requests + - API update + - API down + + notes: + - this function is janky as hell, it should bubble errors up but I'm super lazy + - check appropriate called function ~~if~~ when this function fails + + todo: Change so a folder is not needed, and the entire project is checked for files + """ + + # data:create + global APS_AUTH + if not APS_AUTH: + gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") + auth_write = APS_AUTH + auth_read = APS_AUTH + # Get token from APS API later + file_name = file_path_to_file_name(file_path) + + #""" + #Assign APS File ID + #""" + #file_id_result = get_file_id(auth_read, project_id, folder_id, file_path) + #if file_id_result.is_err(): + # return Err(None) + #file_id: str | None = file_id_result.ok(); + #if not file_id == None: + # gm.ui.messageBox("UPLOAD ERROR","Mirabuf file already exists!") + # update_file_result: Result[str, None] = update_file_version(auth_write, project_id, folder_id, str(file_id), file_name, "1") # Grab real file version number + # if update_file_result.is_err(): + # return Err(None) + # new_version_id = str(update_file_result.ok()) + # return Ok(new_version_id) + + """ + Create APS Storage Location + """ + object_id = create_storage_location(auth_write, project_id, folder_id, file_name) + if object_id.is_err(): + return Err(None) + object_id = object_id.ok() + if object_id == None: + gm.ui.messageBox("UPLOAD ERROR", "Object id is none; check create storage location") + return Err(None) + (prefix, object_key) = str(object_id).split("/", 1) + bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod + + """ + Create Signed URL For APS Upload + """ + generate_signed_url_result = generate_signed_url(auth_read, bucket_key, object_key) + if generate_signed_url_result.is_err(): + return Err(None) + + (upload_key, signed_url) = generate_signed_url_result.ok() + if upload_file(signed_url, file_path).is_err(): + return Err(None) + + """ + Finish Upload and Initialize First File Version + """ + if complete_upload(auth_write, upload_key, bucket_key).is_err(): + return Err(None) + file_name = file_path_to_file_name(file_path) + (_lineage_id, _lineage_href) = create_first_file_version(auth_write, str(object_id), project_id, str(folder_id), file_name) + + return Ok(None) + + +def get_hub_id(auth: str, hub_name: str) -> Result[str | None, str]: + """ + gets a user's hub based on a hub name + + params: + auth - authorization token + hub_name - the name of the desired hub + + returns: + success - the hub's id or none if the hub doesn't exist + failure - the API text if there's an error + """ + + headers = { + "Authorization": f"Bearer {auth}" + } + hub_list_res = requests.get("https://developer.api.autodesk.com/project/v1/hubs", headers=headers) + if not hub_list_res.ok: + gm.ui.messageBox("UPLOAD ERROR", f"Failed to retrieve hubs: {hub_list_res.text}") + return Err(f"Failed to retrieve hubs: {hub_list_res.text}") + hub_list: list[dict[str, Any]] = hub_list_res.json() + for hub in hub_list: + if hub["attributes"]["name"] == hub_name: + id: str = hub["id"] + return Ok(id) + return Ok(None) + +def get_project_id(auth: str, hub_id: str, project_name: str) -> Result[str | None, str]: + """ + gets a project in a hub with a project name + + params: + auth - authorization token + hub_id - the id of the hub + project_name - the name of the desired project + + returns: + success - the project's id or none if the project doesn't exist + failure - the API text if there's an error + + notes: + - a hub_id can be derived from it's name with the get_hub_id function + """ + + headers = { + "Authorization": f"Bearer {auth}" + } + project_list_res = requests.get(f"https://developer.api.autodesk.com/project/v1/hubs/{hub_id}/projects", headers=headers) + if not project_list_res.ok: + gm.ui.messageBox("UPLOAD ERROR", f"Failed to retrieve hubs: {project_list_res.text}") + return Err(f"Failed to retrieve hubs: {project_list_res.text}") + project_list: list[dict[str, Any]] = project_list_res.json() + for project in project_list: + if project["attributes"]["name"] == project_name: + id: str = project["id"] + return Ok(id) + return Ok(None) + + +def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str, file_name: str, curr_file_version: str) -> Result[str, None]: + """ + updates an existing file in an APS folder + + params: + auth - authorization token + project - the project reference object that the file is contain within + folder - the folder reference object that the file is contained within + file_id - the id of the file in APS + file_name - the name of the file in APS ; ex. test.mira + + returns: + success - the new version_id + failure - none + + potential causes of failure: + - invalid auth + - file doesn't exist in that position / with that id / name ; fix: get_file_id() or smth + - version one of the file hasn't been created ; fix: create_first_file_version() + """ + + + object_id_res = create_storage_location(auth, project_id, folder_id, file_name) + if object_id_res.is_err(): + return Err(None) + object_id = object_id_res.ok() + + + headers = { + "Authorization:": f"Bearer {auth}", + "Content-Type": "application/vnd.api+json", + "Accept": "application/vnd.api+json" + + } + + attributes = { + "name": file_name, + "extension": { + "type": "versions:autodesk.core:File", + "version": curr_file_version + } + } + + refs = { + "data": { + "type": "versions", + "id": "", #version URN + "meta": { + "refType": "xrefs", + "direction": "to", + "extension": { + "type": "xrefs:autodesk.core:Xref", + "version": "1.1.0" + } + } + } + } + + relationships: dict[str, Any] = { + "item": { + "data": { + "type": "items", + "id": ""#wtf some id + } + }, + "storage": { + "data": { + "type": "objects", + "id": object_id + } + }, + "refs": refs + } + + data = { + "jsonapi": { + "version": "1.0" + }, + "data": { + "type": "versions", + "attributes": attributes, + "relationships": relationships + } + } + update_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, data=data) + if not update_res.ok: + gm.ui.messageBox("UPLOAD ERROR", f"updating file to new version failed: {update_res.text}") + return Err(None) + gm.ui.messageBox("UPLOAD ERROR", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") + new_id: str = update_res.json()["data"]["id"] + return Ok(new_id) + +def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> Result[str | None, str]: + """ + gets the file id given a file name + + params: + auth - authorization token + project - the project reference object that the file is contain within + folder - the folder reference object that the file is contained within + file_name - the name of the file in APS ; ex. test.mira + + returns: + success - the id of the file, or none if the file doesn't exist + failure - none + + potential causes of failure: + - incorrect auth + + notes: + - checking if a file exists is an intended use-case + """ + + headers: dict[str, str] = { + "Authorization": f"Bearer {auth}" + } + + file_list_res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders/{folder_id}/contents") + if not file_list_res.ok: + gm.ui.messageBox(f"UPLOAD ERROR: {file_list_res.text}", "Failed to get file list") + return Err("Failed to get file list") + file_list_json: list[dict[str, Any]] = file_list_res.json() + for file in file_list_json: + name: str = file["attributes"]["name"] + if name == file_name: + id: str = file["id"] + gm.ui.messageBox("UPLOAD ERROR", f"Found file {name} with id: {id}") + return Ok(id) + return Ok(None) + +def create_storage_location(auth: str, project_id: str, folder_id: str, file_name: str) -> Result[str, str]: + """ + creates a storage location (a bucket) + the bucket can be used to upload a file to + every file must have a reserved storage location + I believe at the moment, object, bucket, and storage location are all used semi-interchangeably by APS documentation + + params: + auth - authorization token + project - a project reference object used for project id ; may be changed to project_id later + folder - a folder reference object used for project id ; may be changed to folder_id later + file_name - the name of the file to be later stored in the bucket + + returns: + success - the object_id of the bucket, which can be split into a bucket_key and upload_key + failure - the API failure text + + notes: + - fails if the project doesn't exist or auth is invalid + - the folder must be inside the project, the storage location will be inside the folder + """ + + data = { + "jsonapi": { + "version": "1.0" + }, + "data": { + "type": "objects", + "attributes": { + "name": file_name + }, + "relationships": { + "target": { + "data": { "type": "folders", "id": f"{folder_id}" } + } + } + } + } + headers = { + "Authorization": f"Bearer {auth}", + "Content-Type": "application/vnd.api+json", + } + storage_location_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/storage", json=data, headers=headers) + if not storage_location_res.ok: + gm.ui.messageBox(f"UPLOAD ERROR: {storage_location_res.text}", f"Failed to create storage location") + return Err(f"Failed to create storage location: {storage_location_res.text}") + storage_location_json: dict[str, Any] = storage_location_res.json() + object_id: str = storage_location_json["data"]["id"] + return Ok(object_id) + +def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> Result[tuple[str, str], str]: + """ + generates a signed_url for a bucket, given a bucket_key and object_key + + params: + auth - authorization token + bucket_key - the key of the bucket that the file will be stored in + object_key - the key of the object that the file will be stored in + + returns: + success - the upload_key and the signed_url + failure - the API error + + notes: + - fails if auth, the bucket, or object keys are invalid + - both params are returned by the create_storage_location function + """ + + headers = { + "Authorization": f"Bearer {auth}", + } + signed_url_res = requests.get(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{object_key}/signeds3upload", headers=headers) + if not signed_url_res.ok: + gm.ui.messageBox("UPLOAD ERROR","Failed to get signed url") + return Err(f"Failed to get signed url: {signed_url_res.text}") + signed_url_json: dict[str, str] = signed_url_res.json() + return Ok((signed_url_json["uploadKey"], signed_url_json["urls"][0])) + +def upload_file(signed_url: str, file_path: str) -> Result[None, str]: + """ + uploads a file to APS given a signed_url a path to the file on your machine + + params: + signed_url - the url to used to upload the file to a specific bucket ; returned by the generate_signed_url function + file_path - the path of the file to be uploaded + + returns: + success - none + failure - the API error + + notes: + - fails if the auth or the signed URL are invalid + """ + + with open(file_path, 'rb') as f: + data = f.read() + upload_response = requests.put(url=signed_url, data=data) + if not upload_response.ok: + gm.ui.messageBox("UPLOAD ERROR", f"Failed to upload to signed url: {upload_response.text}") + return Err(f"Failed to upload to signed url: {upload_response.text}") + return Ok(None) + +def complete_upload(auth: str, upload_key: str, bucket_key: str) -> Result[None, str]: + """ + completes and verifies the APS file upload given the upload_key + + params: + auth - authorization token + upload_key - the key to verify the upload, returned by generate_signed_url function + bucket_key - the key of the bucket that the file was uploaded to, returned by the create_storage_location function + + returns: + success - none + failure - the API error + """ + + headers = { + "Authorization": f"Bearer {auth}", + "Content-Type": "application/json", + } + data = { + "uploadKey": upload_key + } + + completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{upload_key}/signeds3upload", json=data, headers=headers) + if not completed_res.ok: + gm.ui.messageBox(f"UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", "Failed to complete upload") + return Err(f"Failed to complete upload: {completed_res.text}") + return Ok(None) + +def create_first_file_version(auth: str, project_id: str, object_id: str, folder_id: str, file_name: str) -> Result[tuple[str, str], None]: + """ + initializes versioning for a file + + params: + auth - authorization token + project_id - the id of the project the file was uploaded to + object_id - the id of the object the file was uploaded to + folder_id - the id of the folder the file was uploaded to + file_name - the name of the file + + returns: + success - the lineage id of the versioning history of the file and the href to the new version + failure - none + + potential causes of failure + - incorrect auth + - the named file's upload was never completed + - invalid project, object, or folder id + + notes: + - super complex request, probably not written correctly, likely a dev error + """ + + headers = { + "Authorization:": f"Bearer {auth}", + "Content-Type": "application/vnd.api+json", + "Accept": "application/vnd.api+json" + + } + + attributes = { + "name": file_name, + "extension": { + "type": "items:autodesk.core:File", + "version": "1.0" + } + } + + relationships = { + "tip": { + "data": { + "type": "versions", + "id": "1" + } + }, + "parent": { + "data": { + "type": "folders", + "id": folder_id + } + } + } + + included = [ + { + "type": "versions", + "id": "1", + "attributes": attributes, + "relationships": { + "storage": { + "data": { + "type": "objects", + "id": object_id + } + } + } + }, + ] + + data = { + "jsonapi": { + "version": "1.0" + }, + "data": { + "type": "items", + "relationships": relationships + }, + "included": included + } + + first_version_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}L/items", data=data, headers=headers) + if not first_version_res.ok: + gm.ui.messageBox("UPLOAD ERROR", f"Failed to create first file version: {first_version_res.text}") + return Err(None) + first_version_json: dict[str, Any] = first_version_res.json() + + lineage_id: str = first_version_json["data"]["id"] + href: str = first_version_json["links"]["self"]["href"] + + return Ok((lineage_id, href)) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index dfab55eb5e..d51d1493a4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -12,7 +12,7 @@ from ..ExporterOptions import ExporterOptions, ExportMode, ExportLocation from . import Components, JointHierarchy, Joints, Materials, PDMessage from .Utilities import * -from ...temp_file_upload import upload_mirabuf # This line causes everything to break +from ...APS.APS import upload_mirabuf # This line causes everything to break class Parser: From b75974f992f33851317173eae3ab04920a6982ee Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 9 Jul 2024 22:23:10 -0700 Subject: [PATCH 22/65] Revert "fixed auth token issues" This reverts commit 7b85e627d21b970c9f8ad27f50953fbb21759161. --- exporter/SynthesisFusionAddin/.gitignore | 3 +- exporter/SynthesisFusionAddin/proto/deps.py | 3 +- exporter/SynthesisFusionAddin/src/APS/APS.py | 90 ++++++++++---------- fission/src/systems/physics/PhysicsSystem.ts | 2 + 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/exporter/SynthesisFusionAddin/.gitignore b/exporter/SynthesisFusionAddin/.gitignore index 9e33b772e7..9e93b946fb 100644 --- a/exporter/SynthesisFusionAddin/.gitignore +++ b/exporter/SynthesisFusionAddin/.gitignore @@ -107,6 +107,5 @@ site-packages # env files **/.env -proto/proto_out - .aps_auth +proto/proto_out diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 80887337df..887649c1d6 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -5,7 +5,8 @@ import adsk.core import adsk.fusion -from ..src.general_imports import INTERNAL_ID + +from src.general_imports import INTERNAL_ID system = platform.system() diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index c6fec2f73f..b93b5fdc46 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -230,7 +230,7 @@ def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_disp res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders", headers=headers, data=data) if not res.ok: - gm.ui.messageBox("", f"Failed to create folder: {res.text}") + print(f"Failed to create folder: {res.text}") return Err(None) json: dict[str, Any] = res.json() href: str = json["links"]["self"]["href"] @@ -268,39 +268,39 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st todo: Change so a folder is not needed, and the entire project is checked for files """ - # data:create global APS_AUTH if not APS_AUTH: gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") auth_write = APS_AUTH auth_read = APS_AUTH - # Get token from APS API later + gm.ui.messageBox("In Mirabuf") + file_name = file_path_to_file_name(file_path) - #""" - #Assign APS File ID - #""" - #file_id_result = get_file_id(auth_read, project_id, folder_id, file_path) - #if file_id_result.is_err(): - # return Err(None) - #file_id: str | None = file_id_result.ok(); - #if not file_id == None: - # gm.ui.messageBox("UPLOAD ERROR","Mirabuf file already exists!") - # update_file_result: Result[str, None] = update_file_version(auth_write, project_id, folder_id, str(file_id), file_name, "1") # Grab real file version number - # if update_file_result.is_err(): - # return Err(None) - # new_version_id = str(update_file_result.ok()) - # return Ok(new_version_id) + """ + Assign APS File ID + """ + file_id_result = get_file_id(_auth, project_id, folder_id, file_path) + if file_id_result.is_err(): + return Err(None) + file_id: str | None = file_id_result.ok(); + if not file_id == None: + print("Mirabuf file already exists!") + update_file_result: Result[str, None] = update_file_version(_auth, project_id, folder_id, str(file_id), file_name, "1") # Grab real file version number + if update_file_result.is_err(): + return Err(None) + new_version_id = str(update_file_result.ok()) + return Ok(new_version_id) """ Create APS Storage Location """ - object_id = create_storage_location(auth_write, project_id, folder_id, file_name) + object_id = create_storage_location(_auth, project_id, folder_id, file_name) if object_id.is_err(): return Err(None) object_id = object_id.ok() if object_id == None: - gm.ui.messageBox("UPLOAD ERROR", "Object id is none; check create storage location") + print("Object id is none; check create storage location") return Err(None) (prefix, object_key) = str(object_id).split("/", 1) bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod @@ -308,21 +308,21 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ Create Signed URL For APS Upload """ - generate_signed_url_result = generate_signed_url(auth_read, bucket_key, object_key) + generate_signed_url_result = generate_signed_url(_auth, bucket_key, object_key) if generate_signed_url_result.is_err(): return Err(None) - (upload_key, signed_url) = generate_signed_url_result.ok() + (upload_key, signed_url) = str(generate_signed_url_result.ok()) if upload_file(signed_url, file_path).is_err(): return Err(None) """ Finish Upload and Initialize First File Version """ - if complete_upload(auth_write, upload_key, bucket_key).is_err(): + if complete_upload(_auth, upload_key, bucket_key).is_err(): return Err(None) file_name = file_path_to_file_name(file_path) - (_lineage_id, _lineage_href) = create_first_file_version(auth_write, str(object_id), project_id, str(folder_id), file_name) + (_lineage_id, _lineage_href) = create_first_file_version(_auth, str(object_id), project_id, str(folder_id), file_name) return Ok(None) @@ -345,7 +345,7 @@ def get_hub_id(auth: str, hub_name: str) -> Result[str | None, str]: } hub_list_res = requests.get("https://developer.api.autodesk.com/project/v1/hubs", headers=headers) if not hub_list_res.ok: - gm.ui.messageBox("UPLOAD ERROR", f"Failed to retrieve hubs: {hub_list_res.text}") + print(f"Failed to retrieve hubs: {hub_list_res.text}") return Err(f"Failed to retrieve hubs: {hub_list_res.text}") hub_list: list[dict[str, Any]] = hub_list_res.json() for hub in hub_list: @@ -376,7 +376,7 @@ def get_project_id(auth: str, hub_id: str, project_name: str) -> Result[str | No } project_list_res = requests.get(f"https://developer.api.autodesk.com/project/v1/hubs/{hub_id}/projects", headers=headers) if not project_list_res.ok: - gm.ui.messageBox("UPLOAD ERROR", f"Failed to retrieve hubs: {project_list_res.text}") + print(f"Failed to retrieve hubs: {project_list_res.text}") return Err(f"Failed to retrieve hubs: {project_list_res.text}") project_list: list[dict[str, Any]] = project_list_res.json() for project in project_list: @@ -472,9 +472,9 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str } update_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, data=data) if not update_res.ok: - gm.ui.messageBox("UPLOAD ERROR", f"updating file to new version failed: {update_res.text}") + print(f"updating file to new version failed: {update_res.text}") return Err(None) - gm.ui.messageBox("UPLOAD ERROR", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") + print(f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") new_id: str = update_res.json()["data"]["id"] return Ok(new_id) @@ -499,20 +499,16 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> R - checking if a file exists is an intended use-case """ - headers: dict[str, str] = { - "Authorization": f"Bearer {auth}" - } - file_list_res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders/{folder_id}/contents") if not file_list_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR: {file_list_res.text}", "Failed to get file list") + print("Failed to get file list") return Err("Failed to get file list") file_list_json: list[dict[str, Any]] = file_list_res.json() for file in file_list_json: name: str = file["attributes"]["name"] if name == file_name: id: str = file["id"] - gm.ui.messageBox("UPLOAD ERROR", f"Found file {name} with id: {id}") + print(f"Found file {name} with id: {id}") return Ok(id) return Ok(None) @@ -548,19 +544,20 @@ def create_storage_location(auth: str, project_id: str, folder_id: str, file_nam "name": file_name }, "relationships": { - "target": { - "data": { "type": "folders", "id": f"{folder_id}" } + "target": { + "data": { "type": "folders", "id": f"{folder_id}" } } } } } headers = { - "Authorization": f"Bearer {auth}", + "Authorization:": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", + "Accept": "application/vnd.api+json" } - storage_location_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/storage", json=data, headers=headers) + storage_location_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/storage", data=data, headers=headers) if not storage_location_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR: {storage_location_res.text}", f"Failed to create storage location") + print(f"Failed to create storage location") return Err(f"Failed to create storage location: {storage_location_res.text}") storage_location_json: dict[str, Any] = storage_location_res.json() object_id: str = storage_location_json["data"]["id"] @@ -585,11 +582,11 @@ def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> Result[t """ headers = { - "Authorization": f"Bearer {auth}", + "Authorization:": f"Bearer {auth}", } signed_url_res = requests.get(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{object_key}/signeds3upload", headers=headers) if not signed_url_res.ok: - gm.ui.messageBox("UPLOAD ERROR","Failed to get signed url") + print("Failed to get signed url") return Err(f"Failed to get signed url: {signed_url_res.text}") signed_url_json: dict[str, str] = signed_url_res.json() return Ok((signed_url_json["uploadKey"], signed_url_json["urls"][0])) @@ -614,7 +611,7 @@ def upload_file(signed_url: str, file_path: str) -> Result[None, str]: data = f.read() upload_response = requests.put(url=signed_url, data=data) if not upload_response.ok: - gm.ui.messageBox("UPLOAD ERROR", f"Failed to upload to signed url: {upload_response.text}") + print(f"Failed to upload to signed url: {upload_response.text}") return Err(f"Failed to upload to signed url: {upload_response.text}") return Ok(None) @@ -633,16 +630,17 @@ def complete_upload(auth: str, upload_key: str, bucket_key: str) -> Result[None, """ headers = { - "Authorization": f"Bearer {auth}", - "Content-Type": "application/json", + "Authorization:": f"Bearer {auth}", + "Content-Type": "application/vnd.api+json", + "Accept": "application/vnd.api+json" } data = { "uploadKey": upload_key } - completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{upload_key}/signeds3upload", json=data, headers=headers) + completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{upload_key}/signeds3upload", data=data, headers=headers) if not completed_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", "Failed to complete upload") + print(f"Failed to complete upload: {completed_res.text}") return Err(f"Failed to complete upload: {completed_res.text}") return Ok(None) @@ -729,7 +727,7 @@ def create_first_file_version(auth: str, project_id: str, object_id: str, folder first_version_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}L/items", data=data, headers=headers) if not first_version_res.ok: - gm.ui.messageBox("UPLOAD ERROR", f"Failed to create first file version: {first_version_res.text}") + print(f"Failed to create first file version: {first_version_res.text}") return Err(None) first_version_json: dict[str, Any] = first_version_res.json() diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 7d9bce0944..82947cb1d0 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -720,6 +720,8 @@ class PhysicsSystem extends WorldSystem { let substeps = Math.max(1, Math.floor((lastDeltaT / STANDARD_SIMULATION_PERIOD) * STANDARD_SUB_STEPS)) substeps = Math.min(MAX_SUBSTEPS, Math.max(MIN_SUBSTEPS, substeps)) + console.log(`DeltaT: ${lastDeltaT.toFixed(5)}, Substeps: ${substeps}`) + this._joltInterface.Step(lastDeltaT, substeps) } From 589cb48005376d4fd48cd7cec9e46e4a93620c19 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 9 Jul 2024 23:37:40 -0700 Subject: [PATCH 23/65] Reapply "fixed auth token issues" This reverts commit b75974f992f33851317173eae3ab04920a6982ee. --- exporter/SynthesisFusionAddin/.gitignore | 3 +- exporter/SynthesisFusionAddin/proto/deps.py | 3 +- exporter/SynthesisFusionAddin/src/APS/APS.py | 90 ++++++++++---------- fission/src/systems/physics/PhysicsSystem.ts | 2 - 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/exporter/SynthesisFusionAddin/.gitignore b/exporter/SynthesisFusionAddin/.gitignore index 9e93b946fb..9e33b772e7 100644 --- a/exporter/SynthesisFusionAddin/.gitignore +++ b/exporter/SynthesisFusionAddin/.gitignore @@ -107,5 +107,6 @@ site-packages # env files **/.env -.aps_auth proto/proto_out + +.aps_auth diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 887649c1d6..80887337df 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -5,8 +5,7 @@ import adsk.core import adsk.fusion - -from src.general_imports import INTERNAL_ID +from ..src.general_imports import INTERNAL_ID system = platform.system() diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index b93b5fdc46..c6fec2f73f 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -230,7 +230,7 @@ def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_disp res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders", headers=headers, data=data) if not res.ok: - print(f"Failed to create folder: {res.text}") + gm.ui.messageBox("", f"Failed to create folder: {res.text}") return Err(None) json: dict[str, Any] = res.json() href: str = json["links"]["self"]["href"] @@ -268,39 +268,39 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st todo: Change so a folder is not needed, and the entire project is checked for files """ + # data:create global APS_AUTH if not APS_AUTH: gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") auth_write = APS_AUTH auth_read = APS_AUTH - gm.ui.messageBox("In Mirabuf") - + # Get token from APS API later file_name = file_path_to_file_name(file_path) - """ - Assign APS File ID - """ - file_id_result = get_file_id(_auth, project_id, folder_id, file_path) - if file_id_result.is_err(): - return Err(None) - file_id: str | None = file_id_result.ok(); - if not file_id == None: - print("Mirabuf file already exists!") - update_file_result: Result[str, None] = update_file_version(_auth, project_id, folder_id, str(file_id), file_name, "1") # Grab real file version number - if update_file_result.is_err(): - return Err(None) - new_version_id = str(update_file_result.ok()) - return Ok(new_version_id) + #""" + #Assign APS File ID + #""" + #file_id_result = get_file_id(auth_read, project_id, folder_id, file_path) + #if file_id_result.is_err(): + # return Err(None) + #file_id: str | None = file_id_result.ok(); + #if not file_id == None: + # gm.ui.messageBox("UPLOAD ERROR","Mirabuf file already exists!") + # update_file_result: Result[str, None] = update_file_version(auth_write, project_id, folder_id, str(file_id), file_name, "1") # Grab real file version number + # if update_file_result.is_err(): + # return Err(None) + # new_version_id = str(update_file_result.ok()) + # return Ok(new_version_id) """ Create APS Storage Location """ - object_id = create_storage_location(_auth, project_id, folder_id, file_name) + object_id = create_storage_location(auth_write, project_id, folder_id, file_name) if object_id.is_err(): return Err(None) object_id = object_id.ok() if object_id == None: - print("Object id is none; check create storage location") + gm.ui.messageBox("UPLOAD ERROR", "Object id is none; check create storage location") return Err(None) (prefix, object_key) = str(object_id).split("/", 1) bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod @@ -308,21 +308,21 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ Create Signed URL For APS Upload """ - generate_signed_url_result = generate_signed_url(_auth, bucket_key, object_key) + generate_signed_url_result = generate_signed_url(auth_read, bucket_key, object_key) if generate_signed_url_result.is_err(): return Err(None) - (upload_key, signed_url) = str(generate_signed_url_result.ok()) + (upload_key, signed_url) = generate_signed_url_result.ok() if upload_file(signed_url, file_path).is_err(): return Err(None) """ Finish Upload and Initialize First File Version """ - if complete_upload(_auth, upload_key, bucket_key).is_err(): + if complete_upload(auth_write, upload_key, bucket_key).is_err(): return Err(None) file_name = file_path_to_file_name(file_path) - (_lineage_id, _lineage_href) = create_first_file_version(_auth, str(object_id), project_id, str(folder_id), file_name) + (_lineage_id, _lineage_href) = create_first_file_version(auth_write, str(object_id), project_id, str(folder_id), file_name) return Ok(None) @@ -345,7 +345,7 @@ def get_hub_id(auth: str, hub_name: str) -> Result[str | None, str]: } hub_list_res = requests.get("https://developer.api.autodesk.com/project/v1/hubs", headers=headers) if not hub_list_res.ok: - print(f"Failed to retrieve hubs: {hub_list_res.text}") + gm.ui.messageBox("UPLOAD ERROR", f"Failed to retrieve hubs: {hub_list_res.text}") return Err(f"Failed to retrieve hubs: {hub_list_res.text}") hub_list: list[dict[str, Any]] = hub_list_res.json() for hub in hub_list: @@ -376,7 +376,7 @@ def get_project_id(auth: str, hub_id: str, project_name: str) -> Result[str | No } project_list_res = requests.get(f"https://developer.api.autodesk.com/project/v1/hubs/{hub_id}/projects", headers=headers) if not project_list_res.ok: - print(f"Failed to retrieve hubs: {project_list_res.text}") + gm.ui.messageBox("UPLOAD ERROR", f"Failed to retrieve hubs: {project_list_res.text}") return Err(f"Failed to retrieve hubs: {project_list_res.text}") project_list: list[dict[str, Any]] = project_list_res.json() for project in project_list: @@ -472,9 +472,9 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str } update_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, data=data) if not update_res.ok: - print(f"updating file to new version failed: {update_res.text}") + gm.ui.messageBox("UPLOAD ERROR", f"updating file to new version failed: {update_res.text}") return Err(None) - print(f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") + gm.ui.messageBox("UPLOAD ERROR", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") new_id: str = update_res.json()["data"]["id"] return Ok(new_id) @@ -499,16 +499,20 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> R - checking if a file exists is an intended use-case """ + headers: dict[str, str] = { + "Authorization": f"Bearer {auth}" + } + file_list_res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders/{folder_id}/contents") if not file_list_res.ok: - print("Failed to get file list") + gm.ui.messageBox(f"UPLOAD ERROR: {file_list_res.text}", "Failed to get file list") return Err("Failed to get file list") file_list_json: list[dict[str, Any]] = file_list_res.json() for file in file_list_json: name: str = file["attributes"]["name"] if name == file_name: id: str = file["id"] - print(f"Found file {name} with id: {id}") + gm.ui.messageBox("UPLOAD ERROR", f"Found file {name} with id: {id}") return Ok(id) return Ok(None) @@ -544,20 +548,19 @@ def create_storage_location(auth: str, project_id: str, folder_id: str, file_nam "name": file_name }, "relationships": { - "target": { - "data": { "type": "folders", "id": f"{folder_id}" } + "target": { + "data": { "type": "folders", "id": f"{folder_id}" } } } } } headers = { - "Authorization:": f"Bearer {auth}", + "Authorization": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", - "Accept": "application/vnd.api+json" } - storage_location_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/storage", data=data, headers=headers) + storage_location_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/storage", json=data, headers=headers) if not storage_location_res.ok: - print(f"Failed to create storage location") + gm.ui.messageBox(f"UPLOAD ERROR: {storage_location_res.text}", f"Failed to create storage location") return Err(f"Failed to create storage location: {storage_location_res.text}") storage_location_json: dict[str, Any] = storage_location_res.json() object_id: str = storage_location_json["data"]["id"] @@ -582,11 +585,11 @@ def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> Result[t """ headers = { - "Authorization:": f"Bearer {auth}", + "Authorization": f"Bearer {auth}", } signed_url_res = requests.get(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{object_key}/signeds3upload", headers=headers) if not signed_url_res.ok: - print("Failed to get signed url") + gm.ui.messageBox("UPLOAD ERROR","Failed to get signed url") return Err(f"Failed to get signed url: {signed_url_res.text}") signed_url_json: dict[str, str] = signed_url_res.json() return Ok((signed_url_json["uploadKey"], signed_url_json["urls"][0])) @@ -611,7 +614,7 @@ def upload_file(signed_url: str, file_path: str) -> Result[None, str]: data = f.read() upload_response = requests.put(url=signed_url, data=data) if not upload_response.ok: - print(f"Failed to upload to signed url: {upload_response.text}") + gm.ui.messageBox("UPLOAD ERROR", f"Failed to upload to signed url: {upload_response.text}") return Err(f"Failed to upload to signed url: {upload_response.text}") return Ok(None) @@ -630,17 +633,16 @@ def complete_upload(auth: str, upload_key: str, bucket_key: str) -> Result[None, """ headers = { - "Authorization:": f"Bearer {auth}", - "Content-Type": "application/vnd.api+json", - "Accept": "application/vnd.api+json" + "Authorization": f"Bearer {auth}", + "Content-Type": "application/json", } data = { "uploadKey": upload_key } - completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{upload_key}/signeds3upload", data=data, headers=headers) + completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{upload_key}/signeds3upload", json=data, headers=headers) if not completed_res.ok: - print(f"Failed to complete upload: {completed_res.text}") + gm.ui.messageBox(f"UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", "Failed to complete upload") return Err(f"Failed to complete upload: {completed_res.text}") return Ok(None) @@ -727,7 +729,7 @@ def create_first_file_version(auth: str, project_id: str, object_id: str, folder first_version_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}L/items", data=data, headers=headers) if not first_version_res.ok: - print(f"Failed to create first file version: {first_version_res.text}") + gm.ui.messageBox("UPLOAD ERROR", f"Failed to create first file version: {first_version_res.text}") return Err(None) first_version_json: dict[str, Any] = first_version_res.json() diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 82947cb1d0..7d9bce0944 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -720,8 +720,6 @@ class PhysicsSystem extends WorldSystem { let substeps = Math.max(1, Math.floor((lastDeltaT / STANDARD_SIMULATION_PERIOD) * STANDARD_SUB_STEPS)) substeps = Math.min(MAX_SUBSTEPS, Math.max(MIN_SUBSTEPS, substeps)) - console.log(`DeltaT: ${lastDeltaT.toFixed(5)}, Substeps: ${substeps}`) - this._joltInterface.Step(lastDeltaT, substeps) } From 3b3a68b9a8c987b8e4e8679befbbf27f3b942ecd Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 10 Jul 2024 17:15:08 -0700 Subject: [PATCH 24/65] working on fixing error --- exporter/SynthesisFusionAddin/src/APS/APS.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index c6fec2f73f..1bbd9e2072 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -75,17 +75,19 @@ def getAuth() -> APSAuth: try: with open(auth_path, "rb") as f: p = pickle.load(f) + logging.getLogger(f"{INTERNAL_ID}").info(f"Auth Path: {auth_path}\n{json.dumps(p)}") APS_AUTH = APSAuth( access_token=p["access_token"], refresh_token=p["refresh_token"], expires_in=p["expires_in"], - expires_at=int(p["expires_in"] * 1000), + expires_at=p["expires_at"], token_type=p["token_type"], ) except: raise Exception("Need to sign in!") curr_time = int(time.time() * 1000) if curr_time >= APS_AUTH.expires_at: + logging.getLogger(f"{INTERNAL_ID}").info(f"Refreshing {curr_time}\n{json.dumps(APS_AUTH.__dict__)}") refreshAuthToken() if APS_USER_INFO is None: loadUserInfo() @@ -97,15 +99,17 @@ def convertAuthToken(code: str): authUrl = f'http://localhost:80/api/aps/code/?code={code}&redirect_uri={urllib.parse.quote_plus("http://localhost:80/api/aps/exporter/")}' res = urllib.request.urlopen(authUrl) data = _res_json(res)["response"] + curr_time = time.time() * 1000 APS_AUTH = APSAuth( access_token=data["access_token"], refresh_token=data["refresh_token"], expires_in=data["expires_in"], - expires_at=int(data["expires_in"] * 1000), + expires_at=int(curr_time + (p["expires_in"] * 1000)), token_type=data["token_type"], ) with open(auth_path, "wb") as f: - pickle.dump(data, f) + logging.getLogger(f"{INTERNAL_ID}").info(f"APS AUTH: {json.dumps(APS_AUTH.__dict__)}") + pickle.dump(APS_AUTH.__dict__, f) f.close() loadUserInfo() @@ -136,11 +140,12 @@ def refreshAuthToken(): try: res = urllib.request.urlopen(req) data = _res_json(res) + curr_time = time.time() * 1000 APS_AUTH = APSAuth( access_token=data["access_token"], refresh_token=data["refresh_token"], expires_in=data["expires_in"], - expires_at=int(data["expires_in"] * 1000), + expires_at=int(curr_time + (data["expires_in"] * 1000)), token_type=data["token_type"], ) except urllib.request.HTTPError as e: From 00d73ebdc814f2598f64154289d53391d3a40b5b Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 12 Jul 2024 19:15:39 -0700 Subject: [PATCH 25/65] remove exception and fix logging message --- exporter/SynthesisFusionAddin/src/APS/APS.py | 7 ++++--- exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 1bbd9e2072..be18f58382 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -68,7 +68,7 @@ def getCodeChallenge() -> str | None: return data["challenge"] -def getAuth() -> APSAuth: +def getAuth() -> APSAuth | None: global APS_AUTH if APS_AUTH is not None: return APS_AUTH @@ -84,7 +84,8 @@ def getAuth() -> APSAuth: token_type=p["token_type"], ) except: - raise Exception("Need to sign in!") + gm.ui.messageBox("Sign in","Sign in") + return None curr_time = int(time.time() * 1000) if curr_time >= APS_AUTH.expires_at: logging.getLogger(f"{INTERNAL_ID}").info(f"Refreshing {curr_time}\n{json.dumps(APS_AUTH.__dict__)}") @@ -104,7 +105,7 @@ def convertAuthToken(code: str): access_token=data["access_token"], refresh_token=data["refresh_token"], expires_in=data["expires_in"], - expires_at=int(curr_time + (p["expires_in"] * 1000)), + expires_at=int(curr_time + (data["expires_in"] * 1000)), token_type=data["token_type"], ) with open(auth_path, "wb") as f: diff --git a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py index bacc6e094b..b99e71fe2c 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py @@ -139,7 +139,7 @@ def notify(self, args): convertAuthToken(data["code"]) except: - gm.ui.messageBox("Failed:\n".format(traceback.format_exc())) - logging.getLogger(f"{INTERNAL_ID}").error("Failed:\n".format(traceback.format_exc())) + gm.ui.messageBox("Failed:{}\n".format(traceback.format_exc())) + logging.getLogger(f"{INTERNAL_ID}").error("Failed:{}\n".format(traceback.format_exc())) if palette: palette.deleteMe() From f35e83013af139d8dad9340c756cb72ef30fa97b Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Sat, 13 Jul 2024 21:28:02 -0700 Subject: [PATCH 26/65] moved APS AUTH to global scope --- exporter/SynthesisFusionAddin/src/APS/APS.py | 11 +++-------- exporter/SynthesisFusionAddin/src/strings.py | 2 ++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index be18f58382..6c853a7897 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -15,6 +15,8 @@ APP_NAME, DESCRIPTION, INTERNAL_ID, + APS_AUTH, + APS_USER_INFO, gm, my_addin_path, root_logger, @@ -23,10 +25,6 @@ CLIENT_ID = "GCxaewcLjsYlK8ud7Ka9AKf9dPwMR3e4GlybyfhAK2zvl3tU" auth_path = os.path.abspath(os.path.join(my_addin_path, "..", ".aps_auth")) -APS_AUTH = None -APS_USER_INFO = None - - @dataclass class APSAuth: access_token: str @@ -69,7 +67,6 @@ def getCodeChallenge() -> str | None: def getAuth() -> APSAuth | None: - global APS_AUTH if APS_AUTH is not None: return APS_AUTH try: @@ -99,6 +96,7 @@ def convertAuthToken(code: str): global APS_AUTH authUrl = f'http://localhost:80/api/aps/code/?code={code}&redirect_uri={urllib.parse.quote_plus("http://localhost:80/api/aps/exporter/")}' res = urllib.request.urlopen(authUrl) + data = _res_json(res)["response"] curr_time = time.time() * 1000 APS_AUTH = APSAuth( @@ -117,7 +115,6 @@ def convertAuthToken(code: str): def removeAuth(): - global APS_AUTH, APS_USER_INFO APS_AUTH = None APS_USER_INFO = None pathlib.Path.unlink(pathlib.Path(auth_path)) @@ -156,7 +153,6 @@ def refreshAuthToken(): def loadUserInfo() -> APSUserInfo | None: - global APS_AUTH if not APS_AUTH: return None global APS_USER_INFO @@ -275,7 +271,6 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ # data:create - global APS_AUTH if not APS_AUTH: gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") auth_write = APS_AUTH diff --git a/exporter/SynthesisFusionAddin/src/strings.py b/exporter/SynthesisFusionAddin/src/strings.py index 6e3aa109e2..01f85eac40 100644 --- a/exporter/SynthesisFusionAddin/src/strings.py +++ b/exporter/SynthesisFusionAddin/src/strings.py @@ -2,3 +2,5 @@ APP_TITLE = "Synthesis Robot Exporter" DESCRIPTION = "Exports files from Fusion into the Synthesis Format" INTERNAL_ID = "synthesis" +APS_AUTH = None +APS_USER_INFO = None From 62df037d67b23bd1e34618732765976bee06e8fd Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Mon, 15 Jul 2024 01:07:56 -0600 Subject: [PATCH 27/65] Created basic progress notification. Currently integrating into mirabuf assembly spawning --- fission/src/Synthesis.tsx | 12 ++ fission/src/mirabuf/MirabufInstance.ts | 6 +- fission/src/mirabuf/MirabufParser.ts | 5 +- fission/src/mirabuf/MirabufSceneObject.ts | 5 +- .../ui/components/ProgressNotification.tsx | 146 ++++++++++++++++++ 5 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 fission/src/ui/components/ProgressNotification.tsx diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 9edd6954a1..ff93f14609 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -57,6 +57,7 @@ import ImportLocalMirabufModal from "@/modals/mirabuf/ImportLocalMirabufModal.ts import APS from "./aps/APS.ts" import Skybox from "./ui/components/Skybox.tsx" import PokerPanel from "@/panels/PokerPanel.tsx" +import ProgressNotifications, { ProgressHandle, ProgressHandleStatus } from "./ui/components/ProgressNotification.tsx" const DEFAULT_MIRA_PATH = "/api/mira/Robots/Team 2471 (2018)_v7.mira" @@ -97,12 +98,17 @@ function Synthesis() { } const setup = async () => { + const setupProgress = new ProgressHandle("Spawning Default Robot") + setupProgress.Update("Checking cache...", 0.1) + const info = await MirabufCachingService.CacheRemote(mira_path, MiraType.ROBOT) .catch(_ => MirabufCachingService.CacheRemote(DEFAULT_MIRA_PATH, MiraType.ROBOT)) .catch(console.error) const miraAssembly = await MirabufCachingService.Get(info!.id, MiraType.ROBOT) + setupProgress.Update("Parsing assembly...", 0.5) + await (async () => { if (!miraAssembly || !(miraAssembly instanceof mirabuf.Assembly)) { return @@ -111,11 +117,16 @@ function Synthesis() { const parser = new MirabufParser(miraAssembly) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { console.error(`Assembly Parser produced significant errors for '${miraAssembly.info!.name!}'`) + setupProgress.Update("Failed to parse assembly", 1, ProgressHandleStatus.Error) return } + setupProgress.Update("Creating scene object...", 0.9) + const mirabufSceneObject = new MirabufSceneObject(new MirabufInstance(parser), miraAssembly.info!.name!) World.SceneRenderer.RegisterSceneObject(mirabufSceneObject) + + setupProgress.Update("Done", 1, ProgressHandleStatus.Done) })() } @@ -175,6 +186,7 @@ function Synthesis() { {modalElement}

)} + diff --git a/fission/src/mirabuf/MirabufInstance.ts b/fission/src/mirabuf/MirabufInstance.ts index 390c6f3ef6..c940969b9d 100644 --- a/fission/src/mirabuf/MirabufInstance.ts +++ b/fission/src/mirabuf/MirabufInstance.ts @@ -2,6 +2,7 @@ import * as THREE from "three" import { mirabuf } from "../proto/mirabuf" import MirabufParser, { ParseErrorSeverity } from "./MirabufParser.ts" import World from "@/systems/World.ts" +import { ProgressHandle } from "@/ui/components/ProgressNotification.tsx" const WIREFRAME = false @@ -95,7 +96,7 @@ class MirabufInstance { return this._meshes } - public constructor(parser: MirabufParser, materialStyle?: MaterialStyle) { + public constructor(parser: MirabufParser, materialStyle?: MaterialStyle, progressHandle?: ProgressHandle) { if (parser.errors.some(x => x[0] >= ParseErrorSeverity.Unimportable)) { throw new Error("Parser has significant errors...") } @@ -104,7 +105,10 @@ class MirabufInstance { this._materials = new Map() this._meshes = new Map() + progressHandle?.Update('Loading materials...', 0.4) this.LoadMaterials(materialStyle ?? MaterialStyle.Regular) + + progressHandle?.Update('Creating meshes...', 0.5) this.CreateMeshes() } diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index a9a814e85a..515dd9ecdc 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -1,6 +1,7 @@ import * as THREE from "three" import { mirabuf } from "@/proto/mirabuf" import { MirabufTransform_ThreeMatrix4 } from "@/util/TypeConversions" +import { ProgressHandle } from "@/ui/components/ProgressNotification" export enum ParseErrorSeverity { Unimportable = 10, @@ -70,11 +71,13 @@ class MirabufParser { return this._rootNode } - public constructor(assembly: mirabuf.Assembly) { + public constructor(assembly: mirabuf.Assembly, progressHandle?: ProgressHandle) { this._assembly = assembly this._errors = new Array() this._globalTransforms = new Map() + progressHandle?.Update("Parsing assembly...", 0.3) + this.GenerateTreeValues() this.LoadGlobalTransforms() diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index be6ae81225..2ac5ad4595 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -12,6 +12,7 @@ import Mechanism from "@/systems/physics/Mechanism" import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" import InputSystem from "@/systems/input/InputSystem" import TransformGizmos from "@/ui/components/TransformGizmos" +import { ProgressHandle } from "@/ui/components/ProgressNotification" const DEBUG_BODIES = false @@ -40,12 +41,14 @@ class MirabufSceneObject extends SceneObject { return this._mechanism } - public constructor(mirabufInstance: MirabufInstance, assemblyName: string) { + public constructor(mirabufInstance: MirabufInstance, assemblyName: string, progressHandle?: ProgressHandle) { super() this._mirabufInstance = mirabufInstance this._assemblyName = assemblyName + progressHandle?.Update("Creating mechanism...", 0.9) + this._mechanism = World.PhysicsSystem.CreateMechanismFromParser(this._mirabufInstance.parser) if (this._mechanism.layerReserve) { this._physicsLayerReserve = this._mechanism.layerReserve diff --git a/fission/src/ui/components/ProgressNotification.tsx b/fission/src/ui/components/ProgressNotification.tsx new file mode 100644 index 0000000000..097a759039 --- /dev/null +++ b/fission/src/ui/components/ProgressNotification.tsx @@ -0,0 +1,146 @@ +import { styled, Typography } from "@mui/material" +import { Box } from "@mui/system" +import { useEffect, useReducer } from "react" + +const handleMap = new Map() + +const TypoStyled = styled(Typography)((_) => ({ + fontFamily: "Artifakt", + textAlign: "center" +})) + +function ProgressNotifications() { + + const [progressElements, updateProgressElements] = useReducer(() => { + return handleMap.size > 0 + ? + [...handleMap.entries()].map(([id, handle]) => { return ( + + + {handle.title} + { handle.message.length > 0 ? {handle.message} : <> } + + + + )}) + : undefined + }, undefined) + + useEffect(() => { + const onHandleUpdate = (e: Event) => { + const handle = (e as ProgressEvent).handle; + if (handle.status > 0) { + setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 1000) + console.debug('Deleting') + } + handleMap.set(handle.handleId, handle) + updateProgressElements() + } + + window.addEventListener(ProgressEvent.EVENT_KEY, onHandleUpdate) + return () => { + window.removeEventListener(ProgressEvent.EVENT_KEY, onHandleUpdate) + } + }, [updateProgressElements]) + + return ( + + { progressElements ?? <> } + + ) +} + +let nextHandleId = 0 + +export enum ProgressHandleStatus { + inProgress = 0, + Done = 1, + Error = 2 +} + +export class ProgressHandle { + + private _handleId: number + private _title: string + public message: string = "" + public progress: number = 0.0 + public status: ProgressHandleStatus = ProgressHandleStatus.inProgress + + public get handleId() { return this._handleId } + public get title() { return this._title } + + public constructor(title: string) { + this._handleId = nextHandleId++ + this._title = title + } + + public Update(message: string, progress: number, status?: ProgressHandleStatus) { + this.message = message + this.progress = progress + status && (this.status = status) + + this.Push() + } + + public Push() { + window.dispatchEvent(new ProgressEvent(this)) + } +} + +class ProgressEvent extends Event { + public static readonly EVENT_KEY = 'ProgressEvent' + + public handle: ProgressHandle + + public constructor(handle: ProgressHandle) { + super(ProgressEvent.EVENT_KEY) + + this.handle = handle + } +} + +export default ProgressNotifications \ No newline at end of file From c640873474b37edf0b8da1cc830f881ad3b60e54 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Mon, 15 Jul 2024 01:48:38 -0600 Subject: [PATCH 28/65] Progress notifications integrated with spawning. Issue with unit tests however --- fission/src/mirabuf/MirabufSceneObject.ts | 6 ++-- .../ui/components/ProgressNotification.tsx | 11 ++++-- .../ui/panels/mirabuf/ImportMirabufPanel.tsx | 35 +++++++++++++++---- fission/vite.config.ts | 1 + 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 2ac5ad4595..a480b01cc5 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -234,14 +234,14 @@ class MirabufSceneObject extends SceneObject { } } -export async function CreateMirabuf(assembly: mirabuf.Assembly): Promise { - const parser = new MirabufParser(assembly) +export async function CreateMirabuf(assembly: mirabuf.Assembly, progressHandle?: ProgressHandle): Promise { + const parser = new MirabufParser(assembly, progressHandle) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { console.error(`Assembly Parser produced significant errors for '${assembly.info!.name!}'`) return } - return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!) + return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!, progressHandle) } export default MirabufSceneObject diff --git a/fission/src/ui/components/ProgressNotification.tsx b/fission/src/ui/components/ProgressNotification.tsx index 097a759039..514fa100c5 100644 --- a/fission/src/ui/components/ProgressNotification.tsx +++ b/fission/src/ui/components/ProgressNotification.tsx @@ -62,8 +62,7 @@ function ProgressNotifications() { const onHandleUpdate = (e: Event) => { const handle = (e as ProgressEvent).handle; if (handle.status > 0) { - setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 1000) - console.debug('Deleting') + setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 2000) } handleMap.set(handle.handleId, handle) updateProgressElements() @@ -126,6 +125,14 @@ export class ProgressHandle { this.Push() } + public Fail(message?: string) { + this.Update(message ?? "Failed", 1, ProgressHandleStatus.Error) + } + + public Done(message?: string) { + this.Update(message ?? "Done", 1, ProgressHandleStatus.Done) + } + public Push() { window.dispatchEvent(new ProgressEvent(this)) } diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index e4498c7e97..075b841239 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -22,6 +22,7 @@ import Panel, { PanelPropsImpl } from "@/ui/components/Panel" import { usePanelControlContext } from "@/ui/PanelContext" import TaskStatus from "@/util/TaskStatus" import { BiRefresh } from "react-icons/bi" +import { ProgressHandle, ProgressHandleStatus } from "@/ui/components/ProgressNotification" const DownloadIcon = const AddIcon = @@ -124,20 +125,28 @@ function GetCacheInfo(miraType: MiraType): MirabufCacheInfo[] { return Object.values(MirabufCachingService.GetCacheMap(miraType)) } -function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType) { +function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle?: ProgressHandle) { + if (!progressHandle) { + progressHandle = new ProgressHandle(info.name ?? info.cacheKey) + } + MirabufCachingService.Get(info.id, type).then(assembly => { if (assembly) { CreateMirabuf(assembly).then(x => { if (x) { World.SceneRenderer.RegisterSceneObject(x) + progressHandle.Done() + } else { + progressHandle.Fail() } }) if (!info.name) MirabufCachingService.CacheInfo(info.cacheKey, type, assembly.info?.name ?? undefined) } else { + progressHandle.Fail() console.error("Failed to spawn robot") } - }) + }).catch(() => progressHandle.Fail()) } const ImportMirabufPanel: React.FC = ({ panelId }) => { @@ -233,9 +242,16 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { // Cache a selected remote mirabuf assembly, load from cache. const selectRemote = useCallback( (info: MirabufRemoteInfo, type: MiraType) => { + const status = new ProgressHandle(info.displayName) + status.Update("Downloading from Synthesis...", 0.05) + MirabufCachingService.CacheRemote(info.src, type).then(cacheInfo => { - cacheInfo && SpawnCachedMira(cacheInfo, type) - }) + if (cacheInfo) { + SpawnCachedMira(cacheInfo, type, status) + } else { + status.Fail("Failed to cache") + } + }).catch(() => status.Fail()) closePanel(panelId) }, @@ -244,9 +260,16 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { const selectAPS = useCallback( (data: Data, type: MiraType) => { + const status = new ProgressHandle(data.attributes.displayName ?? data.id) + status.Update("Downloading from APS...", 0.05) + MirabufCachingService.CacheAPS(data, type).then(cacheInfo => { - cacheInfo && SpawnCachedMira(cacheInfo, type) - }) + if (cacheInfo) { + SpawnCachedMira(cacheInfo, type, status) + } else { + status.Fail("Failed to cache") + } + }).catch(() => status.Fail()) closePanel(panelId) }, diff --git a/fission/vite.config.ts b/fission/vite.config.ts index 1f052ab08a..da41628d3d 100644 --- a/fission/vite.config.ts +++ b/fission/vite.config.ts @@ -33,6 +33,7 @@ export default defineConfig({ ], }, test: { + testTimeout: 5000, globals: true, environment: 'jsdom', browser: { From 4660906f651c049af1bc1241fe7ebf238f44102f Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Mon, 15 Jul 2024 02:02:14 -0600 Subject: [PATCH 29/65] I didn't change anything and now the issue with vitest taking too long is gone... --- fission/src/test/PhysicsSystem.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index f60447705c..ed2ea62bc5 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -91,7 +91,9 @@ describe("Mirabuf Physics Loading", () => { const assembly = await MirabufCachingService.CacheRemote( "/api/mira/Robots/Team 2471 (2018)_v7.mira", MiraType.ROBOT - ).then(x => MirabufCachingService.Get(x!.id, MiraType.ROBOT)) + ).then(x => { + return MirabufCachingService.Get(x!.id, MiraType.ROBOT) + }) const parser = new MirabufParser(assembly!) const physSystem = new PhysicsSystem() From 19666ccc74e2c336ed83c526bf9a3ca7467667f0 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 09:43:59 -0700 Subject: [PATCH 30/65] add import to general imports --- exporter/SynthesisFusionAddin/proto/deps.py | 2 +- exporter/SynthesisFusionAddin/src/APS/APS.py | 47 +++++++++---------- .../src/general_imports.py | 4 +- exporter/SynthesisFusionAddin/src/strings.py | 2 - 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 80887337df..88034c300a 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -5,7 +5,7 @@ import adsk.core import adsk.fusion -from ..src.general_imports import INTERNAL_ID +from ..src.strings import INTERNAL_ID system = platform.system() diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 6c853a7897..423b355630 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -7,24 +7,22 @@ import urllib.parse import urllib.request from dataclasses import dataclass -from typing import Any -from result import Ok, Err, Result, is_err -import requests from ..general_imports import ( APP_NAME, DESCRIPTION, INTERNAL_ID, - APS_AUTH, - APS_USER_INFO, gm, my_addin_path, - root_logger, ) CLIENT_ID = "GCxaewcLjsYlK8ud7Ka9AKf9dPwMR3e4GlybyfhAK2zvl3tU" auth_path = os.path.abspath(os.path.join(my_addin_path, "..", ".aps_auth")) +APS_AUTH = None +APS_USER_INFO = None + + @dataclass class APSAuth: access_token: str @@ -66,26 +64,25 @@ def getCodeChallenge() -> str | None: return data["challenge"] -def getAuth() -> APSAuth | None: +def getAuth() -> APSAuth: + global APS_AUTH if APS_AUTH is not None: return APS_AUTH try: + curr_time = time.time() with open(auth_path, "rb") as f: p = pickle.load(f) - logging.getLogger(f"{INTERNAL_ID}").info(f"Auth Path: {auth_path}\n{json.dumps(p)}") APS_AUTH = APSAuth( access_token=p["access_token"], refresh_token=p["refresh_token"], expires_in=p["expires_in"], - expires_at=p["expires_at"], + expires_at=int(curr_time + p["expires_in"] * 1000), token_type=p["token_type"], ) except: - gm.ui.messageBox("Sign in","Sign in") - return None + gm.ui.messageBox("Please Sign In", "Please Sign In") curr_time = int(time.time() * 1000) if curr_time >= APS_AUTH.expires_at: - logging.getLogger(f"{INTERNAL_ID}").info(f"Refreshing {curr_time}\n{json.dumps(APS_AUTH.__dict__)}") refreshAuthToken() if APS_USER_INFO is None: loadUserInfo() @@ -96,25 +93,24 @@ def convertAuthToken(code: str): global APS_AUTH authUrl = f'http://localhost:80/api/aps/code/?code={code}&redirect_uri={urllib.parse.quote_plus("http://localhost:80/api/aps/exporter/")}' res = urllib.request.urlopen(authUrl) - data = _res_json(res)["response"] - curr_time = time.time() * 1000 + curr_time = time.time() APS_AUTH = APSAuth( access_token=data["access_token"], refresh_token=data["refresh_token"], expires_in=data["expires_in"], - expires_at=int(curr_time + (data["expires_in"] * 1000)), + expires_at=int(curr_time + data["expires_in"] * 1000), token_type=data["token_type"], ) with open(auth_path, "wb") as f: - logging.getLogger(f"{INTERNAL_ID}").info(f"APS AUTH: {json.dumps(APS_AUTH.__dict__)}") - pickle.dump(APS_AUTH.__dict__, f) + pickle.dump(data, f) f.close() loadUserInfo() def removeAuth(): + global APS_AUTH, APS_USER_INFO APS_AUTH = None APS_USER_INFO = None pathlib.Path.unlink(pathlib.Path(auth_path)) @@ -129,7 +125,7 @@ def refreshAuthToken(): "client_id": CLIENT_ID, "grant_type": "refresh_token", "refresh_token": APS_AUTH.refresh_token, - "scope": "data:create", + "scope": "data:read", } ).encode("utf-8") req = urllib.request.Request("https://developer.api.autodesk.com/authentication/v2/token", data=body) @@ -138,12 +134,12 @@ def refreshAuthToken(): try: res = urllib.request.urlopen(req) data = _res_json(res) - curr_time = time.time() * 1000 + curr_time = time.time() APS_AUTH = APSAuth( access_token=data["access_token"], refresh_token=data["refresh_token"], expires_in=data["expires_in"], - expires_at=int(curr_time + (data["expires_in"] * 1000)), + expires_at=int(curr_time + data["expires_in"] * 1000), token_type=data["token_type"], ) except urllib.request.HTTPError as e: @@ -153,6 +149,7 @@ def refreshAuthToken(): def loadUserInfo() -> APSUserInfo | None: + global APS_AUTH if not APS_AUTH: return None global APS_USER_INFO @@ -271,10 +268,10 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ # data:create + global APS_AUTH if not APS_AUTH: gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") - auth_write = APS_AUTH - auth_read = APS_AUTH + auth = APS_AUTH # Get token from APS API later file_name = file_path_to_file_name(file_path) @@ -296,7 +293,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ Create APS Storage Location """ - object_id = create_storage_location(auth_write, project_id, folder_id, file_name) + object_id = create_storage_location(auth, project_id, folder_id, file_name) if object_id.is_err(): return Err(None) object_id = object_id.ok() @@ -309,7 +306,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st """ Create Signed URL For APS Upload """ - generate_signed_url_result = generate_signed_url(auth_read, bucket_key, object_key) + generate_signed_url_result = generate_signed_url(auth, bucket_key, object_key) if generate_signed_url_result.is_err(): return Err(None) @@ -323,7 +320,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st if complete_upload(auth_write, upload_key, bucket_key).is_err(): return Err(None) file_name = file_path_to_file_name(file_path) - (_lineage_id, _lineage_href) = create_first_file_version(auth_write, str(object_id), project_id, str(folder_id), file_name) + (_lineage_id, _lineage_href) = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index 87a6874c53..e522b17fda 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -9,11 +9,9 @@ from time import time from types import FunctionType -from requests import get, post -from result import Ok, Err, is_err - import adsk.core import adsk.fusion +from .strings import INTERNAL_ID # hard coded to bypass errors for now PROTOBUF = True diff --git a/exporter/SynthesisFusionAddin/src/strings.py b/exporter/SynthesisFusionAddin/src/strings.py index 01f85eac40..6e3aa109e2 100644 --- a/exporter/SynthesisFusionAddin/src/strings.py +++ b/exporter/SynthesisFusionAddin/src/strings.py @@ -2,5 +2,3 @@ APP_TITLE = "Synthesis Robot Exporter" DESCRIPTION = "Exports files from Fusion into the Synthesis Format" INTERNAL_ID = "synthesis" -APS_AUTH = None -APS_USER_INFO = None From abf0bb22e0247fcbe6429e737a1b1570e7d37840 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 10:07:27 -0700 Subject: [PATCH 31/65] removed .. from deps import path --- exporter/SynthesisFusionAddin/proto/deps.py | 2 +- exporter/SynthesisFusionAddin/src/general_imports.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 88034c300a..7a742944cf 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -5,7 +5,7 @@ import adsk.core import adsk.fusion -from ..src.strings import INTERNAL_ID +from src.strings import INTERNAL_ID system = platform.system() diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index e522b17fda..b30041ee17 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -11,7 +11,6 @@ import adsk.core import adsk.fusion -from .strings import INTERNAL_ID # hard coded to bypass errors for now PROTOBUF = True @@ -42,7 +41,6 @@ except: logging.getLogger(f"{INTERNAL_ID}.import_manager").error("Failed\n{}".format(traceback.format_exc())) - try: # simple analytics endpoint # A_EP = AnalyticsEndpoint("UA-188467590-1", 1) From bf82eee0baef123a7ef078814523cc785eb38a4c Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 11:32:18 -0700 Subject: [PATCH 32/65] switch to create token --- exporter/SynthesisFusionAddin/proto/deps.py | 2 +- exporter/SynthesisFusionAddin/src/APS/APS.py | 121 ++++++++---------- .../src/UI/ShowAPSAuthCommand.py | 2 +- 3 files changed, 53 insertions(+), 72 deletions(-) diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 7a742944cf..d6ed949db4 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -179,7 +179,7 @@ def installDependencies(): from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 from requests import get, post - from result import Ok, Err, is_err + from result import Result, Ok, Err, is_err except ImportError or ModuleNotFoundError: installCross(["protobuf==4.23.3", "requests==2.32.3", "result==0.17.0"]) from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 423b355630..ec5b656237 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -7,10 +7,10 @@ import urllib.parse import urllib.request from dataclasses import dataclass +from typing import Any +import requests from ..general_imports import ( - APP_NAME, - DESCRIPTION, INTERNAL_ID, gm, my_addin_path, @@ -185,7 +185,7 @@ def getUserInfo() -> APSUserInfo | None: return APS_USER_INFO return loadUserInfo() -def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_display_name: str) -> Result[str, None]: +def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_display_name: str) -> str | None: """ creates a folder on an APS project @@ -230,16 +230,16 @@ def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_disp res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders", headers=headers, data=data) if not res.ok: gm.ui.messageBox("", f"Failed to create folder: {res.text}") - return Err(None) + return None json: dict[str, Any] = res.json() href: str = json["links"]["self"]["href"] - return Ok(href) + return href def file_path_to_file_name(file_path: str) -> str: return file_path.split("/").pop() -def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[str | None, None]: +def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | None: """ uploads mirabuf file to a specific folder in an APS project the folder and project must be created and valid @@ -269,37 +269,19 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st # data:create global APS_AUTH - if not APS_AUTH: + if APS_AUTH is None: gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") - auth = APS_AUTH + auth = APS_AUTH.access_token # Get token from APS API later file_name = file_path_to_file_name(file_path) - #""" - #Assign APS File ID - #""" - #file_id_result = get_file_id(auth_read, project_id, folder_id, file_path) - #if file_id_result.is_err(): - # return Err(None) - #file_id: str | None = file_id_result.ok(); - #if not file_id == None: - # gm.ui.messageBox("UPLOAD ERROR","Mirabuf file already exists!") - # update_file_result: Result[str, None] = update_file_version(auth_write, project_id, folder_id, str(file_id), file_name, "1") # Grab real file version number - # if update_file_result.is_err(): - # return Err(None) - # new_version_id = str(update_file_result.ok()) - # return Ok(new_version_id) - """ Create APS Storage Location """ object_id = create_storage_location(auth, project_id, folder_id, file_name) - if object_id.is_err(): - return Err(None) - object_id = object_id.ok() - if object_id == None: + if object_id is None: gm.ui.messageBox("UPLOAD ERROR", "Object id is none; check create storage location") - return Err(None) + return None (prefix, object_key) = str(object_id).split("/", 1) bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod @@ -307,25 +289,25 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> Result[st Create Signed URL For APS Upload """ generate_signed_url_result = generate_signed_url(auth, bucket_key, object_key) - if generate_signed_url_result.is_err(): - return Err(None) + if generate_signed_url_result is None: + return None - (upload_key, signed_url) = generate_signed_url_result.ok() - if upload_file(signed_url, file_path).is_err(): - return Err(None) + (upload_key, signed_url) = generate_signed_url_result + if upload_file(signed_url, file_path) is None: + return None """ Finish Upload and Initialize First File Version """ - if complete_upload(auth_write, upload_key, bucket_key).is_err(): - return Err(None) + if complete_upload(auth, upload_key, bucket_key) is None: + return None file_name = file_path_to_file_name(file_path) (_lineage_id, _lineage_href) = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) - return Ok(None) + return None -def get_hub_id(auth: str, hub_name: str) -> Result[str | None, str]: +def get_hub_id(auth: str, hub_name: str) -> str | None: """ gets a user's hub based on a hub name @@ -344,15 +326,15 @@ def get_hub_id(auth: str, hub_name: str) -> Result[str | None, str]: hub_list_res = requests.get("https://developer.api.autodesk.com/project/v1/hubs", headers=headers) if not hub_list_res.ok: gm.ui.messageBox("UPLOAD ERROR", f"Failed to retrieve hubs: {hub_list_res.text}") - return Err(f"Failed to retrieve hubs: {hub_list_res.text}") + return None hub_list: list[dict[str, Any]] = hub_list_res.json() for hub in hub_list: if hub["attributes"]["name"] == hub_name: id: str = hub["id"] - return Ok(id) - return Ok(None) + return id + return "" -def get_project_id(auth: str, hub_id: str, project_name: str) -> Result[str | None, str]: +def get_project_id(auth: str, hub_id: str, project_name: str) -> str | None: """ gets a project in a hub with a project name @@ -375,16 +357,16 @@ def get_project_id(auth: str, hub_id: str, project_name: str) -> Result[str | No project_list_res = requests.get(f"https://developer.api.autodesk.com/project/v1/hubs/{hub_id}/projects", headers=headers) if not project_list_res.ok: gm.ui.messageBox("UPLOAD ERROR", f"Failed to retrieve hubs: {project_list_res.text}") - return Err(f"Failed to retrieve hubs: {project_list_res.text}") + return None project_list: list[dict[str, Any]] = project_list_res.json() for project in project_list: if project["attributes"]["name"] == project_name: id: str = project["id"] - return Ok(id) - return Ok(None) + return id + return "" -def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str, file_name: str, curr_file_version: str) -> Result[str, None]: +def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str, file_name: str, curr_file_version: str) -> str| None: """ updates an existing file in an APS folder @@ -406,10 +388,9 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str """ - object_id_res = create_storage_location(auth, project_id, folder_id, file_name) - if object_id_res.is_err(): - return Err(None) - object_id = object_id_res.ok() + object_id = create_storage_location(auth, project_id, folder_id, file_name) + if object_id is None: + return None headers = { @@ -471,12 +452,12 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str update_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, data=data) if not update_res.ok: gm.ui.messageBox("UPLOAD ERROR", f"updating file to new version failed: {update_res.text}") - return Err(None) + return None gm.ui.messageBox("UPLOAD ERROR", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") new_id: str = update_res.json()["data"]["id"] - return Ok(new_id) + return new_id -def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> Result[str | None, str]: +def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> str | None: """ gets the file id given a file name @@ -504,17 +485,17 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> R file_list_res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders/{folder_id}/contents") if not file_list_res.ok: gm.ui.messageBox(f"UPLOAD ERROR: {file_list_res.text}", "Failed to get file list") - return Err("Failed to get file list") + return None file_list_json: list[dict[str, Any]] = file_list_res.json() for file in file_list_json: name: str = file["attributes"]["name"] if name == file_name: id: str = file["id"] gm.ui.messageBox("UPLOAD ERROR", f"Found file {name} with id: {id}") - return Ok(id) - return Ok(None) + return id + return "" -def create_storage_location(auth: str, project_id: str, folder_id: str, file_name: str) -> Result[str, str]: +def create_storage_location(auth: str, project_id: str, folder_id: str, file_name: str) -> str| None: """ creates a storage location (a bucket) the bucket can be used to upload a file to @@ -559,12 +540,12 @@ def create_storage_location(auth: str, project_id: str, folder_id: str, file_nam storage_location_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/storage", json=data, headers=headers) if not storage_location_res.ok: gm.ui.messageBox(f"UPLOAD ERROR: {storage_location_res.text}", f"Failed to create storage location") - return Err(f"Failed to create storage location: {storage_location_res.text}") + return None storage_location_json: dict[str, Any] = storage_location_res.json() object_id: str = storage_location_json["data"]["id"] - return Ok(object_id) + return object_id -def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> Result[tuple[str, str], str]: +def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> tuple[str, str] | None: """ generates a signed_url for a bucket, given a bucket_key and object_key @@ -588,11 +569,11 @@ def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> Result[t signed_url_res = requests.get(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{object_key}/signeds3upload", headers=headers) if not signed_url_res.ok: gm.ui.messageBox("UPLOAD ERROR","Failed to get signed url") - return Err(f"Failed to get signed url: {signed_url_res.text}") + return None signed_url_json: dict[str, str] = signed_url_res.json() - return Ok((signed_url_json["uploadKey"], signed_url_json["urls"][0])) + return (signed_url_json["uploadKey"], signed_url_json["urls"][0]) -def upload_file(signed_url: str, file_path: str) -> Result[None, str]: +def upload_file(signed_url: str, file_path: str) -> str |None: """ uploads a file to APS given a signed_url a path to the file on your machine @@ -613,10 +594,10 @@ def upload_file(signed_url: str, file_path: str) -> Result[None, str]: upload_response = requests.put(url=signed_url, data=data) if not upload_response.ok: gm.ui.messageBox("UPLOAD ERROR", f"Failed to upload to signed url: {upload_response.text}") - return Err(f"Failed to upload to signed url: {upload_response.text}") - return Ok(None) + return None + return "" -def complete_upload(auth: str, upload_key: str, bucket_key: str) -> Result[None, str]: +def complete_upload(auth: str, upload_key: str, bucket_key: str) -> str |None: """ completes and verifies the APS file upload given the upload_key @@ -641,10 +622,10 @@ def complete_upload(auth: str, upload_key: str, bucket_key: str) -> Result[None, completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{upload_key}/signeds3upload", json=data, headers=headers) if not completed_res.ok: gm.ui.messageBox(f"UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", "Failed to complete upload") - return Err(f"Failed to complete upload: {completed_res.text}") - return Ok(None) + return None + return "" -def create_first_file_version(auth: str, project_id: str, object_id: str, folder_id: str, file_name: str) -> Result[tuple[str, str], None]: +def create_first_file_version(auth: str, project_id: str, object_id: str, folder_id: str, file_name: str) -> tuple[str, str]| None: """ initializes versioning for a file @@ -728,10 +709,10 @@ def create_first_file_version(auth: str, project_id: str, object_id: str, folder first_version_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}L/items", data=data, headers=headers) if not first_version_res.ok: gm.ui.messageBox("UPLOAD ERROR", f"Failed to create first file version: {first_version_res.text}") - return Err(None) + return None first_version_json: dict[str, Any] = first_version_res.json() lineage_id: str = first_version_json["data"]["id"] href: str = first_version_json["links"]["self"]["href"] - return Ok((lineage_id, href)) + return (lineage_id, href) diff --git a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py index b99e71fe2c..152eed278e 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py @@ -42,7 +42,7 @@ def notify(self, args): "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": urllib.parse.quote_plus(callbackUrl), - "scope": "data:read", + "scope": "data:create", "nonce": time.time(), "prompt": "login", "code_challenge": challenge, From 86cc121cbd9cbfadab896ff9114ca118a88021e7 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 11:43:15 -0700 Subject: [PATCH 33/65] fix arg name val --- exporter/SynthesisFusionAddin/src/APS/APS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index ec5b656237..b86126abe2 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -125,7 +125,7 @@ def refreshAuthToken(): "client_id": CLIENT_ID, "grant_type": "refresh_token", "refresh_token": APS_AUTH.refresh_token, - "scope": "data:read", + "scope": "data:create", } ).encode("utf-8") req = urllib.request.Request("https://developer.api.autodesk.com/authentication/v2/token", data=body) From b74f704b7236f72bdf996479baa4299f593c97f4 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 12:40:34 -0700 Subject: [PATCH 34/65] fixed pickling bug --- exporter/SynthesisFusionAddin/src/APS/APS.py | 30 +++++++++---------- .../src/Parser/SynthesisParser/Parser.py | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index b86126abe2..147405f2df 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -64,28 +64,24 @@ def getCodeChallenge() -> str | None: return data["challenge"] -def getAuth() -> APSAuth: +def getAuth() -> APSAuth | None: global APS_AUTH if APS_AUTH is not None: return APS_AUTH try: curr_time = time.time() with open(auth_path, "rb") as f: - p = pickle.load(f) - APS_AUTH = APSAuth( - access_token=p["access_token"], - refresh_token=p["refresh_token"], - expires_in=p["expires_in"], - expires_at=int(curr_time + p["expires_in"] * 1000), - token_type=p["token_type"], - ) - except: - gm.ui.messageBox("Please Sign In", "Please Sign In") + p: APSAuth = pickle.load(f) + logging.getLogger(f"{INTERNAL_ID}").info(msg=f"{json.dumps(p.__dict__)}") + APS_AUTH = p + except Exception as arg: + gm.ui.messageBox(f"ERROR:\n{arg}", "Please Sign In") + return None curr_time = int(time.time() * 1000) if curr_time >= APS_AUTH.expires_at: refreshAuthToken() if APS_USER_INFO is None: - loadUserInfo() + _ = loadUserInfo() return APS_AUTH @@ -103,10 +99,10 @@ def convertAuthToken(code: str): token_type=data["token_type"], ) with open(auth_path, "wb") as f: - pickle.dump(data, f) + pickle.dump(APS_AUTH, f) f.close() - loadUserInfo() + _ = loadUserInfo() def removeAuth(): @@ -142,12 +138,14 @@ def refreshAuthToken(): expires_at=int(curr_time + data["expires_in"] * 1000), token_type=data["token_type"], ) + with open(auth_path, "wb") as f: + pickle.dump(APS_AUTH, f) + f.close() except urllib.request.HTTPError as e: removeAuth() logging.getLogger(f"{INTERNAL_ID}").error(f"Refresh Error:\n{e.code} - {e.reason}") gm.ui.messageBox("Please sign in again.") - def loadUserInfo() -> APSUserInfo | None: global APS_AUTH if not APS_AUTH: @@ -304,7 +302,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non file_name = file_path_to_file_name(file_path) (_lineage_id, _lineage_href) = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) - return None + return "" def get_hub_id(auth: str, hub_name: str) -> str | None: diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index d51d1493a4..bab3d94117 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -191,7 +191,7 @@ def export(self) -> bool: folder_id = project.rootFolder.id file_location = self.exporterOptions.fileLocation gm.ui.messageBox(f"project: {project_id}\nfolder: {folder_id}\nfile: {file_location}", "ARGS:") - if upload_mirabuf(project_id, folder_id, file_location).is_err(): + if upload_mirabuf(project_id, folder_id, file_location) is None: gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later _ = progressDialog.hide() From b480efa5aab4699088219d478e3ca5ecc77c01ca Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 14:02:57 -0700 Subject: [PATCH 35/65] add write scope to token --- exporter/SynthesisFusionAddin/Synthesis.py | 2 ++ exporter/SynthesisFusionAddin/proto/deps.py | 5 ++--- exporter/SynthesisFusionAddin/src/APS/APS.py | 2 +- .../src/Parser/SynthesisParser/Parser.py | 7 +++++-- exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 37403d9f82..a817f9c4a6 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -4,6 +4,7 @@ from shutil import rmtree import adsk.core +from proto.deps import installDependencies from .src.APS import APS from .src.configure import setAnalytics, unload_config @@ -29,6 +30,7 @@ def run(_): """ try: + installDependencies() # Remove all items prior to start just to make sure unregister_all() diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index d6ed949db4..041cf52588 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -178,8 +178,7 @@ def installDependencies(): from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 - from requests import get, post - from result import Result, Ok, Err, is_err + from requests import * except ImportError or ModuleNotFoundError: - installCross(["protobuf==4.23.3", "requests==2.32.3", "result==0.17.0"]) + installCross(["protobuf==4.23.3", "result==0.17.0"]) from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 147405f2df..0f51be1bbf 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -121,7 +121,7 @@ def refreshAuthToken(): "client_id": CLIENT_ID, "grant_type": "refresh_token", "refresh_token": APS_AUTH.refresh_token, - "scope": "data:create", + "scope": "data:create data:write", } ).encode("utf-8") req = urllib.request.Request("https://developer.api.autodesk.com/authentication/v2/token", data=body) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index bab3d94117..a1b797f700 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -183,14 +183,17 @@ def export(self) -> bool: # Upload Mirabuf File to APS if self.exporterOptions.exportLocation == ExportLocation.UPLOAD: self.logger.debug("Uploading file to APS") - project = app.data.dataProjects.item(0) + projects = app.data.dataProjects + project = app.data.activeProject if not project.isValid: gm.ui.messageBox("Project is invalid", "") return False # add throw later + hub_name = app.data.activeHub.name project_id = project.id + project_name = project.name folder_id = project.rootFolder.id file_location = self.exporterOptions.fileLocation - gm.ui.messageBox(f"project: {project_id}\nfolder: {folder_id}\nfile: {file_location}", "ARGS:") + gm.ui.messageBox(f"hub: {hub_name}\nprojects: {projects}\nproject: name - {project_name}; id - {project_id}\nfolder: {folder_id}\nfile: {file_location}", "ARGS:") if upload_mirabuf(project_id, folder_id, file_location) is None: gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later diff --git a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py index 152eed278e..9318ea4c26 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py @@ -42,7 +42,7 @@ def notify(self, args): "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": urllib.parse.quote_plus(callbackUrl), - "scope": "data:create", + "scope": "data:create data:write", "nonce": time.time(), "prompt": "login", "code_challenge": challenge, From 0521a74bc2860871c3681dd1204ba1030ae094d6 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Mon, 15 Jul 2024 15:31:53 -0600 Subject: [PATCH 36/65] Fixed dependencies --- exporter/SynthesisFusionAddin/Synthesis.py | 8 ++++++-- exporter/SynthesisFusionAddin/src/general_imports.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index a817f9c4a6..313d373571 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -1,3 +1,5 @@ +# DO NOT CHANGE ORDER, OR ADD IMPORTS BEFORE UNTIL END COMMENT + import logging import os import traceback @@ -6,8 +8,6 @@ import adsk.core from proto.deps import installDependencies -from .src.APS import APS -from .src.configure import setAnalytics, unload_config from .src.general_imports import APP_NAME, DESCRIPTION, INTERNAL_ID, gm, root_logger from .src.Types.OString import OString from .src.UI import ( @@ -21,6 +21,10 @@ ) from .src.UI.Toolbar import Toolbar +# END OF RESTRICTION + +from .src.APS import APS +from .src.configure import setAnalytics, unload_config def run(_): """## Entry point to application from Fusion. diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py index b30041ee17..07360d6c22 100644 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ b/exporter/SynthesisFusionAddin/src/general_imports.py @@ -38,6 +38,7 @@ sys.path.insert(2, path_proto_files) from proto import deps + deps.installDependencies() except: logging.getLogger(f"{INTERNAL_ID}.import_manager").error("Failed\n{}".format(traceback.format_exc())) From afd6d1a5b60513c6ddb5d888032b1d92fa5c32d1 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Mon, 15 Jul 2024 15:38:15 -0600 Subject: [PATCH 37/65] Removing extra installDeps call --- exporter/SynthesisFusionAddin/Synthesis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 313d373571..70f3583053 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -6,7 +6,6 @@ from shutil import rmtree import adsk.core -from proto.deps import installDependencies from .src.general_imports import APP_NAME, DESCRIPTION, INTERNAL_ID, gm, root_logger from .src.Types.OString import OString From 40c56cc0b0219f418aa912a654d0e35d24462e22 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Mon, 15 Jul 2024 15:40:17 -0600 Subject: [PATCH 38/65] Removed yet another duplicate install dependencies --- exporter/SynthesisFusionAddin/Synthesis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 70f3583053..61c63ce2f7 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -33,7 +33,6 @@ def run(_): """ try: - installDependencies() # Remove all items prior to start just to make sure unregister_all() From d2825d8d597f60156a9d1ccb75a6105dd678987a Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 15:30:57 -0700 Subject: [PATCH 39/65] versioning json fixed --- exporter/SynthesisFusionAddin/src/APS/APS.py | 32 +++++++++++++------ .../src/Parser/SynthesisParser/Parser.py | 2 +- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 0f51be1bbf..14c2339d83 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -297,7 +297,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non """ Finish Upload and Initialize First File Version """ - if complete_upload(auth, upload_key, bucket_key) is None: + if complete_upload(auth, upload_key, object_key, bucket_key) is None: return None file_name = file_path_to_file_name(file_path) (_lineage_id, _lineage_href) = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) @@ -595,7 +595,7 @@ def upload_file(signed_url: str, file_path: str) -> str |None: return None return "" -def complete_upload(auth: str, upload_key: str, bucket_key: str) -> str |None: +def complete_upload(auth: str, upload_key: str, object_key: str, bucket_key: str) -> str |None: """ completes and verifies the APS file upload given the upload_key @@ -617,13 +617,14 @@ def complete_upload(auth: str, upload_key: str, bucket_key: str) -> str |None: "uploadKey": upload_key } - completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{upload_key}/signeds3upload", json=data, headers=headers) + gm.ui.messageBox(f"upload_key: {upload_key}\nobject_key: {object_key}\nbucket_key:{bucket_key}") + completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{object_key}/signeds3upload", json=data, headers=headers) if not completed_res.ok: gm.ui.messageBox(f"UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", "Failed to complete upload") return None return "" -def create_first_file_version(auth: str, project_id: str, object_id: str, folder_id: str, file_name: str) -> tuple[str, str]| None: +def create_first_file_version(auth: str, object_id: str, project_id: str, folder_id: str, file_name: str) -> tuple[str, str]| None: """ initializes versioning for a file @@ -647,21 +648,31 @@ def create_first_file_version(auth: str, project_id: str, object_id: str, folder - super complex request, probably not written correctly, likely a dev error """ + gm.ui.messageBox(f"file_name: {file_name}\nfolder_id: {folder_id}\nobject_id: {object_id}\nproject_id: {project_id}") + headers = { - "Authorization:": f"Bearer {auth}", + "Authorization": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", "Accept": "application/vnd.api+json" } - attributes = { + included_attributes = { "name": file_name, "extension": { - "type": "items:autodesk.core:File", + "type": "versions:autodesk.core:File", "version": "1.0" } } + attributes = { + "displayName": file_name, + "extension": { + "type": "items:autodesk.core:File", + "version": "1.0", + } + } + relationships = { "tip": { "data": { @@ -681,7 +692,7 @@ def create_first_file_version(auth: str, project_id: str, object_id: str, folder { "type": "versions", "id": "1", - "attributes": attributes, + "attributes": included_attributes, "relationships": { "storage": { "data": { @@ -699,14 +710,15 @@ def create_first_file_version(auth: str, project_id: str, object_id: str, folder }, "data": { "type": "items", + "attributes": attributes, "relationships": relationships }, "included": included } - first_version_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}L/items", data=data, headers=headers) + first_version_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/items", json=data, headers=headers) if not first_version_res.ok: - gm.ui.messageBox("UPLOAD ERROR", f"Failed to create first file version: {first_version_res.text}") + gm.ui.messageBox(f"Failed to create first file version: {first_version_res.text}", "UPLOAD ERROR") return None first_version_json: dict[str, Any] = first_version_res.json() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index a1b797f700..bce693e76b 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -193,7 +193,7 @@ def export(self) -> bool: project_name = project.name folder_id = project.rootFolder.id file_location = self.exporterOptions.fileLocation - gm.ui.messageBox(f"hub: {hub_name}\nprojects: {projects}\nproject: name - {project_name}; id - {project_id}\nfolder: {folder_id}\nfile: {file_location}", "ARGS:") + gm.ui.messageBox(f"hub: {hub_name}\nproject: name - {project_name}; id - {project_id}\nfolder: {folder_id}\nfile: {file_location}", "ARGS:") if upload_mirabuf(project_id, folder_id, file_location) is None: gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later From 3fee77cdf6ce9c803284a892803ecaf694b1b172 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 15:36:29 -0700 Subject: [PATCH 40/65] removed annoying prints --- exporter/SynthesisFusionAddin/src/APS/APS.py | 5 +---- .../src/Parser/SynthesisParser/Parser.py | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 14c2339d83..c6cea7e003 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -451,7 +451,7 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str if not update_res.ok: gm.ui.messageBox("UPLOAD ERROR", f"updating file to new version failed: {update_res.text}") return None - gm.ui.messageBox("UPLOAD ERROR", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") + gm.ui.messageBox("UPLOAD SUCCESS", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") new_id: str = update_res.json()["data"]["id"] return new_id @@ -617,7 +617,6 @@ def complete_upload(auth: str, upload_key: str, object_key: str, bucket_key: str "uploadKey": upload_key } - gm.ui.messageBox(f"upload_key: {upload_key}\nobject_key: {object_key}\nbucket_key:{bucket_key}") completed_res = requests.post(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{object_key}/signeds3upload", json=data, headers=headers) if not completed_res.ok: gm.ui.messageBox(f"UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", "Failed to complete upload") @@ -648,8 +647,6 @@ def create_first_file_version(auth: str, object_id: str, project_id: str, folder - super complex request, probably not written correctly, likely a dev error """ - gm.ui.messageBox(f"file_name: {file_name}\nfolder_id: {folder_id}\nobject_id: {object_id}\nproject_id: {project_id}") - headers = { "Authorization": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index bce693e76b..ee0c384680 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -183,17 +183,13 @@ def export(self) -> bool: # Upload Mirabuf File to APS if self.exporterOptions.exportLocation == ExportLocation.UPLOAD: self.logger.debug("Uploading file to APS") - projects = app.data.dataProjects project = app.data.activeProject if not project.isValid: gm.ui.messageBox("Project is invalid", "") return False # add throw later - hub_name = app.data.activeHub.name project_id = project.id - project_name = project.name folder_id = project.rootFolder.id file_location = self.exporterOptions.fileLocation - gm.ui.messageBox(f"hub: {hub_name}\nproject: name - {project_name}; id - {project_id}\nfolder: {folder_id}\nfile: {file_location}", "ARGS:") if upload_mirabuf(project_id, folder_id, file_location) is None: gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later From c01dc72fb9a2af76ead080a8a26431256757a171 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 16:27:44 -0700 Subject: [PATCH 41/65] fixed update_file_version function --- exporter/SynthesisFusionAddin/src/APS/APS.py | 39 ++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index c6cea7e003..9114c64403 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -272,6 +272,10 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non auth = APS_AUTH.access_token # Get token from APS API later file_name = file_path_to_file_name(file_path) + file_id = get_file_id(auth, project_id, folder_id, file_name) + if file_id not None: + update_file_version() + """ Create APS Storage Location @@ -304,6 +308,19 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non return "" +def check_file_exists(auth: str, project_id: str, file_id: str) -> bool | None: + headers = { + "Authorization": f"Bearer {auth}" + } + res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/items/{file_id}", headers=headers) + + if res.status_code == 200: + return True + elif res.status_code == 404: + return False + else: + return None + def get_hub_id(auth: str, hub_name: str) -> str | None: """ @@ -392,7 +409,7 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str headers = { - "Authorization:": f"Bearer {auth}", + "Authorization": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", "Accept": "application/vnd.api+json" @@ -406,35 +423,19 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str } } - refs = { - "data": { - "type": "versions", - "id": "", #version URN - "meta": { - "refType": "xrefs", - "direction": "to", - "extension": { - "type": "xrefs:autodesk.core:Xref", - "version": "1.1.0" - } - } - } - } - relationships: dict[str, Any] = { "item": { "data": { "type": "items", - "id": ""#wtf some id + "id": file_id, } }, "storage": { "data": { "type": "objects", - "id": object_id + "id": object_id, } }, - "refs": refs } data = { From 28dc6899c18d4fed26f9e35b983061512b4d6bb1 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 15 Jul 2024 21:55:12 -0700 Subject: [PATCH 42/65] add linage id serialization --- exporter/SynthesisFusionAddin/src/APS/APS.py | 77 +++++++++++++------ .../src/UI/ShowAPSAuthCommand.py | 2 +- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 9114c64403..9f4c399d0f 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -121,7 +121,7 @@ def refreshAuthToken(): "client_id": CLIENT_ID, "grant_type": "refresh_token", "refresh_token": APS_AUTH.refresh_token, - "scope": "data:create data:write", + "scope": "data:create data:write data:search", } ).encode("utf-8") req = urllib.request.Request("https://developer.api.autodesk.com/authentication/v2/token", data=body) @@ -233,7 +233,6 @@ def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_disp href: str = json["links"]["self"]["href"] return href - def file_path_to_file_name(file_path: str) -> str: return file_path.split("/").pop() @@ -272,9 +271,15 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non auth = APS_AUTH.access_token # Get token from APS API later file_name = file_path_to_file_name(file_path) - file_id = get_file_id(auth, project_id, folder_id, file_name) - if file_id not None: - update_file_version() + (file_id, file_version) = get_file_id(auth, project_id, folder_id, file_name) + + loaded = None + with open(f"lineage_{file_id}", "rb") as f: + loaded = pickle.load(f) + + if loaded is not None and file_id is not None: + lineage_id = loaded[0] + _ = update_file_version(auth, project_id, folder_id, file_id, lineage_id, file_name, file_version) """ @@ -304,7 +309,10 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non if complete_upload(auth, upload_key, object_key, bucket_key) is None: return None file_name = file_path_to_file_name(file_path) - (_lineage_id, _lineage_href) = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) + lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) + + with open(f"lineage_{file_id}", "wb") as f: + _ = pickle.dump(lineage_info, f) return "" @@ -381,7 +389,7 @@ def get_project_id(auth: str, hub_id: str, project_name: str) -> str | None: return "" -def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str, file_name: str, curr_file_version: str) -> str| None: +def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str, lineage_id:str, file_name: str, curr_file_version: str) -> str| None: """ updates an existing file in an APS folder @@ -406,13 +414,15 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str object_id = create_storage_location(auth, project_id, folder_id, file_name) if object_id is None: return None + + (prefix, object_key) = str(object_id).split("/", 1) + bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod + signed_url = generate_signed_url(auth, object_key, bucket_key) headers = { "Authorization": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", - "Accept": "application/vnd.api+json" - } attributes = { @@ -423,6 +433,24 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str } } + refs: dict[str, Any] = { + "data": { + "types": "versions", + "id": lineage_id, + "meta": { + "refType": "xref", + "direction": "to", + "extension": { + "type": "xrefs:autodesk.core:Xref", + "version": "1.1.0", + "data": { + "nestedType": "overlay" + } + } + } + } + } + relationships: dict[str, Any] = { "item": { "data": { @@ -450,13 +478,13 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str } update_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, data=data) if not update_res.ok: - gm.ui.messageBox("UPLOAD ERROR", f"updating file to new version failed: {update_res.text}") + gm.ui.messageBox(f"UPLOAD ERROR:\n{update_res.text}", "Updating file to new version failed") return None gm.ui.messageBox("UPLOAD SUCCESS", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") new_id: str = update_res.json()["data"]["id"] return new_id -def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> str | None: +def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> tuple[str, str]| None: """ gets the file id given a file name @@ -467,7 +495,7 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> s file_name - the name of the file in APS ; ex. test.mira returns: - success - the id of the file, or none if the file doesn't exist + success - the id of the file and it's current version, or an empty tuple string if the file doesn't exist failure - none potential causes of failure: @@ -481,18 +509,21 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> s "Authorization": f"Bearer {auth}" } - file_list_res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders/{folder_id}/contents") - if not file_list_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR: {file_list_res.text}", "Failed to get file list") + params = { + "filter[attributes.name]": file_name + } + + file_res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders/{folder_id}/search", headers=headers, params=params) + if file_res.status_code is 404: + return ("", "") + elif not file_res.ok: + gm.ui.messageBox(f"UPLOAD ERROR: {file_res.text}", "Failed to get file") return None - file_list_json: list[dict[str, Any]] = file_list_res.json() - for file in file_list_json: - name: str = file["attributes"]["name"] - if name == file_name: - id: str = file["id"] - gm.ui.messageBox("UPLOAD ERROR", f"Found file {name} with id: {id}") - return id - return "" + file_json: list[dict[str, Any]] = file_res.json() + name: str = str(file_json["data"][0]["attributes"]["name"]) + id: str = str(file_json["data"][0]["attributes"]["versionNumber"]) + return (name, id) + def create_storage_location(auth: str, project_id: str, folder_id: str, file_name: str) -> str| None: """ diff --git a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py index 9318ea4c26..daab11fdd3 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py @@ -42,7 +42,7 @@ def notify(self, args): "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": urllib.parse.quote_plus(callbackUrl), - "scope": "data:create data:write", + "scope": "data:create data:write data:search", "nonce": time.time(), "prompt": "login", "code_challenge": challenge, From 6dc0490909e6ff5aadb617f74b35af26f9c31b9e Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 16 Jul 2024 10:32:13 -0700 Subject: [PATCH 43/65] very messy but working new versioning code --- exporter/SynthesisFusionAddin/src/APS/APS.py | 84 +++++++------------- 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 9f4c399d0f..7b3a430785 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -270,17 +270,13 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") auth = APS_AUTH.access_token # Get token from APS API later - file_name = file_path_to_file_name(file_path) - (file_id, file_version) = get_file_id(auth, project_id, folder_id, file_name) - loaded = None - with open(f"lineage_{file_id}", "rb") as f: - loaded = pickle.load(f) - - if loaded is not None and file_id is not None: - lineage_id = loaded[0] - _ = update_file_version(auth, project_id, folder_id, file_id, lineage_id, file_name, file_version) - + # Check if file is already on aps + file_name = file_path_to_file_name(file_path) + (lineage_id, file_id, file_version) = get_file_id(auth, project_id, folder_id, file_name) + if file_id is not "": + _ = update_file_version(auth, project_id, folder_id, lineage_id, file_id, file_path, file_version) + return "" """ Create APS Storage Location @@ -308,7 +304,6 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non """ if complete_upload(auth, upload_key, object_key, bucket_key) is None: return None - file_name = file_path_to_file_name(file_path) lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) with open(f"lineage_{file_id}", "wb") as f: @@ -316,20 +311,6 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non return "" -def check_file_exists(auth: str, project_id: str, file_id: str) -> bool | None: - headers = { - "Authorization": f"Bearer {auth}" - } - res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/items/{file_id}", headers=headers) - - if res.status_code == 200: - return True - elif res.status_code == 404: - return False - else: - return None - - def get_hub_id(auth: str, hub_name: str) -> str | None: """ gets a user's hub based on a hub name @@ -389,7 +370,7 @@ def get_project_id(auth: str, hub_id: str, project_name: str) -> str | None: return "" -def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str, lineage_id:str, file_name: str, curr_file_version: str) -> str| None: +def update_file_version(auth: str, project_id: str, folder_id: str, lineage_id: str, file_id: str, file_path: str, curr_file_version: str) -> str| None: """ updates an existing file in an APS folder @@ -410,6 +391,7 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str - version one of the file hasn't been created ; fix: create_first_file_version() """ + file_name = file_path_to_file_name(file_path) object_id = create_storage_location(auth, project_id, folder_id, file_name) if object_id is None: @@ -417,9 +399,16 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str (prefix, object_key) = str(object_id).split("/", 1) bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod - signed_url = generate_signed_url(auth, object_key, bucket_key) + (upload_key, signed_url) = generate_signed_url(auth, bucket_key, object_key) + + if upload_file(signed_url, file_path) is None: + return None + + if complete_upload(auth, upload_key, object_key, bucket_key) is None: + return None + gm.ui.messageBox(f"file_name:{file_name}\nfile_id:{file_id}\ncurr_file_version:{curr_file_version}\nobject_id:{object_id}", "REUPLOAD ARGS") headers = { "Authorization": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", @@ -429,25 +418,7 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str "name": file_name, "extension": { "type": "versions:autodesk.core:File", - "version": curr_file_version - } - } - - refs: dict[str, Any] = { - "data": { - "types": "versions", - "id": lineage_id, - "meta": { - "refType": "xref", - "direction": "to", - "extension": { - "type": "xrefs:autodesk.core:Xref", - "version": "1.1.0", - "data": { - "nestedType": "overlay" - } - } - } + "version": f"{curr_file_version}.0" } } @@ -455,7 +426,7 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str "item": { "data": { "type": "items", - "id": file_id, + "id": lineage_id, } }, "storage": { @@ -474,17 +445,17 @@ def update_file_version(auth: str, project_id: str, folder_id: str, file_id: str "type": "versions", "attributes": attributes, "relationships": relationships - } + }, } - update_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, data=data) + update_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, json=data) if not update_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR:\n{update_res.text}", "Updating file to new version failed") + gm.ui.messageBox(f"REUPLOAD ERROR:\n{update_res.text}", "Updating file to new version failed") return None - gm.ui.messageBox("UPLOAD SUCCESS", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") + gm.ui.messageBox("REUPLOAD SUCCESS", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") new_id: str = update_res.json()["data"]["id"] return new_id -def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> tuple[str, str]| None: +def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> tuple[str, str, str] | None: """ gets the file id given a file name @@ -520,9 +491,10 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> t gm.ui.messageBox(f"UPLOAD ERROR: {file_res.text}", "Failed to get file") return None file_json: list[dict[str, Any]] = file_res.json() - name: str = str(file_json["data"][0]["attributes"]["name"]) - id: str = str(file_json["data"][0]["attributes"]["versionNumber"]) - return (name, id) + id: str = str(file_json["data"][0]["id"]) + lineage: str = str(file_json["data"][0]["relationships"]["item"]["data"]["id"]) + version: str = str(file_json["data"][0]["attributes"]["versionNumber"]) + return (lineage, id, version) def create_storage_location(auth: str, project_id: str, folder_id: str, file_name: str) -> str| None: @@ -598,7 +570,7 @@ def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> tuple[st } signed_url_res = requests.get(f"https://developer.api.autodesk.com/oss/v2/buckets/{bucket_key}/objects/{object_key}/signeds3upload", headers=headers) if not signed_url_res.ok: - gm.ui.messageBox("UPLOAD ERROR","Failed to get signed url") + gm.ui.messageBox(f"UPLOAD ERROR: {signed_url_res.text}","Failed to get signed url") return None signed_url_json: dict[str, str] = signed_url_res.json() return (signed_url_json["uploadKey"], signed_url_json["urls"][0]) From e4ef292f4045270df98f0fee45ffd3e4be398109 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 16 Jul 2024 10:32:56 -0700 Subject: [PATCH 44/65] removed unneeded serialization --- exporter/SynthesisFusionAddin/src/APS/APS.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 7b3a430785..3219f7eda8 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -304,10 +304,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non """ if complete_upload(auth, upload_key, object_key, bucket_key) is None: return None - lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) - - with open(f"lineage_{file_id}", "wb") as f: - _ = pickle.dump(lineage_info, f) + _lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) return "" From 468544950ed637eea25352e384c02f2dc8eb4135 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 16 Jul 2024 11:24:07 -0700 Subject: [PATCH 45/65] allow independent upload (without download) --- exporter/SynthesisFusionAddin/src/APS/APS.py | 61 +++++++++---------- .../src/Parser/SynthesisParser/Parser.py | 37 +++++------ .../src/UI/ConfigCommand.py | 32 +++++----- 3 files changed, 65 insertions(+), 65 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 3219f7eda8..8304cdae1f 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -236,7 +236,7 @@ def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_disp def file_path_to_file_name(file_path: str) -> str: return file_path.split("/").pop() -def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | None: +def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_contents: str) -> str | None: """ uploads mirabuf file to a specific folder in an APS project the folder and project must be created and valid @@ -271,13 +271,8 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non auth = APS_AUTH.access_token # Get token from APS API later - # Check if file is already on aps - file_name = file_path_to_file_name(file_path) (lineage_id, file_id, file_version) = get_file_id(auth, project_id, folder_id, file_name) - if file_id is not "": - _ = update_file_version(auth, project_id, folder_id, lineage_id, file_id, file_path, file_version) - return "" - + """ Create APS Storage Location """ @@ -296,16 +291,18 @@ def upload_mirabuf(project_id: str, folder_id: str, file_path: str) -> str | Non return None (upload_key, signed_url) = generate_signed_url_result - if upload_file(signed_url, file_path) is None: + if upload_file(signed_url, file_contents) is None: return None """ - Finish Upload and Initialize First File Version + Finish Upload and Initialize File Version """ if complete_upload(auth, upload_key, object_key, bucket_key) is None: return None - _lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) - + if file_id is not "": + update_file_version(auth, project_id, folder_id, lineage_id, file_id, file_name, file_contents, file_version, object_id) + else: + _lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) return "" def get_hub_id(auth: str, hub_name: str) -> str | None: @@ -367,7 +364,7 @@ def get_project_id(auth: str, hub_id: str, project_name: str) -> str | None: return "" -def update_file_version(auth: str, project_id: str, folder_id: str, lineage_id: str, file_id: str, file_path: str, curr_file_version: str) -> str| None: +def update_file_version(auth: str, project_id: str, folder_id: str, lineage_id: str, file_id: str, file_name: str, file_contents: str, curr_file_version: str, object_id: str) -> str| None: """ updates an existing file in an APS folder @@ -388,24 +385,22 @@ def update_file_version(auth: str, project_id: str, folder_id: str, lineage_id: - version one of the file hasn't been created ; fix: create_first_file_version() """ - file_name = file_path_to_file_name(file_path) + #object_id = create_storage_location(auth, project_id, folder_id, file_name) + #if object_id is None: + # return None + # + #(prefix, object_key) = str(object_id).split("/", 1) + #bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod + #(upload_key, signed_url) = generate_signed_url(auth, bucket_key, object_key) + # + #if upload_file(signed_url, file_contents) is None: + # return None - object_id = create_storage_location(auth, project_id, folder_id, file_name) - if object_id is None: - return None - - (prefix, object_key) = str(object_id).split("/", 1) - bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod - (upload_key, signed_url) = generate_signed_url(auth, bucket_key, object_key) - - if upload_file(signed_url, file_path) is None: - return None - - if complete_upload(auth, upload_key, object_key, bucket_key) is None: - return None + #if complete_upload(auth, upload_key, object_key, bucket_key) is None: + # return None - gm.ui.messageBox(f"file_name:{file_name}\nfile_id:{file_id}\ncurr_file_version:{curr_file_version}\nobject_id:{object_id}", "REUPLOAD ARGS") + #gm.ui.messageBox(f"file_name:{file_name}\nlineage_id:{lineage_id}\nfile_id:{file_id}\ncurr_file_version:{curr_file_version}\nobject_id:{object_id}", "REUPLOAD ARGS") headers = { "Authorization": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", @@ -415,7 +410,7 @@ def update_file_version(auth: str, project_id: str, folder_id: str, lineage_id: "name": file_name, "extension": { "type": "versions:autodesk.core:File", - "version": f"{curr_file_version}.0" + "version": f"1.0" } } @@ -472,6 +467,7 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> t notes: - checking if a file exists is an intended use-case """ + gm.ui.messageBox(f"ARGS: {file_name}", "") headers: dict[str, str] = { "Authorization": f"Bearer {auth}" @@ -488,6 +484,8 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> t gm.ui.messageBox(f"UPLOAD ERROR: {file_res.text}", "Failed to get file") return None file_json: list[dict[str, Any]] = file_res.json() + if len(file_json["data"]) is 0: + return ("", "") id: str = str(file_json["data"][0]["id"]) lineage: str = str(file_json["data"][0]["relationships"]["item"]["data"]["id"]) version: str = str(file_json["data"][0]["attributes"]["versionNumber"]) @@ -572,7 +570,7 @@ def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> tuple[st signed_url_json: dict[str, str] = signed_url_res.json() return (signed_url_json["uploadKey"], signed_url_json["urls"][0]) -def upload_file(signed_url: str, file_path: str) -> str |None: +def upload_file(signed_url: str, file_contents: str) -> str |None: """ uploads a file to APS given a signed_url a path to the file on your machine @@ -587,10 +585,7 @@ def upload_file(signed_url: str, file_path: str) -> str |None: notes: - fails if the auth or the signed URL are invalid """ - - with open(file_path, 'rb') as f: - data = f.read() - upload_response = requests.put(url=signed_url, data=data) + upload_response = requests.put(url=signed_url, data=file_contents) if not upload_response.ok: gm.ui.messageBox("UPLOAD ERROR", f"Failed to upload to signed url: {upload_response.text}") return None diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index ee0c384680..15adc8f977 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -159,26 +159,13 @@ def export(self) -> bool: self.pdMessage.currentMessage = "Compressing File..." self.pdMessage.update() - # check if entire path exists and create if not since gzip doesn't do that. - path = pathlib.Path(self.exporterOptions.fileLocation).parent - path.mkdir(parents=True, exist_ok=True) - + ### Print out assembly as JSON # miraJson = MessageToJson(assembly_out) # miraJsonFile = open(f'', 'wb') # miraJsonFile.write(str.encode(miraJson)) # miraJsonFile.close() - - if self.exporterOptions.compressOutput: - self.logger.debug("Compressing file") - with gzip.open(self.exporterOptions.fileLocation, "wb", 9) as f: - self.pdMessage.currentMessage = "Saving File..." - self.pdMessage.update() - f.write(assembly_out.SerializeToString()) - else: - f = open(self.exporterOptions.fileLocation, "wb") - f.write(assembly_out.SerializeToString()) - f.close() + # Upload Mirabuf File to APS if self.exporterOptions.exportLocation == ExportLocation.UPLOAD: @@ -189,10 +176,26 @@ def export(self) -> bool: return False # add throw later project_id = project.id folder_id = project.rootFolder.id - file_location = self.exporterOptions.fileLocation - if upload_mirabuf(project_id, folder_id, file_location) is None: + file_name = f"{self.exporterOptions.fileLocation}.mira" + if upload_mirabuf(project_id, folder_id, file_name, assembly_out.SerializeToString()) is None: gm.ui.messageBox("FAILED TO UPLOAD FILE TO APS", "ERROR") # add throw later + # Download Mirabuf File + else: + # check if entire path exists and create if not since gzip doesn't do that. + path = pathlib.Path(self.exporterOptions.fileLocation).parent + path.mkdir(parents=True, exist_ok=True) + if self.exporterOptions.compressOutput: + self.logger.debug("Compressing file") + with gzip.open(self.exporterOptions.fileLocation, "wb", 9) as f: + self.pdMessage.currentMessage = "Saving File..." + self.pdMessage.update() + f.write(assembly_out.SerializeToString()) + else: + f = open(self.exporterOptions.fileLocation, "wb") + f.write(assembly_out.SerializeToString()) + f.close() + _ = progressDialog.hide() if DEBUG: diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index a303689773..af5830ffff 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -1001,7 +1001,6 @@ def notify(self, args): self.log.error("Could not execute configuration due to failure") return - processedFileName = gm.app.activeDocument.name.replace(" ", "_") dropdownExportMode = INPUTS_ROOT.itemById("mode") if dropdownExportMode.selectedItem.index == 0: @@ -1009,22 +1008,24 @@ def notify(self, args): elif dropdownExportMode.selectedItem.index == 1: isRobot = False - if isRobot: - savepath = FileDialogConfig.SaveFileDialog( - defaultPath=exporterOptions.fileLocation, - ext="Synthesis File (*.synth)", - ) - else: - savepath = FileDialogConfig.SaveFileDialog(defaultPath=exporterOptions.fileLocation) - - if savepath == False: - # save was canceled - return + if exporterOptions.exportLocation == ExportLocation.DOWNLOAD: + if isRobot: + savepath = FileDialogConfig.SaveFileDialog( + defaultPath=exporterOptions.fileLocation, + ext="Synthesis File (*.synth)", + ) + else: + savepath = FileDialogConfig.SaveFileDialog(defaultPath=exporterOptions.fileLocation) - updatedPath = pathlib.Path(savepath).parent - if updatedPath != self.current.filePath: - self.current.filePath = str(updatedPath) + if savepath == False: + # save was canceled + return + updatedPath = pathlib.Path(savepath).parent + if updatedPath != self.current.filePath: + self.current.filePath = str(updatedPath) + else: + savepath = processedFileName adsk.doEvents() # get active document design = gm.app.activeDocument.design @@ -1207,6 +1208,7 @@ def notify(self, args): exportAsPart=export_as_part_boolean, ) + _: bool = Parser(exporterOptions).export() exporterOptions.writeToDesign() except: From 573ed7b01482b20acda9ac9d9e5bcc40206a4a78 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 16 Jul 2024 13:02:55 -0700 Subject: [PATCH 46/65] fix dumb type errors --- exporter/SynthesisFusionAddin/src/APS/APS.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 8304cdae1f..7e17d1fd9f 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -478,14 +478,14 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> t } file_res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders/{folder_id}/search", headers=headers, params=params) - if file_res.status_code is 404: - return ("", "") + if file_res.status_code == 404: + return ("", "", "") elif not file_res.ok: gm.ui.messageBox(f"UPLOAD ERROR: {file_res.text}", "Failed to get file") return None file_json: list[dict[str, Any]] = file_res.json() - if len(file_json["data"]) is 0: - return ("", "") + if len(file_json["data"]) == 0: + return ("", "", "") id: str = str(file_json["data"][0]["id"]) lineage: str = str(file_json["data"][0]["relationships"]["item"]["data"]["id"]) version: str = str(file_json["data"][0]["attributes"]["versionNumber"]) From 45a0042c8c365aca9334ca561d46f9c6c9cf2c00 Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Tue, 16 Jul 2024 14:07:12 -0600 Subject: [PATCH 47/65] Minor fixes --- exporter/SynthesisFusionAddin/proto/deps.py | 3 ++- exporter/SynthesisFusionAddin/src/APS/APS.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/exporter/SynthesisFusionAddin/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py index 041cf52588..65f95416b2 100644 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ b/exporter/SynthesisFusionAddin/proto/deps.py @@ -178,7 +178,8 @@ def installDependencies(): from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 - from requests import * + from requests import get, post except ImportError or ModuleNotFoundError: installCross(["protobuf==4.23.3", "result==0.17.0"]) from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 + from requests import get, post diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 7e17d1fd9f..90ecc7154e 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -299,7 +299,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content """ if complete_upload(auth, upload_key, object_key, bucket_key) is None: return None - if file_id is not "": + if file_id != "": update_file_version(auth, project_id, folder_id, lineage_id, file_id, file_name, file_contents, file_version, object_id) else: _lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name) From 6bf45463359f35c9e195fe0acb31ebe16174632e Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Tue, 16 Jul 2024 14:10:00 -0600 Subject: [PATCH 48/65] Minor refactoring --- fission/src/Synthesis.tsx | 7 +- fission/src/mirabuf/MirabufInstance.ts | 2 +- fission/src/mirabuf/MirabufParser.ts | 2 +- fission/src/mirabuf/MirabufSceneObject.ts | 2 +- .../ui/components/ProgressNotification.tsx | 66 ++---------------- .../ui/components/ProgressNotificationData.ts | 68 +++++++++++++++++++ 6 files changed, 81 insertions(+), 66 deletions(-) create mode 100644 fission/src/ui/components/ProgressNotificationData.ts diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 0e498d7f97..ad93d59303 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -57,7 +57,8 @@ import APS from "./aps/APS.ts" import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel.tsx" import Skybox from "./ui/components/Skybox.tsx" import PokerPanel from "@/panels/PokerPanel.tsx" -import ProgressNotifications, { ProgressHandle, ProgressHandleStatus } from "./ui/components/ProgressNotification.tsx" +import ProgressNotifications from "./ui/components/ProgressNotification.tsx" +import { ProgressHandle } from "./ui/components/ProgressNotificationData.ts" const DEFAULT_MIRA_PATH = "/api/mira/Robots/Team 2471 (2018)_v7.mira" @@ -117,7 +118,7 @@ function Synthesis() { const parser = new MirabufParser(miraAssembly) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { console.error(`Assembly Parser produced significant errors for '${miraAssembly.info!.name!}'`) - setupProgress.Update("Failed to parse assembly", 1, ProgressHandleStatus.Error) + setupProgress.Fail("Failed to parse assembly") return } @@ -126,7 +127,7 @@ function Synthesis() { const mirabufSceneObject = new MirabufSceneObject(new MirabufInstance(parser), miraAssembly.info!.name!) World.SceneRenderer.RegisterSceneObject(mirabufSceneObject) - setupProgress.Update("Done", 1, ProgressHandleStatus.Done) + setupProgress.Done() })() } diff --git a/fission/src/mirabuf/MirabufInstance.ts b/fission/src/mirabuf/MirabufInstance.ts index c940969b9d..2d27797f4f 100644 --- a/fission/src/mirabuf/MirabufInstance.ts +++ b/fission/src/mirabuf/MirabufInstance.ts @@ -2,7 +2,7 @@ import * as THREE from "three" import { mirabuf } from "../proto/mirabuf" import MirabufParser, { ParseErrorSeverity } from "./MirabufParser.ts" import World from "@/systems/World.ts" -import { ProgressHandle } from "@/ui/components/ProgressNotification.tsx" +import { ProgressHandle } from "@/ui/components/ProgressNotificationData.ts" const WIREFRAME = false diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index 515dd9ecdc..7f6a3f3418 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -1,7 +1,7 @@ import * as THREE from "three" import { mirabuf } from "@/proto/mirabuf" import { MirabufTransform_ThreeMatrix4 } from "@/util/TypeConversions" -import { ProgressHandle } from "@/ui/components/ProgressNotification" +import { ProgressHandle } from "@/ui/components/ProgressNotificationData" export enum ParseErrorSeverity { Unimportable = 10, diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index a480b01cc5..e288de8de5 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -12,7 +12,7 @@ import Mechanism from "@/systems/physics/Mechanism" import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" import InputSystem from "@/systems/input/InputSystem" import TransformGizmos from "@/ui/components/TransformGizmos" -import { ProgressHandle } from "@/ui/components/ProgressNotification" +import { ProgressHandle } from "@/ui/components/ProgressNotificationData" const DEBUG_BODIES = false diff --git a/fission/src/ui/components/ProgressNotification.tsx b/fission/src/ui/components/ProgressNotification.tsx index 514fa100c5..b594ef24ac 100644 --- a/fission/src/ui/components/ProgressNotification.tsx +++ b/fission/src/ui/components/ProgressNotification.tsx @@ -1,6 +1,7 @@ import { styled, Typography } from "@mui/material" import { Box } from "@mui/system" import { useEffect, useReducer } from "react" +import { ProgressHandle, ProgressHandleStatus, ProgressEvent } from "./ProgressNotificationData" const handleMap = new Map() @@ -59,18 +60,18 @@ function ProgressNotifications() { }, undefined) useEffect(() => { - const onHandleUpdate = (e: Event) => { - const handle = (e as ProgressEvent).handle; + const onHandleUpdate = (e: ProgressEvent) => { + const handle = e.handle; if (handle.status > 0) { setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 2000) } handleMap.set(handle.handleId, handle) updateProgressElements() } - - window.addEventListener(ProgressEvent.EVENT_KEY, onHandleUpdate) + + ProgressEvent.AddListener(onHandleUpdate) return () => { - window.removeEventListener(ProgressEvent.EVENT_KEY, onHandleUpdate) + ProgressEvent.RemoveListener(onHandleUpdate) } }, [updateProgressElements]) @@ -93,61 +94,6 @@ function ProgressNotifications() { ) } -let nextHandleId = 0 - -export enum ProgressHandleStatus { - inProgress = 0, - Done = 1, - Error = 2 -} - -export class ProgressHandle { - - private _handleId: number - private _title: string - public message: string = "" - public progress: number = 0.0 - public status: ProgressHandleStatus = ProgressHandleStatus.inProgress - - public get handleId() { return this._handleId } - public get title() { return this._title } - - public constructor(title: string) { - this._handleId = nextHandleId++ - this._title = title - } - - public Update(message: string, progress: number, status?: ProgressHandleStatus) { - this.message = message - this.progress = progress - status && (this.status = status) - - this.Push() - } - - public Fail(message?: string) { - this.Update(message ?? "Failed", 1, ProgressHandleStatus.Error) - } - - public Done(message?: string) { - this.Update(message ?? "Done", 1, ProgressHandleStatus.Done) - } - - public Push() { - window.dispatchEvent(new ProgressEvent(this)) - } -} - -class ProgressEvent extends Event { - public static readonly EVENT_KEY = 'ProgressEvent' - - public handle: ProgressHandle - public constructor(handle: ProgressHandle) { - super(ProgressEvent.EVENT_KEY) - - this.handle = handle - } -} export default ProgressNotifications \ No newline at end of file diff --git a/fission/src/ui/components/ProgressNotificationData.ts b/fission/src/ui/components/ProgressNotificationData.ts new file mode 100644 index 0000000000..1174863844 --- /dev/null +++ b/fission/src/ui/components/ProgressNotificationData.ts @@ -0,0 +1,68 @@ +let nextHandleId = 0 + +export enum ProgressHandleStatus { + inProgress = 0, + Done = 1, + Error = 2 +} + +export class ProgressHandle { + + private _handleId: number + private _title: string + public message: string = "" + public progress: number = 0.0 + public status: ProgressHandleStatus = ProgressHandleStatus.inProgress + + public get handleId() { return this._handleId } + public get title() { return this._title } + + public constructor(title: string) { + this._handleId = nextHandleId++ + this._title = title + } + + public Update(message: string, progress: number, status?: ProgressHandleStatus) { + this.message = message + this.progress = progress + status && (this.status = status) + + this.Push() + } + + public Fail(message?: string) { + this.Update(message ?? "Failed", 1, ProgressHandleStatus.Error) + } + + public Done(message?: string) { + this.Update(message ?? "Done", 1, ProgressHandleStatus.Done) + } + + public Push() { + ProgressEvent.Dispatch(this) + } +} + +export class ProgressEvent extends Event { + public static readonly EVENT_KEY = 'ProgressEvent' + + public handle: ProgressHandle + + private constructor(handle: ProgressHandle) { + super(ProgressEvent.EVENT_KEY) + + this.handle = handle + } + + public static Dispatch(handle: ProgressHandle) { + window.dispatchEvent(new ProgressEvent(handle)) + } + + public static AddListener(func: (e: ProgressEvent) => void) { + window.addEventListener(this.EVENT_KEY, func as (e: Event) => void) + } + + public static RemoveListener(func: (e: ProgressEvent) => void) { + window.removeEventListener(this.EVENT_KEY, func as (e: Event) => void) + } +} \ No newline at end of file From cc531dcbc487b14b1261604e494efee52746c1e6 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 16 Jul 2024 13:50:44 -0700 Subject: [PATCH 49/65] added dedicated mirabuf folder to aps project current folder --- exporter/SynthesisFusionAddin/src/APS/APS.py | 32 +++++++++++++++---- .../src/UI/ShowAPSAuthCommand.py | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 7e17d1fd9f..0688b4b249 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -121,7 +121,7 @@ def refreshAuthToken(): "client_id": CLIENT_ID, "grant_type": "refresh_token", "refresh_token": APS_AUTH.refresh_token, - "scope": "data:create data:write data:search", + "scope": "data:create data:write data:search data:read", } ).encode("utf-8") req = urllib.request.Request("https://developer.api.autodesk.com/authentication/v2/token", data=body) @@ -225,13 +225,13 @@ def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_disp } } - res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders", headers=headers, data=data) + res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders", headers=headers, json=data) if not res.ok: - gm.ui.messageBox("", f"Failed to create folder: {res.text}") + gm.ui.messageBox(f"Failed to create new folder: {res.text}", "ERROR") return None json: dict[str, Any] = res.json() - href: str = json["links"]["self"]["href"] - return href + id: str = json["data"]["id"] + return id def file_path_to_file_name(file_path: str) -> str: return file_path.split("/").pop() @@ -270,7 +270,12 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") auth = APS_AUTH.access_token # Get token from APS API later - + + new_folder_id = get_item_id(auth, project_id, folder_id, "MirabufDir", "folders") + if new_folder_id is None: + folder_id = create_folder(auth, project_id, folder_id, "MirabufDir") + else: + folder_id = new_folder_id (lineage_id, file_id, file_version) = get_file_id(auth, project_id, folder_id, file_name) """ @@ -363,6 +368,21 @@ def get_project_id(auth: str, hub_id: str, project_name: str) -> str | None: return id return "" +def get_item_id(auth: str, project_id: str, parent_folder_id: str, folder_name: str, item_type: str) -> str | None: + headers = { + "Authorization": f"Bearer {auth}" + } + res = requests.get(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders/{parent_folder_id}/contents", headers=headers) + if not res.ok: + gm.ui.messageBox(f"Failed to get item: {res.text}") + return None + data: list[dict[str, Any]] = res.json()["data"] + if len(data) == 0: + return "" + for item in data: + if item["type"] == item_type and item["attributes"]["name"] == folder_name: + return item["id"] + return None def update_file_version(auth: str, project_id: str, folder_id: str, lineage_id: str, file_id: str, file_name: str, file_contents: str, curr_file_version: str, object_id: str) -> str| None: """ diff --git a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py index daab11fdd3..efa00e1e23 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py @@ -42,7 +42,7 @@ def notify(self, args): "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": urllib.parse.quote_plus(callbackUrl), - "scope": "data:create data:write data:search", + "scope": "data:create data:write data:search data:read", "nonce": time.time(), "prompt": "login", "code_challenge": challenge, From f1d99cae21b1b525a9c9821e3fb469466e94c259 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 16 Jul 2024 13:58:14 -0700 Subject: [PATCH 50/65] added success uploading message --- exporter/SynthesisFusionAddin/src/APS/APS.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index dcf0fe7c82..ed2613eb2e 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -461,9 +461,9 @@ def update_file_version(auth: str, project_id: str, folder_id: str, lineage_id: } update_res = requests.post(f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, json=data) if not update_res.ok: - gm.ui.messageBox(f"REUPLOAD ERROR:\n{update_res.text}", "Updating file to new version failed") + gm.ui.messageBox(f"UPLOAD ERROR:\n{update_res.text}", "Updating file to new version failed") return None - gm.ui.messageBox("REUPLOAD SUCCESS", f"File {file_name} successfully updated to version {int(curr_file_version) + 1}") + gm.ui.messageBox(f"Successfully updated file {file_name} to version {int(curr_file_version) + 1} on APS", "UPLOAD SUCCESS") new_id: str = update_res.json()["data"]["id"] return new_id @@ -487,7 +487,6 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> t notes: - checking if a file exists is an intended use-case """ - gm.ui.messageBox(f"ARGS: {file_name}", "") headers: dict[str, str] = { "Authorization": f"Bearer {auth}" @@ -738,4 +737,6 @@ def create_first_file_version(auth: str, object_id: str, project_id: str, folder lineage_id: str = first_version_json["data"]["id"] href: str = first_version_json["links"]["self"]["href"] + gm.ui.messageBox(f"Successful Upload of {file_name} to APS", "UPLOAD SUCCESS") + return (lineage_id, href) From d878388d11f04cc36413d33a1d6284858f701ace Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 16 Jul 2024 15:42:01 -0700 Subject: [PATCH 51/65] auth uses synthesis website now --- exporter/SynthesisFusionAddin/Synthesis.py | 2 +- exporter/SynthesisFusionAddin/src/APS/APS.py | 5 ++--- exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 61c63ce2f7..9cef7bc67c 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -140,7 +140,7 @@ def register_ui() -> None: work_panel, Helper.check_solid_open, ShowAPSAuthCommand.ShowAPSAuthCommandCreatedHandler, - description=f"APS TEST", + description=f"APS", command=True, ) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index ed2613eb2e..a639471067 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -58,7 +58,7 @@ def _res_json(res): def getCodeChallenge() -> str | None: - endpoint = "http://localhost:80/api/aps/challenge/" + endpoint = "https://synthesis.autodesk.com/api/aps/challenge/" res = urllib.request.urlopen(endpoint) data = _res_json(res) return data["challenge"] @@ -84,10 +84,9 @@ def getAuth() -> APSAuth | None: _ = loadUserInfo() return APS_AUTH - def convertAuthToken(code: str): global APS_AUTH - authUrl = f'http://localhost:80/api/aps/code/?code={code}&redirect_uri={urllib.parse.quote_plus("http://localhost:80/api/aps/exporter/")}' + authUrl = f'https://synthesis.autodesk.com/api/aps/code/?code={code}&redirect_uri={urllib.parse.quote_plus("https://synthesis.autodesk.com/api/aps/exporter/")}' res = urllib.request.urlopen(authUrl) data = _res_json(res)["response"] curr_time = time.time() diff --git a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py index efa00e1e23..c6dc93038e 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py @@ -31,7 +31,7 @@ def notify(self, args): global palette palette = gm.ui.palettes.itemById("authPalette") if not palette: - callbackUrl = "http://localhost:80/api/aps/exporter/" + callbackUrl = "https://synthesis.autodesk.com/api/aps/exporter/" challenge = getCodeChallenge() if challenge is None: logging.getLogger(f"{INTERNAL_ID}").error( From a30d7b30a5a3580e82e8b63c45454b355b8a9faf Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Tue, 16 Jul 2024 17:06:26 -0600 Subject: [PATCH 52/65] Formatted --- fission/src/mirabuf/MirabufInstance.ts | 4 +- fission/src/mirabuf/MirabufSceneObject.ts | 5 +- .../ui/components/ProgressNotification.tsx | 110 +++++++++--------- .../ui/components/ProgressNotificationData.ts | 15 ++- .../ui/panels/mirabuf/ImportMirabufPanel.tsx | 68 ++++++----- 5 files changed, 110 insertions(+), 92 deletions(-) diff --git a/fission/src/mirabuf/MirabufInstance.ts b/fission/src/mirabuf/MirabufInstance.ts index d07e660e2a..e8156644c4 100644 --- a/fission/src/mirabuf/MirabufInstance.ts +++ b/fission/src/mirabuf/MirabufInstance.ts @@ -118,10 +118,10 @@ class MirabufInstance { this._meshes = new Map() this._batches = new Array() - progressHandle?.Update('Loading materials...', 0.4) + progressHandle?.Update("Loading materials...", 0.4) this.LoadMaterials(materialStyle ?? MaterialStyle.Regular) - progressHandle?.Update('Creating meshes...', 0.5) + progressHandle?.Update("Creating meshes...", 0.5) this.CreateMeshes() } diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index e21ad83f3e..b962995a13 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -361,7 +361,10 @@ class MirabufSceneObject extends SceneObject { } } -export async function CreateMirabuf(assembly: mirabuf.Assembly, progressHandle?: ProgressHandle): Promise { +export async function CreateMirabuf( + assembly: mirabuf.Assembly, + progressHandle?: ProgressHandle +): Promise { const parser = new MirabufParser(assembly, progressHandle) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { console.error(`Assembly Parser produced significant errors for '${assembly.info!.name!}'`) diff --git a/fission/src/ui/components/ProgressNotification.tsx b/fission/src/ui/components/ProgressNotification.tsx index b594ef24ac..2e4d4c943b 100644 --- a/fission/src/ui/components/ProgressNotification.tsx +++ b/fission/src/ui/components/ProgressNotification.tsx @@ -5,70 +5,78 @@ import { ProgressHandle, ProgressHandleStatus, ProgressEvent } from "./ProgressN const handleMap = new Map() -const TypoStyled = styled(Typography)((_) => ({ +const TypoStyled = styled(Typography)(_ => ({ fontFamily: "Artifakt", - textAlign: "center" + textAlign: "center", })) function ProgressNotifications() { - const [progressElements, updateProgressElements] = useReducer(() => { return handleMap.size > 0 - ? - [...handleMap.entries()].map(([id, handle]) => { return ( - - - {handle.title} - { handle.message.length > 0 ? {handle.message} : <> } - - - - )}) + ? [...handleMap.entries()].map(([id, handle]) => { + return ( + + + + {handle.title} + + {handle.message.length > 0 ? ( + {handle.message} + ) : ( + <> + )} + + + + ) + }) : undefined }, undefined) useEffect(() => { const onHandleUpdate = (e: ProgressEvent) => { - const handle = e.handle; + const handle = e.handle if (handle.status > 0) { setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 2000) } handleMap.set(handle.handleId, handle) updateProgressElements() } - + ProgressEvent.AddListener(onHandleUpdate) return () => { ProgressEvent.RemoveListener(onHandleUpdate) @@ -86,14 +94,12 @@ function ProgressNotifications() { transform: "translate(-50%, 0)", maxWidth: "50vw", flexWrap: "wrap", - gap: "0.5rem" + gap: "0.5rem", }} > - { progressElements ?? <> } + {progressElements ?? <>} ) } - - -export default ProgressNotifications \ No newline at end of file +export default ProgressNotifications diff --git a/fission/src/ui/components/ProgressNotificationData.ts b/fission/src/ui/components/ProgressNotificationData.ts index 1174863844..cc759cb992 100644 --- a/fission/src/ui/components/ProgressNotificationData.ts +++ b/fission/src/ui/components/ProgressNotificationData.ts @@ -3,19 +3,22 @@ let nextHandleId = 0 export enum ProgressHandleStatus { inProgress = 0, Done = 1, - Error = 2 + Error = 2, } export class ProgressHandle { - private _handleId: number private _title: string public message: string = "" public progress: number = 0.0 public status: ProgressHandleStatus = ProgressHandleStatus.inProgress - public get handleId() { return this._handleId } - public get title() { return this._title } + public get handleId() { + return this._handleId + } + public get title() { + return this._title + } public constructor(title: string) { this._handleId = nextHandleId++ @@ -44,7 +47,7 @@ export class ProgressHandle { } export class ProgressEvent extends Event { - public static readonly EVENT_KEY = 'ProgressEvent' + public static readonly EVENT_KEY = "ProgressEvent" public handle: ProgressHandle @@ -65,4 +68,4 @@ export class ProgressEvent extends Event { public static RemoveListener(func: (e: ProgressEvent) => void) { window.removeEventListener(this.EVENT_KEY, func as (e: Event) => void) } -} \ No newline at end of file +} diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index 075b841239..faac3f954f 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -22,7 +22,7 @@ import Panel, { PanelPropsImpl } from "@/ui/components/Panel" import { usePanelControlContext } from "@/ui/PanelContext" import TaskStatus from "@/util/TaskStatus" import { BiRefresh } from "react-icons/bi" -import { ProgressHandle, ProgressHandleStatus } from "@/ui/components/ProgressNotification" +import { ProgressHandle } from "@/ui/components/ProgressNotificationData" const DownloadIcon = const AddIcon = @@ -130,23 +130,25 @@ function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle? progressHandle = new ProgressHandle(info.name ?? info.cacheKey) } - MirabufCachingService.Get(info.id, type).then(assembly => { - if (assembly) { - CreateMirabuf(assembly).then(x => { - if (x) { - World.SceneRenderer.RegisterSceneObject(x) - progressHandle.Done() - } else { - progressHandle.Fail() - } - }) + MirabufCachingService.Get(info.id, type) + .then(assembly => { + if (assembly) { + CreateMirabuf(assembly).then(x => { + if (x) { + World.SceneRenderer.RegisterSceneObject(x) + progressHandle.Done() + } else { + progressHandle.Fail() + } + }) - if (!info.name) MirabufCachingService.CacheInfo(info.cacheKey, type, assembly.info?.name ?? undefined) - } else { - progressHandle.Fail() - console.error("Failed to spawn robot") - } - }).catch(() => progressHandle.Fail()) + if (!info.name) MirabufCachingService.CacheInfo(info.cacheKey, type, assembly.info?.name ?? undefined) + } else { + progressHandle.Fail() + console.error("Failed to spawn robot") + } + }) + .catch(() => progressHandle.Fail()) } const ImportMirabufPanel: React.FC = ({ panelId }) => { @@ -245,13 +247,15 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { const status = new ProgressHandle(info.displayName) status.Update("Downloading from Synthesis...", 0.05) - MirabufCachingService.CacheRemote(info.src, type).then(cacheInfo => { - if (cacheInfo) { - SpawnCachedMira(cacheInfo, type, status) - } else { - status.Fail("Failed to cache") - } - }).catch(() => status.Fail()) + MirabufCachingService.CacheRemote(info.src, type) + .then(cacheInfo => { + if (cacheInfo) { + SpawnCachedMira(cacheInfo, type, status) + } else { + status.Fail("Failed to cache") + } + }) + .catch(() => status.Fail()) closePanel(panelId) }, @@ -263,13 +267,15 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { const status = new ProgressHandle(data.attributes.displayName ?? data.id) status.Update("Downloading from APS...", 0.05) - MirabufCachingService.CacheAPS(data, type).then(cacheInfo => { - if (cacheInfo) { - SpawnCachedMira(cacheInfo, type, status) - } else { - status.Fail("Failed to cache") - } - }).catch(() => status.Fail()) + MirabufCachingService.CacheAPS(data, type) + .then(cacheInfo => { + if (cacheInfo) { + SpawnCachedMira(cacheInfo, type, status) + } else { + status.Fail("Failed to cache") + } + }) + .catch(() => status.Fail()) closePanel(panelId) }, From fec36d84be4caed41c21af6dd51d7aa62c13c27d Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Wed, 17 Jul 2024 12:30:13 -0600 Subject: [PATCH 53/65] Linting and build errors --- .../simulation/wpilib_brain/WPILibBrain.ts | 2 +- fission/src/ui/panels/WSViewPanel.tsx | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index b00d4aabec..835bf27c5a 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -10,7 +10,7 @@ const PWM_SPEED = " From d5b8a8e90f4aa754c6eef712214afdd4f3d453e4 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Wed, 17 Jul 2024 12:32:05 -0600 Subject: [PATCH 54/65] Formatted --- fission/src/Synthesis.tsx | 6 +- .../systems/simulation/SimulationSystem.ts | 28 +-- .../simulation/wpilib_brain/WPILibBrain.ts | 109 +++++------ .../simulation/wpilib_brain/WPILibWSWorker.ts | 36 ++-- fission/src/ui/components/MainHUD.tsx | 11 +- fission/src/ui/panels/WSViewPanel.tsx | 185 ++++++++++++------ 6 files changed, 217 insertions(+), 158 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 48a5c73b88..cb2dd3ea32 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -60,10 +60,10 @@ import Skybox from "./ui/components/Skybox.tsx" import ConfigureRobotModal from "./ui/modals/configuring/ConfigureRobotModal.tsx" import ResetAllInputsModal from "./ui/modals/configuring/ResetAllInputsModal.tsx" -import WPILibWSWorker from '@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker' -import WSViewPanel from './ui/panels/WSViewPanel.tsx'; +import WPILibWSWorker from "@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker" +import WSViewPanel from "./ui/panels/WSViewPanel.tsx" -const DEFAULT_MIRA_PATH = '/api/mira/Robots/Team 2471 (2018)_v7.mira'; +const DEFAULT_MIRA_PATH = "/api/mira/Robots/Team 2471 (2018)_v7.mira" export let worker: Worker | undefined = undefined diff --git a/fission/src/systems/simulation/SimulationSystem.ts b/fission/src/systems/simulation/SimulationSystem.ts index 9023815afb..fb18d3af4f 100644 --- a/fission/src/systems/simulation/SimulationSystem.ts +++ b/fission/src/systems/simulation/SimulationSystem.ts @@ -1,16 +1,16 @@ -import JOLT from "@/util/loading/JoltSyncLoader"; -import Mechanism from "../physics/Mechanism"; -import WorldSystem from "../WorldSystem"; -import Brain from "./Brain"; -import Driver from "./driver/Driver"; -import Stimulus from "./stimulus/Stimulus"; -import HingeDriver from "./driver/HingeDriver"; -import WheelDriver from "./driver/WheelDriver"; -import SliderDriver from "./driver/SliderDriver"; -import HingeStimulus from "./stimulus/HingeStimulus"; -import WheelRotationStimulus from "./stimulus/WheelStimulus"; -import SliderStimulus from "./stimulus/SliderStimulus"; -import ChassisStimulus from "./stimulus/ChassisStimulus"; +import JOLT from "@/util/loading/JoltSyncLoader" +import Mechanism from "../physics/Mechanism" +import WorldSystem from "../WorldSystem" +import Brain from "./Brain" +import Driver from "./driver/Driver" +import Stimulus from "./stimulus/Stimulus" +import HingeDriver from "./driver/HingeDriver" +import WheelDriver from "./driver/WheelDriver" +import SliderDriver from "./driver/SliderDriver" +import HingeStimulus from "./stimulus/HingeStimulus" +import WheelRotationStimulus from "./stimulus/WheelStimulus" +import SliderStimulus from "./stimulus/SliderStimulus" +import ChassisStimulus from "./stimulus/ChassisStimulus" // import WPILibConnector from "./wpilib_brain/WPILibConnector"; class SimulationSystem extends WorldSystem { @@ -19,7 +19,7 @@ class SimulationSystem extends WorldSystem { constructor() { super() - this._simMechanisms = new Map(); + this._simMechanisms = new Map() // WPILibConnector.getInstance() } diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index 835bf27c5a..43127ece74 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import Mechanism from "@/systems/physics/Mechanism"; -import Brain from "../Brain"; +import Mechanism from "@/systems/physics/Mechanism" +import Brain from "../Brain" -import WPILibWSWorker from './WPILibWSWorker?worker' +import WPILibWSWorker from "./WPILibWSWorker?worker" const worker = new WPILibWSWorker() @@ -12,15 +12,13 @@ const CANMOTOR_DUTY_CYCLE = "" ? FieldType.Both : FieldType.Read - case '>': + case ">": return FieldType.Write default: return FieldType.Unknown @@ -41,7 +39,7 @@ function GetFieldType(field: string): FieldType { export const simMap = new Map>() export class SimGeneric { - private constructor() { } + private constructor() {} public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { const fieldType = GetFieldType(field) @@ -49,7 +47,7 @@ export class SimGeneric { console.warn(`Field '${field}' is not a read or both field type`) return undefined } - + const map = simMap.get(simType) if (!map) { console.warn(`No '${simType}' devices found`) @@ -62,7 +60,7 @@ export class SimGeneric { return undefined } - return data[field] as (T | undefined) ?? defaultValue + return (data[field] as T | undefined) ?? defaultValue } public static Set(simType: SimType, device: string, field: string, value: T): boolean { @@ -89,12 +87,12 @@ export class SimGeneric { data[field] = value worker.postMessage({ - command: 'update', + command: "update", data: { type: simType, device: device, - data: selectedData - } + data: selectedData, + }, }) window.dispatchEvent(new SimMapUpdateEvent(true)) @@ -103,41 +101,41 @@ export class SimGeneric { } export class SimPWM { - private constructor() { } + private constructor() {} public static GetSpeed(device: string): number | undefined { - return SimGeneric.Get('PWM', device, PWM_SPEED, 0.0) + return SimGeneric.Get("PWM", device, PWM_SPEED, 0.0) } public static GetPosition(device: string): number | undefined { - return SimGeneric.Get('PWM', device, PWM_POSITION, 0.0) + return SimGeneric.Get("PWM", device, PWM_POSITION, 0.0) } } export class SimCANMotor { - private constructor() { } + private constructor() {} public static GetDutyCycle(device: string): number | undefined { - return SimGeneric.Get('CANMotor', device, CANMOTOR_DUTY_CYCLE, 0.0) + return SimGeneric.Get("CANMotor", device, CANMOTOR_DUTY_CYCLE, 0.0) } public static SetSupplyVoltage(device: string, voltage: number): boolean { - return SimGeneric.Set('CANMotor', device, CANMOTOR_SUPPLY_VOLTAGE, voltage) + return SimGeneric.Set("CANMotor", device, CANMOTOR_SUPPLY_VOLTAGE, voltage) } } export class SimCANEncoder { - private constructor() { } + private constructor() {} public static SetRawInputPosition(device: string, rawInputPosition: number): boolean { - return SimGeneric.Set('CANEncoder', device, CANENCODER_RAW_INPUT_POSITION, rawInputPosition) + return SimGeneric.Set("CANEncoder", device, CANENCODER_RAW_INPUT_POSITION, rawInputPosition) } } -worker.addEventListener('message', (eventData: MessageEvent) => { - let data: any | undefined; +worker.addEventListener("message", (eventData: MessageEvent) => { + let data: any | undefined try { - if (typeof(eventData.data) == 'object') { + if (typeof eventData.data == "object") { data = eventData.data } else { data = JSON.parse(eventData.data) @@ -145,9 +143,9 @@ worker.addEventListener('message', (eventData: MessageEvent) => { } catch (e) { console.warn(`Failed to parse data:\n${JSON.stringify(eventData.data)}`) } - + if (!data || !data.type) { - console.log('No data, bailing out') + console.log("No data, bailing out") return } @@ -157,25 +155,25 @@ worker.addEventListener('message', (eventData: MessageEvent) => { const updateData = data.data switch (data.type) { - case 'PWM': - console.debug('pwm') - UpdateSimMap('PWM', device, updateData) + case "PWM": + console.debug("pwm") + UpdateSimMap("PWM", device, updateData) break - case 'Solenoid': - console.debug('solenoid') - UpdateSimMap('Solenoid', device, updateData) + case "Solenoid": + console.debug("solenoid") + UpdateSimMap("Solenoid", device, updateData) break - case 'SimDevice': - console.debug('simdevice') - UpdateSimMap('SimDevice', device, updateData) + case "SimDevice": + console.debug("simdevice") + UpdateSimMap("SimDevice", device, updateData) break - case 'CANMotor': - console.debug('canmotor') - UpdateSimMap('CANMotor', device, updateData) + case "CANMotor": + console.debug("canmotor") + UpdateSimMap("CANMotor", device, updateData) break - case 'CANEncoder': - console.debug('canencoder') - UpdateSimMap('CANEncoder', device, updateData) + case "CANEncoder": + console.debug("canencoder") + UpdateSimMap("CANEncoder", device, updateData) break default: // console.debug(`Unrecognized Message:\n${data}`) @@ -195,35 +193,32 @@ function UpdateSimMap(type: SimType, device: string, updateData: any) { currentData = {} typeMap.set(device, currentData) } - Object.entries(updateData).forEach(kvp => currentData[kvp[0]] = kvp[1]) + Object.entries(updateData).forEach(kvp => (currentData[kvp[0]] = kvp[1])) window.dispatchEvent(new SimMapUpdateEvent(false)) } class WPILibBrain extends Brain { - constructor(mech: Mechanism) { super(mech) } - public Update(_: number): void { } - + public Update(_: number): void {} + public Enable(): void { - worker.postMessage({ command: 'connect' }) + worker.postMessage({ command: "connect" }) } public Disable(): void { - worker.postMessage({ command: 'disconnect' }) + worker.postMessage({ command: "disconnect" }) } - } export class SimMapUpdateEvent extends Event { - public static readonly TYPE: string = "ws/sim-map-update" - private _internalUpdate: boolean; - + private _internalUpdate: boolean + public get internalUpdate(): boolean { return this._internalUpdate } @@ -235,4 +230,4 @@ export class SimMapUpdateEvent extends Event { } } -export default WPILibBrain \ No newline at end of file +export default WPILibBrain diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts index 784db32306..836dbd0d4f 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts @@ -5,18 +5,26 @@ let socket: WebSocket | undefined = undefined const connectMutex = new Mutex() async function tryConnect(port: number | undefined): Promise { - await connectMutex.runExclusive(() => { - if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { - return - } + await connectMutex + .runExclusive(() => { + if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { + return + } - socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) + socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`) - socket.addEventListener('open', () => { console.log('WS Opened'); self.postMessage({ status: 'open' }); }) - socket.addEventListener('error', () => { console.log('WS Could not open'); self.postMessage({ status: 'error' }) }) + socket.addEventListener("open", () => { + console.log("WS Opened") + self.postMessage({ status: "open" }) + }) + socket.addEventListener("error", () => { + console.log("WS Could not open") + self.postMessage({ status: "error" }) + }) - socket.addEventListener('message', onMessage) - }).then(() => console.debug('Mutex released')) + socket.addEventListener("message", onMessage) + }) + .then(() => console.debug("Mutex released")) } async function tryDisconnect(): Promise { @@ -35,15 +43,15 @@ function onMessage(event: MessageEvent) { self.postMessage(event.data) } -self.addEventListener('message', e => { +self.addEventListener("message", e => { switch (e.data.command) { - case 'connect': + case "connect": tryConnect(e.data.port) break - case 'disconnect': + case "disconnect": tryDisconnect() break - case 'update': + case "update": if (socket) { socket.send(JSON.stringify(e.data.data)) } @@ -54,4 +62,4 @@ self.addEventListener('message', e => { } }) -console.log('Worker started') \ No newline at end of file +console.log("Worker started") diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index d920f03774..9d5c6db6d7 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -146,11 +146,7 @@ const MainHUD: React.FC = () => { } }} /> - } - onClick={() => openPanel("ws-view")} - /> + } onClick={() => openPanel("ws-view")} />
{ icon={} onClick={() => { // worker?.postMessage({ command: 'connect' }); - const miraObjs = [...World.SceneRenderer.sceneObjects.entries()] - .filter(x => x[1] instanceof MirabufSceneObject) + const miraObjs = [...World.SceneRenderer.sceneObjects.entries()].filter( + x => x[1] instanceof MirabufSceneObject + ) console.log(`Number of mirabuf scene objects: ${miraObjs.length}`) if (miraObjs.length > 0) { const mechanism = (miraObjs[0][1] as MirabufSceneObject).mechanism diff --git a/fission/src/ui/panels/WSViewPanel.tsx b/fission/src/ui/panels/WSViewPanel.tsx index 969ce74a50..2168b2f203 100644 --- a/fission/src/ui/panels/WSViewPanel.tsx +++ b/fission/src/ui/panels/WSViewPanel.tsx @@ -1,6 +1,17 @@ import Panel, { PanelPropsImpl } from "@/components/Panel" import { SimMapUpdateEvent, SimGeneric, simMap, SimType } from "@/systems/simulation/wpilib_brain/WPILibBrain" -import { Box, Stack, styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" +import { + Box, + Stack, + styled, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material" import { useCallback, useEffect, useMemo, useState } from "react" import { GrConnect } from "react-icons/gr" import Dropdown from "../components/Dropdown" @@ -18,42 +29,84 @@ const TypoStyled = styled(Typography)({ function generateTableBody() { return ( - {simMap.has('PWM') ? [...simMap.get('PWM')!.entries()].filter(x => x[1][" { - return ( - - PWM - {x[0]} - {JSON.stringify(x[1])} - - ) - }) : <>} - {simMap.has('SimDevice') ? [...simMap.get('SimDevice')!.entries()].map(x => { - return ( - - SimDevice - {x[0]} - {JSON.stringify(x[1])} - - ) - }): <>} - {simMap.has('CANMotor') ? [...simMap.get('CANMotor')!.entries()].map(x => { - return ( - - CAN Motor - {x[0]} - {JSON.stringify(x[1])} - - ) - }) : <>} - {simMap.has('CANEncoder') ? [...simMap.get('CANEncoder')!.entries()].map(x => { - return ( - - CAN Encoder - {x[0]} - {JSON.stringify(x[1])} - - ) - }) : <>} + {simMap.has("PWM") ? ( + [...simMap.get("PWM")!.entries()] + .filter(x => x[1][" { + return ( + + + PWM + + + {x[0]} + + + {JSON.stringify(x[1])} + + + ) + }) + ) : ( + <> + )} + {simMap.has("SimDevice") ? ( + [...simMap.get("SimDevice")!.entries()].map(x => { + return ( + + + SimDevice + + + {x[0]} + + + {JSON.stringify(x[1])} + + + ) + }) + ) : ( + <> + )} + {simMap.has("CANMotor") ? ( + [...simMap.get("CANMotor")!.entries()].map(x => { + return ( + + + CAN Motor + + + {x[0]} + + + {JSON.stringify(x[1])} + + + ) + }) + ) : ( + <> + )} + {simMap.has("CANEncoder") ? ( + [...simMap.get("CANEncoder")!.entries()].map(x => { + return ( + + + CAN Encoder + + + {x[0]} + + + {JSON.stringify(x[1])} + + + ) + }) + ) : ( + <> + )} ) } @@ -76,7 +129,6 @@ function setGeneric(simType: SimType, device: string, field: string, value: stri } const WSViewPanel: React.FC = ({ panelId }) => { - const [tb, setTb] = useState(generateTableBody()) const [selectedType, setSelectedType] = useState() @@ -87,12 +139,10 @@ const WSViewPanel: React.FC = ({ panelId }) => { const deviceSelect = useMemo(() => { if (!selectedType || !simMap.has(selectedType)) { - return (<>) + return <> } - return ( - setSelectedDevice(v)}/> - ) + return setSelectedDevice(v)} /> }, [selectedType]) useEffect(() => { @@ -112,45 +162,54 @@ const WSViewPanel: React.FC = ({ panelId }) => { }, [onSimMapUpdate]) return ( - } - panelId={panelId} - openLocation="right" - sidePadding={4} - > + } panelId={panelId} openLocation="right" sidePadding={4}> - Type - Device - Data + + Type + + + Device + + + Data + {tb}
- setSelectedType(v as unknown as SimType)} /> + setSelectedType(v as unknown as SimType)} + /> {deviceSelect} - {selectedDevice - ? - setField(v)} /> - setValue(v)} /> - setSelectedValueType(v as ValueType)} /> + {selectedDevice ? ( + + setField(v)} /> + setValue(v)} /> + setSelectedValueType(v as ValueType)} + />