diff --git a/Autogen/CAN/Doc/GRCAN.CANdo b/Autogen/CAN/Doc/GRCAN.CANdo index 3612039b6..93a024923 100644 --- a/Autogen/CAN/Doc/GRCAN.CANdo +++ b/Autogen/CAN/Doc/GRCAN.CANdo @@ -16,7 +16,6 @@ routing: - msg: BCU Status 1 - msg: BCU Status 2 - msg: BCU Status 3 - - msg: DC-DC Status CAN2: Debugger: - msg: Debug FD @@ -286,7 +285,7 @@ Message ID: ECU Status 1: MSG ID: 0x003 MSG LENGTH: 8 - state_messages: + ecu_state: bit_start: 0 comment: [Byte 0 / Bits 0-1] GLV States @@ -3185,49 +3184,6 @@ Message ID: scaled min: 0 scaled max: 63.75 map equation: "0.25x" - DC-DC Status: - MSG ID: 0x012 - MSG LENGTH: 7 - Input Voltage: - bit_start: 0 - comment: ~20v for LV (LV only. Send 0 for HV) - data type: u16 - units: Volts - scaled min: 0 - scaled max: 65.535 - map equation: "x/1000" - Output Voltage: - bit_start: 16 - comment: ~12v for LV and ~20v for HV - data type: u16 - units: Volts - scaled min: 0 - scaled max: 65.535 - map equation: "x/1000" - Input Current: - bit_start: 32 - comment: Input current (LV only. Send 0 for HV) - data type: u8 - units: Amps - scaled min: 0 - scaled max: 25.5 - map equation: "0.1x" - Output Current: - bit_start: 40 - comment: Output current - data type: u8 - units: Amps - scaled min: 0 - scaled max: 25.5 - map equation: "0.1x" - DC-DC Temp: - bit_start: 48 - comment: Temp of DC-DC converter - data type: u8 - units: Celsius - scaled min: 0 - scaled max: 255 - map equation: "1x" Inverter Status 1: MSG ID: 0x013 MSG LENGTH: 6 @@ -3284,7 +3240,7 @@ Message ID: map equation: "x-40" Inverter Status 3: MSG ID: 0x015 - MSG LENGTH: 2 + MSG LENGTH: 3 Motor temperature: bit_start: 0 comment: Celsius + 40, uint8_t @@ -3293,45 +3249,10 @@ Message ID: scaled min: -40 scaled max: 215 map equation: "x-40" - Over voltage faults: + fault_bits: bit_start: 16 - comment: TS above set max voltage - data type: b - units: Bool - Under voltage fault: - bit_start: 17 - comment: TS below set min voltage - data type: b - units: Bool - Inv. overtemp fault: - bit_start: 18 - comment: Inverter over set max temp - data type: b - units: Bool - Motor overtemp fault: - bit_start: 19 - comment: Motor over set max temp - data type: b - units: Bool - Transistor fault: - bit_start: 20 - comment: Mosfet or mosfet drive error - data type: b - units: Bool - Encoder fault: - bit_start: 21 - comment: Encoder communication or calc error - data type: b - units: Bool - CAN fault: - bit_start: 22 - comment: CAN message error or timeout - data type: b - units: Bool - Future use: - bit_start: 23 - data type: b - units: Bool + comment: TS above set max voltage, TS below set min voltage, Inverter over set max temp, Motor over set max temp, Mosfet or mosfet drive error, Encoder communication or calc error, CAN message error or timeout + data type: u8 Inverter Config: MSG ID: 0x016 MSG LENGTH: 7 @@ -3453,291 +3374,46 @@ Message ID: MSG LENGTH: 3 button_flags: bit_start: 0 - comment: TS Active = bit 0, RTD = bit 1, bits 2–7 reserved + comment: + [Byte 0 / Bits 0-7] + 0: TS Active + 1: RTD + 2-7: Reserved data type: u8 led_bits: bit_start: 8 - comment: BMS = bit 0 of this byte, IMD = bit 1, BSPD = bit 2, bits 3–7 reserved + comment: + [Byte 1 / Bits 8-15] + 0: BMS + 1: IMD + 2: BSPD + 3-7: Reserved data type: u8 Dash Config: MSG ID: 0x01B MSG LENGTH: 1 - BMS LED: - bit_start: 0 - comment: LED command (0: off, 1: on) - data type: b - units: Bool - IMD LED: - bit_start: 1 - comment: LED command (0: off, 1: on) - data type: b - units: Bool - BSPD LED: - bit_start: 2 - comment: LED command (0: off, 1: on) - data type: b - units: Bool - Steering Status: - MSG ID: 0x01C - MSG LENGTH: 2 - Current Encoder: - bit_start: 0 - comment: Position of knob (1-16) - data type: u4 - units: Position - scaled min: 1 - scaled max: 16 - map equation: "1x" - Torque Map Encoder: - bit_start: 4 - comment: Position of knob (1-16) - data type: u4 - units: Position - scaled min: 1 - scaled max: 16 - map equation: "1x" - Regen: - bit_start: 8 - comment: Position of knob (1-16) - data type: u4 - units: Position - scaled min: 1 - scaled max: 16 - map equation: "1x" - Button 1: - bit_start: 12 - comment: Button State - data type: b - units: Bool - Button 2: - bit_start: 13 - comment: Button State - data type: b - units: Bool - Button 3: - bit_start: 14 - comment: Button State - data type: b - units: Bool - Button 4: - bit_start: 15 - comment: Button State - data type: b - units: Bool - Steering Config: - MSG ID: 0x01D - MSG LENGTH: 0 - Reserved: - bit_start: 0 - SAM Brake IR: - MSG ID: 0x01E - MSG LENGTH: 1 - Temp: - bit_start: 0 - comment: IR Temp of Brakes - data type: u8 - units: Celsius - scaled min: 0 - scaled max: 255 - map equation: "1x" - SAM Tire Temp: - MSG ID: 0x01F - MSG LENGTH: 4 - Outside Temp: - bit_start: 0 - comment: Furthest from chassis - data type: u8 - units: Celsius - scaled min: 0 - scaled max: 255 - map equation: "1x" - Outside Middle Temp: - bit_start: 8 - comment: Middle of tire - data type: u8 - units: Celsius - scaled min: 0 - scaled max: 255 - map equation: "1x" - Inside Middle Temp: - bit_start: 16 - comment: Middle of tire - data type: u8 - units: Celsius - scaled min: 0 - scaled max: 255 - map equation: "1x" - Inside Temp: - bit_start: 24 - comment: Closest to chassis - data type: u8 - units: Celsius - scaled min: 0 - scaled max: 255 - map equation: "1x" - SAM IMU: - MSG ID: 0x020 - MSG LENGTH: 12 - Accel X: - bit_start: 0 - comment: Acceleration in X-axis - data type: u16 - units: Meters/s^2 - scaled min: -327.68 - scaled max: 327.67 - map equation: "0.01x-327.68" - Accel Y: - bit_start: 16 - comment: Acceleration in Y-axis - data type: u16 - units: Meters/s^2 - scaled min: -327.68 - scaled max: 327.67 - map equation: "0.01x-327.68" - Accel Z: - bit_start: 32 - comment: Acceleration in Z-axis - data type: u16 - units: Meters/s^2 - scaled min: -327.68 - scaled max: 327.67 - map equation: "0.01x-327.68" - Gyro X: - bit_start: 48 - comment: Angular velocity in X-axis - data type: u16 - units: Meters/s^2 - scaled min: -32.768 - scaled max: 32.767 - map equation: "0.001x-32.768" - Gyro Y: - bit_start: 64 - comment: Angular velocity in Y-axis - data type: u16 - units: Meters/s^2 - scaled min: -32.768 - scaled max: 32.767 - map equation: "0.001x-32.768" - Gyro Z: - bit_start: 80 - comment: Angular velocity in Z-axis - data type: u16 - units: Meters/s^2 - scaled min: -32.768 - scaled max: 32.767 - map equation: "0.001x-32.768" - SAM GPS 1: - MSG ID: 0x021 - MSG LENGTH: 8 - Latitude: - bit_start: 0 - comment: Latitude in decimal degrees - data type: u32 - units: Degrees - Longitude: - bit_start: 32 - comment: Longitude in decimal degrees - data type: u32 - units: Degrees - SAM GPS 2: - MSG ID: 0x022 - MSG LENGTH: 8 - Accuracy: - bit_start: 0 - comment: GPS position accuracy - data type: u32 - Attitude: - bit_start: 32 - comment: Vehicle attitude - data type: u32 - SAM GPS Time: - MSG ID: 0x023 - MSG LENGTH: 8 - Time: - bit_start: 0 - comment: Time in seconds since GPS Epoch - data type: u32 - Time of Week Ms: - bit_start: 32 - comment: Time of week in milliseconds - data type: u32 - SAM GPS Heading: - MSG ID: 0x024 - MSG LENGTH: 4 - Heading from North: - bit_start: 0 - comment: Heading angle relative to true North - data type: u32 - SAM Sus Pots: - MSG ID: 0x025 - MSG LENGTH: 1 - Suspension Angle: + led_bits: bit_start: 0 - comment: Pot Pos + comment: + [Byte 0 / Bits 0-7] + 0: BMS LED command + 1: IMD LED command + 2: BSPD LED command + 3-7: Reserved data type: u8 - units: degrees - scaled min: 0 - scaled max: 255 - map equation: "1x" - SAM TOF: - MSG ID: 0x026 - MSG LENGTH: 2 - Height: - bit_start: 0 - comment: Ride Height - data type: u16 - units: mm - scaled min: 0 - scaled max: 255 - map equation: "x/256" - SAM Rear Wheelspeed: - MSG ID: 0x027 - MSG LENGTH: 2 - Speed: - bit_start: 0 - comment: Wheel RPM - data type: u16 - units: RPM - scaled min: -3276.8 - scaled max: 3276.7 - map equation: "0.1x--3276.8" - SAM Pushrod Force: - MSG ID: 0x028 - MSG LENGTH: 2 - Load Force: - bit_start: 0 - comment: Pushrod Force - data type: u16 - units: N - scaled min: -3276.8 - scaled max: 3276.7 - map equation: "0.1x--3276.8" TCM Status: MSG ID: 0x029 MSG LENGTH: 8 - Connection Status: + status_bits: bit_start: 0 - comment: 1: OK, 0: Timeout - data type: b - units: Bool - MQTT Status: - bit_start: 1 - comment: 1: OK, 0: Timeout - data type: b - units: Bool - Epic Shelter Status: - bit_start: 2 - comment: 1: In Progress, 0: Idle - data type: b - units: Bool - Camera Status: - bit_start: 3 - comment: 1: Recording, 0: Idle - data type: b - units: Bool - Reserved: - bit_start: 4-7 - Ping: + comment: + [Byte 0 / Bits 0-7] + 0: Connection Status + 1: MQTT Status + 2: Epic Shelter Status + 3: Camera Status + 4-7: Reserved + mapache_ping: bit_start: 8 comment: Mapache ping (upload) data type: u16 @@ -3863,51 +3539,6 @@ Message ID: comment: power draw in mW data type: u16 units: Celsius - Dash Warning Flags: - MSG ID: 0x02B - MSG LENGTH: 1 - BSE APPS Violation: - bit_start: 0 - comment: 1: Violation, 0: OK - data type: b - units: Bool - Reserved: - bit_start: 1 - Reserved: - bit_start: 2 - Reserved: - bit_start: 3 - Reserved: - bit_start: 4 - Reserved: - bit_start: 5 - Reserved: - bit_start: 6 - Reserved: - bit_start: 7 - Specific Brake IR: - MSG ID: 0x02C - MSG LENGTH: 2 - Wheel identifier: - bit_start: 0 - comment: Wheel identifier according to the wiki - data type: u8 - Temp: - bit_start: 8 - comment: IR Temp of Brakes - data type: u8 - units: Celsius - scaled min: 0 - scaled max: 255 - map equation: "1x" - ECU Ping Information: - MSG ID: 0x02D - MSG LENGTH: 3 - Online pings: - bit_start: 0 - comment: Literal copy of ECU Status's status bit map - data type: b[24] - units: Bool map ECU Analog Data: MSG ID: 0x02E MSG LENGTH: 16 @@ -4063,9 +3694,8 @@ Message ID: bit_start: 0 comment: Represents the total number of clock cycles elapsed for 10 iterations of the main loop - data type: u32 - units: Clock Cycles - data type: u8 + data type: u32 + units: Clock Cycles Custom CAN ID: DTI Control 1: diff --git a/Autogen/CAN/Doc/GRCAN.dbc b/Autogen/CAN/Doc/GRCAN.dbc index f63b31fcf..2013ca6d4 100644 --- a/Autogen/CAN/Doc/GRCAN.dbc +++ b/Autogen/CAN/Doc/GRCAN.dbc @@ -37,13 +37,6 @@ BO_ 2147682562 BCU_BCU_Status_3_to_ECU: 8 BCU SG_ HV_Input_Current : 32|16@1+ (0.001,0) [0|65.535] "Amps" ECU SG_ HV_Output_Current : 48|16@1+ (0.001,0) [0|65.535] "Amps" ECU -BO_ 2147684866 BCU_DC_DC_Status_to_ECU: 7 BCU - SG_ Input_Voltage : 0|16@1+ (0.001,0) [0|65.535] "Volts" ECU - SG_ Output_Voltage : 16|16@1+ (0.001,0) [0|65.535] "Volts" ECU - SG_ Input_Current : 32|8@1+ (0.1,0) [0|25.5] "Amps" ECU - SG_ Output_Current : 40|8@1+ (0.1,0) [0|25.5] "Amps" ECU - SG_ DC_DC_Temp : 48|8@1+ (1,0) [0|255] "Celsius" ECU - BO_ 2147680513 BCU_Debug_FD_to_Debugger: 64 BCU SG_ Debug : 0|64@1- (1,0) [0|0] "" Debugger @@ -388,6 +381,28 @@ BO_ 2550588916 BCU_Charger_Control_to_Charger: 5 BCU SG_ Set_Current : 16|8@1+ (1,0) [0|0] "" Charger SG_ Charge_Enable : 17|8@1+ (1,0) [0|0] "" Charger +BO_ 2147682050 BCU_BCU_Status_1_to_CCU: 8 BCU + SG_ Accumulator_Voltage : 0|16@1+ (0.01,0) [0|655.35] "Volts" CCU + SG_ TS_Voltage : 16|16@1+ (0.01,0) [0|655.35] "Volts" CCU + SG_ Accumulator_Current : 32|16@1- (0.01,0) [-327.68|327.67] "Amps" CCU + SG_ Accumulator_SOC : 48|8@1+ (0.392157,0) [0|100] "%" CCU + SG_ GLV_SOC : 56|8@1+ (0.392157,0) [0|100] "%" CCU + +BO_ 2147682306 BCU_BCU_Status_2_to_CCU: 7 BCU + SG_ 20v_Voltage : 0|8@1+ (1,0) [0|0] "" CCU + SG_ 12v_Voltage : 8|8@1+ (1,0) [0|0] "" CCU + SG_ SDC_Voltage : 16|8@1+ (1,0) [0|0] "" CCU + SG_ Min_Cell_Voltage : 24|8@1+ (1,0) [0|0] "" CCU + SG_ Max_Cell_Temp : 32|8@1+ (1,0) [0|0] "" CCU + SG_ status_flags : 40|1@1+ (1,0) [0|0] "" CCU + SG_ precharge_latch_flags : 48|1@1+ (1,0) [0|0] "" CCU + +BO_ 2147682562 BCU_BCU_Status_3_to_CCU: 8 BCU + SG_ HV_Input_Voltage : 0|16@1+ (0.01,0) [0|655.35] "Volts" CCU + SG_ HV_Output_Voltage : 16|16@1+ (0.01,0) [0|655.35] "Volts" CCU + SG_ HV_Input_Current : 32|16@1+ (0.001,0) [0|65.535] "Amps" CCU + SG_ HV_Output_Current : 48|16@1+ (0.001,0) [0|65.535] "Amps" CCU + BO_ 2566869221 Charger_Charger_Data_to_BCU: 8 Charger SG_ Output_Voltage : 0|8@1+ (1,0) [0|0] "" BCU SG_ Output_Current : 16|8@1+ (1,0) [0|0] "" BCU @@ -618,15 +633,13 @@ BO_ 2147615247 ECU_Ping_to_Fan_Controller_3: 4 ECU SG_ Timestamp : 0|32@1+ (1,0) [0|4,294,967,296] "ms" Fan_Controller_3 BO_ 2147621637 ECU_Dash_Config_to_Dash_Panel: 1 ECU - SG_ BMS_LED : 0|1@1+ (1,0) [0|0] "Bool" Dash_Panel - SG_ IMD_LED : 1|1@1+ (1,0) [0|0] "Bool" Dash_Panel - SG_ BSPD_LED : 2|1@1+ (1,0) [0|0] "Bool" Dash_Panel + SG_ led_bits : 0|8@1+ (1,0) [0|0] "" Dash_Panel BO_ 2147615237 ECU_Ping_to_Dash_Panel: 4 ECU SG_ Timestamp : 0|32@1+ (1,0) [0|4,294,967,296] "ms" Dash_Panel BO_ 2147615743 ECU_ECU_Status_1_to_ALL: 8 ECU - SG_ state_messages : 0|1@1+ (1,0) [0|0] "Bool" ALL + SG_ ecu_state : 0|1@1+ (1,0) [0|0] "Bool" ALL SG_ status_flags : 8|1@1+ (1,0) [0|0] "" ALL SG_ Power_Level : 16|4@1+ (1,0) [0|0] "" ALL SG_ Torque_Map : 20|4@1+ (1,0) [0|0] "" ALL @@ -653,7 +666,7 @@ BO_ 2147614977 ECU_Debug_FD_to_Debugger: 64 ECU BO_ 2147615233 ECU_Ping_to_Debugger: 4 ECU SG_ Timestamp : 0|32@1+ (1,0) [0|4,294,967,296] "ms" Debugger -BO_ 2147626500 ECU_ECU_Pedals_Data_to_TCM: 16 ECU +BO_ 2147626500 ECU_ECU_Analog_Data_to_TCM: 16 ECU SG_ BSPD_Signal : 0|16@1+ (0.0015259,0) [0|100] "%" TCM SG_ BSE_Signal : 16|16@1+ (0.0015259,0) [0|100] "%" TCM SG_ APPS_1_Signal : 32|16@1+ (0.0015259,0) [0|100] "%" TCM @@ -663,6 +676,9 @@ BO_ 2147626500 ECU_ECU_Pedals_Data_to_TCM: 16 ECU SG_ Steering_Angle_Signal : 96|16@1+ (0.0015259,0) [0|100] "%" TCM SG_ AUX_Signal : 112|16@1+ (0.0015259,0) [0|100] "%" TCM +BO_ 2147689220 ECU_ECU_Performance_to_TCM: 4 ECU + SG_ Elapsed_Cycles : 0|32@1+ (1,0) [0|0] "Clock Cycles" TCM + BO_ 269 Energy_Meter_EM_Measurement_to_BCU: 8 Energy_Meter SG_ Current : 0|8@1+ (1,0) [0|0] "" BCU SG_ Voltage : 32|8@1+ (1,0) [0|0] "" BCU @@ -752,16 +768,9 @@ BO_ 2148013058 GR_Inverter_Inverter_Status_2_to_ECU: 6 GR_Inverter SG_ V_MOSFET_temperature : 16|8@1+ (1,-40) [-40|215] "Celsius" ECU SG_ W_MOSFET_temperature : 32|8@1+ (1,-40) [-40|215] "Celsius" ECU -BO_ 2148013314 GR_Inverter_Inverter_Status_3_to_ECU: 2 GR_Inverter +BO_ 2148013314 GR_Inverter_Inverter_Status_3_to_ECU: 3 GR_Inverter SG_ Motor_temperature : 0|8@1+ (1,-40) [-40|215] "Celsius" ECU - SG_ Over_voltage_faults : 16|1@1+ (1,0) [0|0] "Bool" ECU - SG_ Under_voltage_fault : 17|1@1+ (1,0) [0|0] "Bool" ECU - SG_ Inv_overtemp_fault : 18|1@1+ (1,0) [0|0] "Bool" ECU - SG_ Motor_overtemp_fault : 19|1@1+ (1,0) [0|0] "Bool" ECU - SG_ Transistor_fault : 20|1@1+ (1,0) [0|0] "Bool" ECU - SG_ Encoder_fault : 21|1@1+ (1,0) [0|0] "Bool" ECU - SG_ CAN_fault : 22|1@1+ (1,0) [0|0] "Bool" ECU - SG_ Future_use : 23|1@1+ (1,0) [0|0] "Bool" ECU + SG_ fault_bits : 16|8@1+ (1,0) [0|0] "" ECU BO_ 35 IMD_IMD_response_to_BCU: 0 IMD diff --git a/Autogen/CAN/Inc/GRCAN_MSG_DATA.h b/Autogen/CAN/Inc/GRCAN_MSG_DATA.h index bf6fe2b6a..5105f8c26 100644 --- a/Autogen/CAN/Inc/GRCAN_MSG_DATA.h +++ b/Autogen/CAN/Inc/GRCAN_MSG_DATA.h @@ -4,18 +4,6 @@ #include -/** Debug 2.0 */ -typedef struct { - /** Essentially a print statement up to 8 bytes long that whichever targeted can parse (Byte 0) */ - uint8_t debug[8]; -} GRCAN_DEBUG_2_0_MSG; - -/** Debug FD */ -typedef struct { - /** Essentially a print statement up to 64 bytes long that whichever targeted can parse (Byte 0) */ - uint8_t debug[64]; -} GRCAN_DEBUG_FD_MSG; - /** Ping */ typedef struct { /** Time in millis (Byte 0) */ @@ -310,18 +298,13 @@ typedef struct { /** Dash Config */ typedef struct { - /** BMS = bit 0 of this byte, IMD = bit 1, BSPD = bit 2, bits 3–7 reserved (Byte 1) */ + /** BMS LED command = bit 0, IMD LED command = bit 1, BSPD LED command = bit 2, bits 3-7 reserved (Byte 0) */ uint8_t led_bits; } GRCAN_DASH_CONFIG_MSG; /** TCM Status */ typedef struct { - /** - * Connection Status - 1: OK, 0: Timeout (bit 0) - * MQTT Status - 1: OK, 0: Timeout (bit 1) - * Epic Shelter Status - 1: In Progress, 0: Idle (bit 2) - * Camera Status - 1: Recording, 0: Idle (bit 3) - */ + /** "Connection Status = bit 0, MQTT Status = bit 1, Epic Shelter Status = bit 2, Camera Status = bit 3, bits 4-7 reserved" (Byte 0) */ uint8_t status_bits; /** Mapache ping (upload) (Byte 1) */ uint16_t mapache_ping; @@ -387,7 +370,7 @@ typedef struct { uint16_t power_draw; } GRCAN_TCM_RESOURCE_UTILIZATION_MSG; -/** ECU Pedals Data */ +/** ECU Analog Data */ typedef struct { /** 4-20 mA signal (Byte 0) */ uint16_t bspd_signal; @@ -467,10 +450,7 @@ typedef struct { /** ECU Performance */ typedef struct { - /** - * Represents the total number of clock cycles elapsed for 10 iterations of the main loop - * data type: u32 - * units: Clock Cycles (Byte 0) */ + /** Represents the total number of clock cycles elapsed for 10 iterations of the main loop (Byte 0) */ uint32_t elapsed_cycles; } GRCAN_ECU_PERFORMANCE_MSG; diff --git a/Autogen/CAN/Inc/GRCAN_MSG_ID.h b/Autogen/CAN/Inc/GRCAN_MSG_ID.h index f5739421f..a495dfc46 100644 --- a/Autogen/CAN/Inc/GRCAN_MSG_ID.h +++ b/Autogen/CAN/Inc/GRCAN_MSG_ID.h @@ -32,7 +32,6 @@ typedef enum { GRCAN_DASH_CONFIG = 0x01B, GRCAN_TCM_STATUS = 0x029, GRCAN_TCM_RESOURCE_UTILIZATION = 0x02A, - GRCAN_DASH_WARNING_FLAGS = 0x02B, GRCAN_ECU_ANALOG_DATA = 0x02E, GRCAN_GPS_LAT = 0x031, GRCAN_GPS_LON = 0x032, diff --git a/Autogen/CAN/Inc/GRCAN_NODE_ID.h b/Autogen/CAN/Inc/GRCAN_NODE_ID.h index f68b80888..deaea3c39 100644 --- a/Autogen/CAN/Inc/GRCAN_NODE_ID.h +++ b/Autogen/CAN/Inc/GRCAN_NODE_ID.h @@ -5,17 +5,20 @@ typedef enum { GRCAN_ALL = 0xFF, GRCAN_BCU = 0x03, - GRCAN_ECU = 0x02, GRCAN_CCU = 0x02, GRCAN_Charger = 0x00, GRCAN_Charging_SDC = 0x0C, GRCAN_DGPS = 0x30, + GRCAN_DTI_Inverter = 0x00, GRCAN_Dash_Panel = 0x05, GRCAN_Debugger = 0x01, + GRCAN_ECU = 0x02, + GRCAN_Energy_Meter = 0x00, GRCAN_Fan_Controller_1 = 0x0D, GRCAN_Fan_Controller_2 = 0x0E, GRCAN_Fan_Controller_3 = 0x0F, GRCAN_GR_Inverter = 0x08, + GRCAN_IMD = 0x00, GRCAN_TCM = 0x04, } GRCAN_NODE_ID; diff --git a/Autogen/CAN/Src/GRparser.pl b/Autogen/CAN/Src/GRparser.pl index d79ef535c..886af19f4 100644 --- a/Autogen/CAN/Src/GRparser.pl +++ b/Autogen/CAN/Src/GRparser.pl @@ -73,33 +73,27 @@ sub generate_gr_header_content { push @header_lines, "#define GR_IDS_H\n\n"; push @header_lines, "typedef enum {\n"; - # --- TRACKERS FOR ISSUE #373 --- + # Track emitted names to avoid exact duplicate enum symbols. my %seen_names; - my %seen_values; my @sorted = sort { $a->{name} cmp $b->{name} } @{$ids_ref}; for my $item (@sorted) { - my $const_name = $item->{name}; + my $const_name = 'GRCAN_' . $item->{name}; $const_name =~ s/[[:^alnum:]]/_/gsmx; my $val = $item->{id}; - # --- FIX FOR #373: Skip if name or ID value is already in the list --- + # Skip only duplicate enum names. if ( $seen_names{$const_name} ) { warn "Skipping duplicate Node Name: $const_name\n"; next; } - if ( defined $val && $seen_values{$val} ) { - warn "Issue #373: Skipping duplicate Node ID value: $val ($const_name)\n"; - next; - } if ( defined $val && $val ne q{} ) { push @header_lines, sprintf " %s = %s,\n", $const_name, $val; # Mark as processed $seen_names{$const_name} = 1; - $seen_values{$val} = 1; } } diff --git a/Autogen/CAN/Src/MSGparser.pl b/Autogen/CAN/Src/MSGparser.pl index 516098f85..cc880bb07 100644 --- a/Autogen/CAN/Src/MSGparser.pl +++ b/Autogen/CAN/Src/MSGparser.pl @@ -82,7 +82,7 @@ sub find_id_in_lines { ${$idx_ref} = $j; my $id = $1; - my $enum_name = 'MSG_' . uc $msg_name; + my $enum_name = 'GRCAN_' . uc $msg_name; $enum_name =~ s/\W+/_/gsmx; $enum_name =~ s/_+/_/gsmx; $enum_name =~ s/^_|_$//gsmx; diff --git a/Autogen/CAN/Src/STRUCTparser.pl b/Autogen/CAN/Src/STRUCTparser.pl index 59268493a..21a51a900 100644 --- a/Autogen/CAN/Src/STRUCTparser.pl +++ b/Autogen/CAN/Src/STRUCTparser.pl @@ -156,7 +156,9 @@ sub generate_header { if ($current_msg) { push @output, process_message( $current_msg, \@fields, $d_map, $prefix ); } - $current_msg = $msg_name; + + # Debug payload structs are intentionally excluded. + $current_msg = ( $msg_name =~ /^Debug(?:\s+(?:2[.]0|FD))?$/ismx ) ? $EMPTY_STR : $msg_name; @fields = (); } @@ -233,6 +235,7 @@ sub clean_field_name { sub process_message { my ( $name, $f_ref, $d_map, $prefix ) = @_; + my @buf; my $tag = uc $name =~ s/[^[:alpha:][:digit:]]/_/grsmx =~ s/_+/_/grsmx =~ s/^_|_$//grsmx; @@ -248,45 +251,72 @@ sub process_message { push @buf, "/** $name */\ntypedef struct {\n"; my @sorted = sort { $a <=> $b } keys %byte_map; + my %used_field_names; for my $i ( 0 .. $#sorted ) { - push @buf, process_byte_entry( $name, \@sorted, \%byte_map, \$i, $d_map ); + my %byte_ctx = ( + msg_name => $name, + sorted_ref => \@sorted, + map_ref => \%byte_map, + idx_ref => \$i, + d_map => $d_map, + seen_ref => \%used_field_names, + ); + push @buf, process_byte_entry( \%byte_ctx ); } push @buf, "} ${prefix}_${tag}_MSG;\n\n"; return join $EMPTY_STR, @buf; } sub process_byte_entry { - my ( $msg_name, $sorted_ref, $map_ref, $idx_ref, $d_map ) = @_; + my ($ctx_ref) = @_; + my $msg_name = $ctx_ref->{msg_name}; + my $sorted_ref = $ctx_ref->{sorted_ref}; + my $map_ref = $ctx_ref->{map_ref}; + my $idx_ref = $ctx_ref->{idx_ref}; + my $d_map = $ctx_ref->{d_map}; + my $seen_ref = $ctx_ref->{seen_ref}; my @out; my $b_idx = ${$sorted_ref}[ ${$idx_ref} ]; my $fields = ${$map_ref}{$b_idx}; if ( scalar @{$fields} > 2 && ${$fields}[0]->{name} =~ /reserved|ping|byte/ismx ) { - push @out, handle_multi_field_range( $sorted_ref, $map_ref, $idx_ref ); + push @out, handle_multi_field_range( $sorted_ref, $map_ref, $idx_ref, $seen_ref ); } else { - my $f_var = - ( scalar @{$fields} == 1 ) - ? clean_field_name( ${$fields}[0]->{name} ) - : join q{_}, map { clean_field_name( $_->{name} ) } @{$fields}; - if ( $f_var =~ /^[[:digit:]]/smx ) { - $f_var = q{_} . $f_var; + # For dense semantic bytes, emit each explicit CANdo field separately. + if ( scalar @{$fields} > 2 ) { + for my $f ( @{$fields} ) { + my $f_var = assign_unique_field_name( clean_field_name( $f->{name} ), $seen_ref ); + my $type = + ( $f->{type} =~ /32/smx ) ? 'uint32_t' + : ( $f->{type} =~ /16/smx ) ? 'uint16_t' + : 'uint8_t'; + my $desc = ${$d_map}{ $msg_name . q{::} . clean_field_name( $f->{name} ) } // "Byte $b_idx"; + push @out, sprintf "\t/** %s (Byte %d) */\n\t%-10s %-30s\n", $desc, $b_idx, $type, $f_var . q{;}; + } + } + else { + my $f_var = + ( scalar @{$fields} == 1 ) + ? clean_field_name( ${$fields}[0]->{name} ) + : join q{_}, map { clean_field_name( $_->{name} ) } @{$fields}; + $f_var = assign_unique_field_name( $f_var, $seen_ref ); + + my $type = + ( ${$fields}[0]->{type} =~ /32/smx ) ? 'uint32_t' + : ( ${$fields}[0]->{type} =~ /16/smx ) ? 'uint16_t' + : 'uint8_t'; + my $desc = join $SPACE_STR, map { ${$d_map}{ $msg_name . q{::} . clean_field_name( $_->{name} ) } // () } @{$fields}; + + push @out, sprintf "\t/** %s (Byte %d) */\n\t%-10s %-30s\n", ( $desc || "Byte $b_idx" ), $b_idx, $type, $f_var . q{;}; } - - my $type = - ( ${$fields}[0]->{type} =~ /32/smx ) ? 'uint32_t' - : ( ${$fields}[0]->{type} =~ /16/smx ) ? 'uint16_t' - : 'uint8_t'; - my $desc = join $SPACE_STR, map { ${$d_map}{ $msg_name . q{::} . clean_field_name( $_->{name} ) } // () } @{$fields}; - - push @out, sprintf "\t/** %s (Byte %d) */\n\t%-10s %-30s\n", ( $desc || "Byte $b_idx" ), $b_idx, $type, $f_var . q{;}; } return join $EMPTY_STR, @out; } sub handle_multi_field_range { - my ( $bytes_ref, $map_ref, $idx_ref ) = @_; + my ( $bytes_ref, $map_ref, $idx_ref, $seen_ref ) = @_; my $start_byte = ${$bytes_ref}[ ${$idx_ref} ]; my $has_error = grep { $_->{name} =~ /error|fault|violation/ismx } @{ ${$map_ref}{$start_byte} }; @@ -302,7 +332,27 @@ sub handle_multi_field_range { } my $len = ( ${$bytes_ref}[ ${$idx_ref} ] - $start_byte ) + 1; - my $v_name = $has_error ? 'error_fault_violation_bits' : 'ping_block'; - my $suffix = ( $len > 1 ) ? "[$len]" : $EMPTY_STR; + my $v_name = $has_error ? 'error_fault_violation_bits' : 'ping_block'; + $v_name = assign_unique_field_name( $v_name, $seen_ref ); + my $suffix = ( $len > 1 ) ? "[$len]" : $EMPTY_STR; return sprintf "\tuint8_t %s%s;\n", $v_name, $suffix; } + +sub assign_unique_field_name { + my ( $base_name, $seen_ref ) = @_; + my $name = $base_name // 'unknown_field'; + if ( $name =~ /^[[:digit:]]/smx ) { + $name = q{_} . $name; + } + if ( !$seen_ref->{$name} ) { + $seen_ref->{$name} = 1; + return $name; + } + my $idx = 1; + while ( $seen_ref->{ $name . q{_} . $idx } ) { + $idx++; + } + my $unique = $name . q{_} . $idx; + $seen_ref->{$unique} = 1; + return $unique; +} diff --git a/ECU/Application/Src/CANdler.c b/ECU/Application/Src/CANdler.c index 3ab7a4eff..223523428 100644 --- a/ECU/Application/Src/CANdler.c +++ b/ECU/Application/Src/CANdler.c @@ -31,7 +31,7 @@ void ECU_CAN_MessageHandler(ECU_StateData *state_data, GRCAN_BUS_ID bus_id, GRCA { switch (msg_id) { case GRCAN_DEBUG_2_0: - if (data_length > sizeof(GRCAN_DEBUG_2_0_MSG)) { + if (data_length > 8) { ReportBadMessageLength(bus_id, msg_id, sender_id); break; } @@ -39,7 +39,7 @@ void ECU_CAN_MessageHandler(ECU_StateData *state_data, GRCAN_BUS_ID bus_id, GRCA break; case GRCAN_DEBUG_FD: - if (data_length > sizeof(GRCAN_DEBUG_FD_MSG)) { + if (data_length > 64) { ReportBadMessageLength(bus_id, msg_id, sender_id); break; } diff --git a/Web/Changelog.md b/Web/Changelog.md new file mode 100644 index 000000000..2bb214f7f --- /dev/null +++ b/Web/Changelog.md @@ -0,0 +1,47 @@ +# Changelog + +## 2026-04-06 (patch 2) + +### Stricter topology enforcement across all entry points + +**Add Route form (`formRoutingAdd.js`) — save-time hard blocks:** +- Receiver field: if the typed node is not registered in GR ID → blocks with "Node does not exist" +- Receiver field: if the node exists but is not physically on the selected bus → blocks with `"" is not physically on ` +- Bus field: if the sender device exists and is not physically wired to the selected bus → blocks with `"" is not physically wired to ` (covers the case where a locked bus was manually set to an invalid value) + +**Add Bus form (`formBusAdd.js`) — highest-level guard:** +- Bus dropdown is now filtered to only show buses the device is physically wired to (same pattern as the route form's bus filter) +- Save handler hard-blocks with `"" is not physically wired to ` even if somehow an invalid bus is submitted + +All new blocks are skipped when `PhysicalTopology` isn't loaded, preserving graceful degradation. + +## 2026-04-06 (patch) + +### Fix: bus filter silently overriding locked bus in Add Route form +- When "Add Route" was opened from a locked-bus context (e.g. clicking add on an existing CAN3 block for DGPS), the topology bus filter excluded CAN3 from the option list. `makeSelect` received `"CAN3"` as the selected value with no matching option, so the browser defaulted to `"CAN1"`. The select was then disabled at the wrong bus, causing routes to be written to CAN1 instead of CAN3. +- Fix: bus filtering now only applies when `busPort` is not pre-provided (i.e. the user has free choice of bus). When the bus is already locked, all three options are always included so the disabled select renders the correct value. + +## 2026-04-06 + +### Physical CAN topology enforcement +- Added `can_topology.txt` — a human-editable plain-text file (CANdo-style indented format with `#` comments) that declares which nodes are physically wired to which CAN bus. Edit this file when hardware layout changes; no coding knowledge required. +- Added `physicalTopology.js` → `window.PhysicalTopology` — a fully self-contained module that owns all topology logic (fetch, parse, cache). Other files call only `isLoaded()`, `isOnBus()`, and `getNodesForBus()`; they have zero knowledge of internals. +- `formRoutingAdd.js`: receiver autocomplete dropdown is now filtered to only show nodes physically on the selected bus. If the user manually types a known node that isn't on the selected bus, an inline warning appears: `"" is not physically on .` +- `formRoutingAdd.js`: bus dropdown is filtered to only show buses the sender device is physically wired to (when device is pre-selected and topology is loaded). +- `candoDocument.js`: added V8 `PHYSICAL_BUS_VIOLATION` warning to `validate()` — flags existing routes where sender or receiver is not physically on the routed bus. Reported in the console on load alongside other violations. +- `Debugger` and `ALL` are always exempt from topology checks. +- All topology checks silently no-op if `can_topology.txt` fails to load (e.g. local file mode), so the UI degrades gracefully. + +## 2026-04-05 + +### Orange "Custom" chip for Custom CAN ID nodes +- Nodes with GR ID `"0x00"` (Charger, DTI Inverter, Energy Meter, IMD) now display an + orange **"Custom"** chip instead of the misleading purple `0x00` badge in the Nodes panel. +- Added `.item-accent-custom` CSS class in `viewer.css` with orange color scheme. +- Updated `appendNodeIdAccent()` in `viewer.js` to detect `"0x00"` and apply custom styling. + +### Prune unrouted messages at download +- Messages with no routing entries are automatically removed from the downloaded `.CANdo` file. +- Added `_getRoutedMessageNames()` helper and `pruneUnrouted` flag to `_serialize()` in `candoDocument.js`. +- `getSerializedText()` now calls `_serialize(true)` to prune on download; mutation path is unchanged. +- Added 5 tests covering Message ID pruning, Custom CAN ID pruning, ALL-receiver routes, count validation, and no-side-effect contract. diff --git a/Web/can_topology.txt b/Web/can_topology.txt new file mode 100644 index 000000000..087bcb1c9 --- /dev/null +++ b/Web/can_topology.txt @@ -0,0 +1,36 @@ +# Physical CAN bus topology. +# Edit this file when hardware layout changes. +# Node names must exactly match GR ID entries in GRCAN.CANdo. +# "Debugger" and "ALL" are always exempt — do not list them here. +# A node can appear on multiple buses. +# Lines starting with # are comments. Blank lines are ignored. + +CAN1: + ECU + TCM + BCU + DGPS + # Both inverters share CAN1. Whichever isn't physically connected + # has its messages go nowhere — no firmware switch needed. + GR Inverter + DTI Inverter + Fan Controller 1 + Fan Controller 2 + Fan Controller 3 + Energy Meter + Dash Panel + +CAN2: + ECU + TCM + BCU + DGPS + # Add digital sensor nodes here as they are registered in GR ID + +CAN3: + CCU + BCU + Charger + IMD + Energy Meter + Charging SDC diff --git a/Web/candoDocument.js b/Web/candoDocument.js index 65df8b27b..4ddbf304e 100644 --- a/Web/candoDocument.js +++ b/Web/candoDocument.js @@ -373,9 +373,10 @@ return out; } - function _serializeMessageIds() { + function _serializeMessageIds(routedOnly = null) { let out = "Message ID:\n"; for (const msg of _messageIds.values()) { + if (routedOnly && !routedOnly.has(msg.name)) continue; out += " " + msg.name + ":\n"; out += " MSG ID: " + msg.msgId + "\n"; out += " MSG LENGTH: " + msg.msgLength + "\n"; @@ -409,9 +410,10 @@ return out; } - function _serializeCustomCanIds() { + function _serializeCustomCanIds(routedOnly = null) { let out = "Custom CAN ID:\n"; for (const entry of _customCanIds.values()) { + if (routedOnly && !routedOnly.has(entry.name)) continue; out += " " + entry.name + ":\n"; out += " CAN ID: " + entry.canId + "\n"; out += " Length: " + entry.length + "\n"; @@ -448,13 +450,28 @@ return out; } - function _serialize() { + function _getRoutedMessageNames() { + const routed = new Set(); + for (const device of _devices.values()) { + for (const bus of device.buses.values()) { + for (const receiver of bus.receivers.values()) { + for (const route of receiver.routes) { + routed.add(route.msgName); + } + } + } + } + return routed; + } + + function _serialize(pruneUnrouted = false) { + const routedOnly = pruneUnrouted ? _getRoutedMessageNames() : null; const parts = [ _busIdsText, _serializeRouting(), _byteOrderText, - _serializeMessageIds(), - _serializeCustomCanIds(), + _serializeMessageIds(routedOnly), + _serializeCustomCanIds(routedOnly), _serializeGrIds(), ]; // Strip trailing newlines from each part, join with exactly one blank line, @@ -590,6 +607,42 @@ } } + // V8: PHYSICAL_BUS_VIOLATION + // Only runs when PhysicalTopology has successfully loaded can_topology.txt. + const _topo = (typeof window !== "undefined" ? window : {}) + .PhysicalTopology; + if (_topo && _topo.isLoaded()) { + for (const device of _devices.values()) { + for (const bus of device.buses.values()) { + if (!_topo.isOnBus(device.deviceName, bus.busPort)) { + results.push({ + severity: "warning", + code: "PHYSICAL_BUS_VIOLATION", + message: `Device "${device.deviceName}" is not physically wired to ${bus.busPort}`, + context: { + device: device.deviceName, + bus: bus.busPort, + }, + }); + } + for (const receiverName of bus.receivers.keys()) { + if (!_topo.isOnBus(receiverName, bus.busPort)) { + results.push({ + severity: "warning", + code: "PHYSICAL_BUS_VIOLATION", + message: `Receiver "${receiverName}" in "${device.deviceName}" > ${bus.busPort} is not physically on ${bus.busPort}`, + context: { + device: device.deviceName, + bus: bus.busPort, + receiver: receiverName, + }, + }); + } + } + } + } + } + return results; } @@ -623,7 +676,16 @@ _parse(editor.getRawText()); const result = fn(); if (result.ok !== false) { - editor.updateRawText(_serialize()); + const newText = _serialize(); + editor.updateRawText(newText); + // If the post-mutation canonical text matches the canonical original, + // the user has undone all their changes — clear the changed indicators. + _parse(editor.getOriginalRawText()); + const canonicalOriginal = _serialize(); + _parse(newText); // restore internal state to current + if (newText === canonicalOriginal) { + editor.resetEditState(); + } } return result; } @@ -1259,13 +1321,24 @@ return _serialize(); } + function _serializeFromStatePruned() { + return _serialize(true); + } + // Returns the canonical serialized form of the current editor text. // Parse → serialize without side effects (does not update editor state). function getSerializedText() { const editor = _getEditor(); if (!editor) return null; _parse(editor.getRawText()); - return _serialize(); + return _serialize(true); + } + + // Parse an arbitrary raw text and serialize it with pruning — used to compute + // the canonical download form of any snapshot (e.g. the original file). + function getSerializedTextFrom(rawText) { + _parse(rawText || ""); + return _serialize(true); } // ==================== Public API ==================== @@ -1301,8 +1374,10 @@ getRouteReceivers, getGraphDataForBus, getSerializedText, + getSerializedTextFrom, // Test hooks _parseForTest, _serializeFromState, + _serializeFromStatePruned, }; }); diff --git a/Web/editor.css b/Web/editor.css index 772ab29ad..28a8a5715 100644 --- a/Web/editor.css +++ b/Web/editor.css @@ -191,6 +191,50 @@ vertical-align: middle; } +.download-notice { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + z-index: 2300; + display: flex; + align-items: center; + gap: 8px; + background: #1e293b; + color: #e2e8f0; + border: 1px solid #334155; + border-left: 4px solid #38bdf8; + border-radius: 6px; + padding: 10px 14px; + font-size: 13px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + max-width: 480px; + pointer-events: all; +} + +.download-notice-icon { + flex-shrink: 0; +} + +.download-notice-msg { + flex: 1; +} + +.download-notice-close { + flex-shrink: 0; + background: none; + border: none; + color: #94a3b8; + cursor: pointer; + font-size: 16px; + line-height: 1; + padding: 0 2px; +} + +.download-notice-close:hover { + color: #e2e8f0; +} + .dv-overlay { position: fixed; inset: 0; diff --git a/Web/editor.js b/Web/editor.js index 126e73153..b200e7ec1 100644 --- a/Web/editor.js +++ b/Web/editor.js @@ -155,6 +155,12 @@ return !!key && newKeys.has(key); } + function resetEditState() { + hasEdits = false; + editedKeys.clear(); + newKeys.clear(); + } + function deleteLineRange(startLine, endLine) { spliceLines(startLine, endLine - startLine, null); } @@ -468,6 +474,7 @@ isEdited, markNew, isNew, + resetEditState, downloadCando, triggerReRender, createEditBtn, diff --git a/Web/formBusAdd.js b/Web/formBusAdd.js index 85cc4cfaf..3aa9da957 100644 --- a/Web/formBusAdd.js +++ b/Web/formBusAdd.js @@ -20,9 +20,20 @@ nodeF.input.disabled = true; body.appendChild(nodeF.row); + // Filter the bus dropdown to only buses the device is physically wired to. + // If the topology file isn't loaded yet, all three buses are shown. + const _allBuses = ["CAN1", "CAN2", "CAN3"]; + const _topo = window.PhysicalTopology; + const _busChoices = + deviceName && _topo && _topo.isLoaded() + ? _allBuses.filter((b) => _topo.isOnBus(deviceName, b)) + : _allBuses; const busF = fu.makeFormRow( "Bus", - fu.makeSelect(["CAN1", "CAN2", "CAN3"], "CAN1"), + fu.makeSelect( + _busChoices.length > 0 ? _busChoices : _allBuses, + _busChoices[0] || "CAN1", + ), true, ); body.appendChild(busF.row); @@ -35,6 +46,14 @@ saveBtn.addEventListener("click", () => { const bus = busF.input.value; + + // Hard-block: device must be physically wired to the selected bus. + const topo = window.PhysicalTopology; + if (topo && topo.isLoaded() && !topo.isOnBus(deviceName, bus)) { + busF.error.textContent = `"${deviceName}" is not physically wired to ${bus}`; + return; + } + const result = window.GrcanDocument.addBus(deviceName, bus); if (!result.ok) { busF.error.textContent = result.error; diff --git a/Web/formRoutingAdd.js b/Web/formRoutingAdd.js index ea76aa91d..03c275ab5 100644 --- a/Web/formRoutingAdd.js +++ b/Web/formRoutingAdd.js @@ -18,7 +18,8 @@ // Current routing usage is still derived from in-memory text for unused-first ranking. let allMessageNames = []; const hasRoute = new Map(); - let receiverList = []; + let receiverList = []; // topology-filtered; drives the autocomplete dropdown + let _fullReceiverList = []; // all known nodes pre-filter; used for inline topology warning function routingSectionBounds(lines) { const routingStart = lines.findIndex((l) => l.startsWith("routing:")); @@ -108,7 +109,15 @@ const baseReceivers = currentBusPort ? [...routingNames] : [...new Set([...nodes, ...routingNames])]; - receiverList = [...new Set(baseReceivers)].sort((a, b) => + // Store the full pre-filter list for inline topology warning checks. + _fullReceiverList = [...new Set(baseReceivers)]; + // Apply physical topology filter: only show nodes physically on the selected bus. + const _topo = window.PhysicalTopology; + const filteredReceivers = + currentBusPort && _topo && _topo.isLoaded() + ? baseReceivers.filter((name) => _topo.isOnBus(name, currentBusPort)) + : baseReceivers; + receiverList = [...new Set(filteredReceivers)].sort((a, b) => a.localeCompare(b), ); if (!receiverList.includes("ALL")) receiverList.unshift("ALL"); @@ -141,9 +150,22 @@ // Run once on open in case a device name was pre-filled. if (!devF.input.disabled) updateGrIdVisibility(); + // Filter available buses to only those the device is physically wired to. + // Only applies when bus is NOT already locked (busPort provided) — if the + // bus is pre-selected, the select is disabled anyway, so filtering its + // option list would cause the locked value to have no matching option and + // the browser would silently default to the first entry (wrong bus). + const _allBuses = ["CAN1", "CAN2", "CAN3"]; + const _topoForBus = window.PhysicalTopology; + const _availableBuses = + !busPort && deviceName && _topoForBus && _topoForBus.isLoaded() + ? _allBuses.filter((b) => _topoForBus.isOnBus(deviceName, b)) + : _allBuses; + const _busChoices = + _availableBuses.length > 0 ? _availableBuses : _allBuses; const busF = fu.makeFormRow( "Bus", - fu.makeSelect(["CAN1", "CAN2", "CAN3"], busPort || "CAN1"), + fu.makeSelect(_busChoices, busPort || _busChoices[0] || "CAN1"), true, ); if (busPort) { @@ -215,8 +237,26 @@ recSuggestBox.classList.remove("hidden"); } + // Warn (but do not block) when the user manually types a node that is + // recognized but not physically wired to the selected bus. + function checkReceiverTopology() { + const val = recF.input.value.trim(); + const currentBus = busF.input.value || null; + const topo = window.PhysicalTopology; + if (!val || !currentBus || !topo || !topo.isLoaded()) { + recF.error.textContent = ""; + return; + } + if (_fullReceiverList.includes(val) && !topo.isOnBus(val, currentBus)) { + recF.error.textContent = `"${val}" is not physically on ${currentBus}.`; + } else { + recF.error.textContent = ""; + } + } + recF.input.addEventListener("input", () => { renderReceiverSuggestions(recF.input.value); + checkReceiverTopology(); }); recF.input.addEventListener("focus", () => { renderReceiverSuggestions(recF.input.value); @@ -407,11 +447,41 @@ } else devF.error.textContent = ""; const bus = busF.input.value; + + // Check that the sender device is physically wired to the selected bus. + // Only enforced for existing devices — new devices aren't in the topology file yet. + const _isExistingDev = + dev && window.GrcanDocument && window.GrcanDocument.deviceExists(dev); + const _topo = window.PhysicalTopology; + if ( + _isExistingDev && + _topo && + _topo.isLoaded() && + !_topo.isOnBus(dev, bus) + ) { + busF.error.textContent = `"${dev}" is not physically wired to ${bus}`; + ok = false; + } else { + busF.error.textContent = ""; + } + const rec = recF.input.value.trim(); if (!rec) { recF.error.textContent = "Required"; ok = false; - } else recF.error.textContent = ""; + } else if ( + window.GrcanDocument && + !window.GrcanDocument.grIdExists(rec) + ) { + // Receiver must be registered in GR ID before it can be used as a route target. + recF.error.textContent = "Node does not exist"; + ok = false; + } else if (_topo && _topo.isLoaded() && !_topo.isOnBus(rec, bus)) { + recF.error.textContent = `"${rec}" is not physically on ${bus}`; + ok = false; + } else { + recF.error.textContent = ""; + } const msg = msgF.input.value.trim(); if (!msg) { diff --git a/Web/index.html b/Web/index.html index d06054f8b..d2ec4fb14 100644 --- a/Web/index.html +++ b/Web/index.html @@ -10,8 +10,6 @@ -
Not an official GR tool.
-
@@ -106,6 +104,7 @@

