Skip to main content

haptic_skin_firmware/
main.rs

1#![no_std]
2#![no_main]
3
4use defmt::*;
5use embassy_executor::Spawner;
6use embassy_futures::join::join;
7use embassy_futures::select::{select, Either};
8use embassy_rp::bind_interrupts;
9use embassy_rp::gpio::{Level, Output};
10use embassy_rp::peripherals::USB;
11use embassy_rp::pwm::{Config as PwmConfig, Pwm};
12use embassy_rp::usb::{Driver, InterruptHandler};
13use embassy_time::{Duration, Timer};
14use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
15use embassy_usb::{Builder, Config};
16use {defmt_rtt as _, panic_probe as _};
17
18mod config;
19mod drivers;
20mod proto;
21mod tasks;
22
23use config::{
24    MOTOR_COUNT, MOTOR_MAX_DUTY, PWM_TOP, STATUS_LED_PIN_DESC, USB_MANUFACTURER, USB_PID,
25    USB_PRODUCT, USB_VID,
26};
27use drivers::motors::Motors;
28use drivers::status_led::{self, Status};
29use proto::{Command, Event, FrameParser};
30use tasks::power_guard;
31
32bind_interrupts!(struct Irqs {
33    USBCTRL_IRQ => InterruptHandler<USB>;
34});
35
36/// Config PWM de base : ~20 kHz, slice activé, duty 0 (moteur éteint au boot).
37fn pwm_cfg() -> PwmConfig {
38    let mut c = PwmConfig::default();
39    c.top = PWM_TOP; // fréquence = 125 MHz / (TOP + 1) ≈ 20 kHz
40    c.enable = true; // sans ça le slice ne génère aucun signal (défaut = false)
41    c
42}
43
44/// Une passe de self-test : chaque moteur monte en rampe puis s'éteint.
45/// Un seul moteur actif à la fois → toujours sous le plafond 500 mA USB.
46async fn self_test(motors: &mut Motors<'_>) {
47    info!("self-test : balayage des {} moteurs", MOTOR_COUNT);
48    for m in 0..MOTOR_COUNT {
49        for duty in (0..=MOTOR_MAX_DUTY).step_by(5) {
50            motors.set(m, duty);
51            Timer::after(Duration::from_millis(4)).await;
52        }
53        motors.set(m, 0);
54        Timer::after(Duration::from_millis(80)).await;
55    }
56}
57
58#[embassy_executor::main]
59async fn main(spawner: Spawner) {
60    let p = embassy_rp::init(Default::default());
61    info!("HAPTIC.SKIN — boot, {} moteurs PWM @ ~20 kHz", MOTOR_COUNT);
62
63    // --- LED de statut (LED externe, cf. config::STATUS_LED_PIN_DESC) ---
64    // Tâche dédiée pilotée par Signal → ne bloque jamais la boucle de commandes.
65    info!("LED de statut sur {}", STATUS_LED_PIN_DESC);
66    let status_led = Output::new(p.PIN_16, Level::Low);
67    spawner.must_spawn(status_led::run(status_led));
68    status_led::set(Status::Boot); // clignote pendant le self-test
69
70    // --- 8 moteurs = 8 sorties PWM indépendantes ---
71    // Sur RP2040, un GPIO pair est le canal A de la slice (gpio / 2). On prend
72    // GP0,2,4,6,8,10,12,14 → canal A des slices 0..7 = 8 duty indépendants.
73    // (On évite GP23/24/25/29, réservés au chip WiFi CYW43 sur le Pico W.)
74    let mut motors = Motors::new([
75        Pwm::new_output_a(p.PWM_SLICE0, p.PIN_0, pwm_cfg()),
76        Pwm::new_output_a(p.PWM_SLICE1, p.PIN_2, pwm_cfg()),
77        Pwm::new_output_a(p.PWM_SLICE2, p.PIN_4, pwm_cfg()),
78        Pwm::new_output_a(p.PWM_SLICE3, p.PIN_6, pwm_cfg()),
79        Pwm::new_output_a(p.PWM_SLICE4, p.PIN_8, pwm_cfg()),
80        Pwm::new_output_a(p.PWM_SLICE5, p.PIN_10, pwm_cfg()),
81        Pwm::new_output_a(p.PWM_SLICE6, p.PIN_12, pwm_cfg()),
82        Pwm::new_output_a(p.PWM_SLICE7, p.PIN_14, pwm_cfg()),
83    ]);
84
85    // Validation visuelle au boot, avant de passer sous contrôle de l'hôte.
86    self_test(&mut motors).await;
87    status_led::set(Status::Idle); // self-test fini, on attend l'hôte USB
88
89    // --- USB CDC-ACM : on parle le protocole binaire avec l'hôte ---
90    let driver = Driver::new(p.USB, Irqs);
91
92    let mut usb_config = Config::new(USB_VID, USB_PID);
93    usb_config.manufacturer = Some(USB_MANUFACTURER);
94    usb_config.product = Some(USB_PRODUCT);
95    usb_config.serial_number = Some("0001");
96    usb_config.max_power = 250; // mA déclarés (on plafonne réellement via power_guard)
97    usb_config.max_packet_size_0 = 64;
98
99    // Buffers requis par le stack USB. Ils vivent sur la pile de `main`, qui ne
100    // retourne jamais (le `join` final ne se résout pas) → pas besoin de StaticCell.
101    let mut config_descriptor = [0u8; 256];
102    let mut bos_descriptor = [0u8; 16];
103    let mut msos_descriptor: [u8; 0] = [];
104    let mut control_buf = [0u8; 64];
105    let mut state = State::new();
106
107    let mut builder = Builder::new(
108        driver,
109        usb_config,
110        &mut config_descriptor,
111        &mut bos_descriptor,
112        &mut msos_descriptor,
113        &mut control_buf,
114    );
115
116    let mut class = CdcAcmClass::new(&mut builder, &mut state, 64);
117    let mut usb = builder.build();
118
119    // Le device USB tourne en parallèle de notre boucle de commandes.
120    let usb_fut = usb.run();
121
122    // Deadman de sécurité : sur un objet porté au cou, un moteur ne doit JAMAIS
123    // rester bloqué ON. Si l'hôte cesse de nous parler pendant COMMAND_TIMEOUT
124    // (crash / fermeture du port sans déconnexion USB), on coupe tout. Et on
125    // borne chaque écriture (WRITE_TIMEOUT) pour qu'un hôte qui ne lit plus ne
126    // fige pas la boucle (sinon : plus aucune réponse au ping → « no reply »).
127    const COMMAND_TIMEOUT: Duration = Duration::from_millis(1500);
128    const WRITE_TIMEOUT: Duration = Duration::from_millis(200);
129
130    let comms_fut = async {
131        let mut parser = FrameParser::new();
132        let mut buf = [0u8; 64];
133        // Intensités *demandées* par l'hôte (avant power-guard), pour les 8 moteurs.
134        // On garde cet état pour pouvoir réappliquer le power-guard sur l'ensemble
135        // du collier à CHAQUE commande, y compris les SetMotor unitaires : sans ça,
136        // 8 SetMotor successifs contourneraient le plafond « 6 moteurs simultanés ».
137        let mut requested = [0u8; MOTOR_COUNT];
138        loop {
139            class.wait_connection().await;
140            info!("hôte connecté");
141            status_led::set(Status::Connected);
142            loop {
143                match select(class.read_packet(&mut buf), Timer::after(COMMAND_TIMEOUT)).await {
144                    Either::First(Ok(n)) => {
145                        for &byte in &buf[..n] {
146                            match parser.push(byte) {
147                                Ok(Some(cmd)) => match cmd {
148                                    Command::Ping => {
149                                        let mut out = [0u8; 4];
150                                        let len = proto::encode_event(Event::Pong, &[], &mut out);
151                                        let _ = select(
152                                            class.write_packet(&out[..len]),
153                                            Timer::after(WRITE_TIMEOUT),
154                                        )
155                                        .await;
156                                    }
157                                    Command::SetMotor { idx, intensity } => {
158                                        if (idx as usize) < MOTOR_COUNT {
159                                            requested[idx as usize] = intensity;
160                                            let mut guarded = requested;
161                                            power_guard::apply(&mut guarded);
162                                            motors.set_all(&guarded);
163                                        }
164                                    }
165                                    Command::SetAll(vals) => {
166                                        requested = vals;
167                                        let mut guarded = requested;
168                                        power_guard::apply(&mut guarded);
169                                        motors.set_all(&guarded);
170                                    }
171                                    Command::StopAll => {
172                                        requested = [0u8; MOTOR_COUNT];
173                                        motors.stop_all();
174                                    }
175                                },
176                                Ok(None) => {}
177                                Err(e) => {
178                                    warn!("trame invalide : {:?}", e);
179                                    status_led::set(Status::Error);
180                                    // on prévient l'hôte : 0xBB Error [code] xor
181                                    let mut out = [0u8; 5];
182                                    let len = proto::encode_event(Event::Error, &[e.code()], &mut out);
183                                    let _ = select(
184                                        class.write_packet(&out[..len]),
185                                        Timer::after(WRITE_TIMEOUT),
186                                    )
187                                    .await;
188                                }
189                            }
190                        }
191                    }
192                    Either::First(Err(_)) => break, // hôte déconnecté (USB)
193                    Either::Second(_) => {
194                        // silence prolongé de l'hôte → arrêt de sécurité (on continue d'écouter)
195                        requested = [0u8; MOTOR_COUNT];
196                        motors.stop_all();
197                    }
198                }
199            }
200            info!("hôte déconnecté → arrêt moteurs");
201            requested = [0u8; MOTOR_COUNT];
202            motors.stop_all();
203            status_led::set(Status::Idle);
204        }
205    };
206
207    join(usb_fut, comms_fut).await;
208}