GRCAN Viewer

diffViewer.js → DiffViewer (standalone diff modal) viewer.js → main controller (rendering + edit-mode wiring) background.js → decorative canvas (no dependencies) --> + @@ -120,7 +119,7 @@

GRCAN Viewer

- + diff --git a/Web/physicalTopology.js b/Web/physicalTopology.js new file mode 100644 index 000000000..070c541d9 --- /dev/null +++ b/Web/physicalTopology.js @@ -0,0 +1,79 @@ +// Purpose: Physical CAN bus topology enforcement module. +// Loads and parses Web/can_topology.txt — the human-editable source of truth +// for which devices are physically wired to which CAN bus. +// All exemption logic (Debugger, ALL) lives here and nowhere else. +// No other file knows about storage, parsing, or fetch internals. +// Callers use only the four public methods below. +// Exposed as: window.PhysicalTopology + +(function () { + "use strict"; + + // Nodes that are always allowed on any bus — never topology-checked. + const _EXEMPT = new Set(["Debugger", "ALL"]); + + // Map> + let _topology = new Map(); + let _loaded = false; + + // ==================== Parser ==================== + // Pure function: text → Map> + + function _parse(text) { + const result = new Map(); + let currentBus = null; + for (const raw of String(text || "").split("\n")) { + const line = raw.replace(/#.*$/, "").trimEnd(); // strip inline comments + if (!line.trim()) continue; + const busMatch = line.match(/^(CAN\d+)\s*:/); + if (busMatch) { + currentBus = busMatch[1]; + result.set(currentBus, new Set()); + } else if (currentBus && /^\s+\S/.test(line)) { + result.get(currentBus).add(line.trim()); + } + } + return result; + } + + // ==================== Public API ==================== + + window.PhysicalTopology = { + // Fetch and parse can_topology.txt. Call once at startup. + // Resolves even on failure — isLoaded() will return false in that case. + load: async function () { + try { + const resp = await fetch("can_topology.txt"); + if (!resp.ok) return; + const text = await resp.text(); + _topology = _parse(text); + _loaded = true; + } catch (_) { + // Silently no-op: fetch not available (e.g. file:// local mode). + } + }, + + // True only after a successful load(). + isLoaded: function () { + return _loaded; + }, + + // Is nodeName physically wired to busPort? + // Always returns true for exempt nodes (Debugger, ALL) or if not loaded. + isOnBus: function (nodeName, busPort) { + if (!_loaded) return true; + if (_EXEMPT.has(nodeName)) return true; + const busSet = _topology.get(busPort); + if (!busSet) return false; + return busSet.has(nodeName); + }, + + // All node names registered for busPort in the topology file. + // Returns [] if not loaded or bus unknown. + getNodesForBus: function (busPort) { + if (!_loaded) return []; + const busSet = _topology.get(busPort); + return busSet ? [...busSet] : []; + }, + }; +})(); diff --git a/Web/viewer.css b/Web/viewer.css index fe8db5284..ab40626b2 100644 --- a/Web/viewer.css +++ b/Web/viewer.css @@ -128,6 +128,12 @@ margin-left: 4px; } +.item-accent-custom { + color: #fdba74; + background: rgba(194, 65, 12, 0.25); + border-color: rgba(251, 146, 60, 0.35); +} + .panel-item.active .item-chevron { color: #8b5cf6; } diff --git a/Web/viewer.js b/Web/viewer.js index a093ecdc3..fda7ea0fc 100644 --- a/Web/viewer.js +++ b/Web/viewer.js @@ -20,6 +20,25 @@ window.addEventListener("DOMContentLoaded", function () { const searchInput = document.getElementById("viewer-search"); let nodeIdMap = new Map(); let currentRef = ""; + const requestedQueryKey = (() => { + try { + const params = new URLSearchParams(window.location.search); + if (params.has("ref")) return "ref"; + if (params.has("branch")) return "branch"; + return "ref"; + } catch (_err) { + return "ref"; + } + })(); + const requestedRefFromUrl = (() => { + try { + const params = new URLSearchParams(window.location.search); + const ref = params.get("ref") || params.get("branch"); + return ref ? ref.trim() : ""; + } catch (_err) { + return ""; + } + })(); let _allNodes = []; // persisted node→bus→messages index for search let _searchDropdown = null; @@ -63,6 +82,31 @@ window.addEventListener("DOMContentLoaded", function () { container.appendChild(d); } + function updateLocationState(ref) { + const url = new URL(window.location.href); + const isCustomFile = !!window.GrcanApi.isLocalMode(); + const hasEdits = + !isCustomFile && + !!editor && + !!editor.hasUnsavedEdits && + editor.hasUnsavedEdits(); + + if (isCustomFile) { + url.search = ""; + url.hash = "custom"; + } else { + if (ref) { + url.searchParams.set(requestedQueryKey, ref); + } else { + url.searchParams.delete("ref"); + url.searchParams.delete("branch"); + } + url.hash = hasEdits ? "edited" : ""; + } + + window.history.replaceState(null, "", url); + } + function messageChangeState(msgName, deviceName, busCanonical) { const busPort = busCanonical ? window.GrcanApi.busToPort(busCanonical) @@ -113,6 +157,26 @@ window.addEventListener("DOMContentLoaded", function () { el.innerHTML = `${text}`; } + function showDownloadNotice(message) { + const existing = document.getElementById("download-notice"); + if (existing) existing.remove(); + const notice = document.createElement("div"); + notice.id = "download-notice"; + notice.className = "download-notice"; + notice.innerHTML = + '\u2139\ufe0f' + + '' + + ''; + notice.querySelector(".download-notice-msg").textContent = message; + notice + .querySelector(".download-notice-close") + .addEventListener("click", () => notice.remove()); + document.body.appendChild(notice); + setTimeout(() => { + if (notice.parentNode) notice.remove(); + }, 5000); + } + function makeItem(labelText, hasChevron) { const item = document.createElement("div"); item.className = "panel-item"; @@ -143,9 +207,12 @@ window.addEventListener("DOMContentLoaded", function () { function appendNodeIdAccent(item, nodeName) { const nodeId = nodeIdForName(nodeName); if (!nodeId) return; + const isCustom = nodeId === "0x00"; const accent = document.createElement("span"); - accent.className = "item-accent"; - accent.textContent = nodeId; + accent.className = isCustom + ? "item-accent item-accent-custom" + : "item-accent"; + accent.textContent = isCustom ? "Custom" : nodeId; const chev = item.querySelector(".item-chevron"); if (chev) { item.insertBefore(accent, chev); @@ -369,17 +436,36 @@ window.addEventListener("DOMContentLoaded", function () { }); dlBtn.addEventListener("click", function () { - const oldText = editor.getOriginalRawText + const doc = window.GrcanDocument; + const origRaw = editor.getOriginalRawText ? editor.getOriginalRawText() : ""; - const newText = editor.getRawText ? editor.getRawText() : ""; - if (!window.DiffViewer || oldText === newText) { + const origDownload = doc ? doc.getSerializedTextFrom(origRaw) : origRaw; + const newDownload = doc + ? doc.getSerializedText() + : editor.getRawText + ? editor.getRawText() + : ""; + if (origDownload === newDownload) { + // Download content unchanged — check if there are unsaved working changes + // (e.g. unrouted message definitions) and surface a notice. + const rawChanged = editor.getRawText && editor.getRawText() !== origRaw; + if (rawChanged) { + showDownloadNotice( + "Nothing new to export \u2014 message definitions without routes are excluded. Add a route to include them.", + ); + } else { + editor.downloadCando(); + } + return; + } + if (!window.DiffViewer) { editor.downloadCando(); return; } window.DiffViewer.show({ - oldText, - newText, + oldText: origDownload, + newText: newDownload, onConfirm: function () { editor.downloadCando(); }, @@ -1113,6 +1199,7 @@ window.addEventListener("DOMContentLoaded", function () { await renderBusNode(null, text); } restoreSelection(snapshot || navSnapshot()); + updateLocationState(currentRef); } if (editor) { @@ -1142,14 +1229,20 @@ window.addEventListener("DOMContentLoaded", function () { "You have unsaved changes. Download your changes before switching reference?", ); if (wantsDownload) { - const oldText = editor.getOriginalRawText + const doc = window.GrcanDocument; + const origRaw = editor.getOriginalRawText ? editor.getOriginalRawText() : ""; - const newText = editor.getRawText ? editor.getRawText() : ""; - if (window.DiffViewer && oldText !== newText) { + const origDownload = doc ? doc.getSerializedTextFrom(origRaw) : origRaw; + const newDownload = doc + ? doc.getSerializedText() + : editor.getRawText + ? editor.getRawText() + : ""; + if (window.DiffViewer && origDownload !== newDownload) { window.DiffViewer.show({ - oldText, - newText, + oldText: origDownload, + newText: newDownload, onConfirm: function () { editor.downloadCando(); }, @@ -1179,6 +1272,7 @@ window.addEventListener("DOMContentLoaded", function () { } await renderHierarchy(ref); currentRef = ref; + updateLocationState(currentRef); if (typeof window.regenerateAndDrawBg === "function") { window.regenerateAndDrawBg(); } @@ -1188,6 +1282,8 @@ window.addEventListener("DOMContentLoaded", function () { setHierarchyHeaders(); wireEditModeButtons(); setPlaceholder(firstList, "Loading..."); + // Load physical topology in the background; non-blocking. + if (window.PhysicalTopology) window.PhysicalTopology.load(); const [branches, tags] = await Promise.all([ window.GrcanApi.fetchBranches(), @@ -1210,12 +1306,21 @@ window.addEventListener("DOMContentLoaded", function () { refSelect.appendChild(opt); }); - if (branches.includes("main")) { - refSelect.value = "main"; - await renderHierarchy("main"); - currentRef = "main"; + const availableRefs = new Set([...branches, ...tags]); + const initialRef = availableRefs.has(requestedRefFromUrl) + ? requestedRefFromUrl + : branches.includes("main") + ? "main" + : ""; + + if (initialRef) { + refSelect.value = initialRef; + await renderHierarchy(initialRef); + currentRef = initialRef; + updateLocationState(currentRef); } else { setPlaceholder(firstList, "Select a ref"); + updateLocationState(""); } } @@ -1235,6 +1340,7 @@ window.addEventListener("DOMContentLoaded", function () { localFileInput.value = ""; window.GrcanApi.setLocalCandoText(null); refSelect.disabled = false; + updateLocationState(currentRef); if (currentRef) renderHierarchy(currentRef); } }); @@ -1246,6 +1352,7 @@ window.addEventListener("DOMContentLoaded", function () { localFileInput.style.display = "none"; window.GrcanApi.setLocalCandoText(null); refSelect.disabled = false; + updateLocationState(currentRef); return; } const reader = new FileReader(); @@ -1253,6 +1360,7 @@ window.addEventListener("DOMContentLoaded", function () { window.GrcanApi.setLocalCandoText(e.target.result); refSelect.disabled = true; renderHierarchy(currentRef || "local"); + updateLocationState(currentRef); }; reader.readAsText(file); }); @@ -1260,6 +1368,9 @@ window.addEventListener("DOMContentLoaded", function () { localFileInput.addEventListener("cancel", function () { localToggle.checked = false; localFileInput.style.display = "none"; + window.GrcanApi.setLocalCandoText(null); + refSelect.disabled = false; + updateLocationState(currentRef); }); }