Ma HP Prime passe le plus clair de son temps à faire des maths — mais c’est aussi, sous le capot, un petit ordinateur avec un port USB et un vrai langage de programmation (le PPL, HP Prime Programming Language). Et depuis quelques versions de firmware, elle sait causer en USB-HID brut avec n’importe quel périphérique USB, sans passer par un protocole calculatrice propriétaire. L’idée du projet : transformer un Arduino Leonardo en passerelle générique — I/O numériques, entrées analogiques, bus I2C — et piloter le tout directement depuis un programme PPL sur la calculatrice, capteur d’humidité à la clé.
L’idée de départ n’est pas sortie de nulle part : c’est Mark Power qui l’a présentée au HPCC 2023 (HP Calculator Conference), avec sa propre version du pont USB-HID basée sur une puce FT260 (un convertisseur USB vers I2C/UART d’FTDI). Cette présentation m’a donné envie de relever le défi à ma façon, avec le matériel que j’avais sous la main — un Arduino Leonardo plutôt qu’un FT260 — en m’appuyant sur des informations trouvées sur le forum HP Museum pour la partie Arduino.
Pourquoi la HP Prime peut parler USB-HID
Sur la HP Prime G2, depuis la version de firmware 14603 (décembre 2021), trois fonctions PPL donnent un accès direct au port USB : USBOpen(VID, PID), USBSend(trame) et USBRecv(). Rien de spécifique à un protocole calculatrice : on ouvre un périphérique par son identifiant VID:PID comme n’importe quel hôte USB-HID, on lui envoie des octets, on en reçoit. Autant dire que c’est resté assez peu exploité — la documentation officielle sur le sujet est maigre, et la plupart des exemples qui circulent n’ont jamais été confirmés sur du matériel réel.
De l’autre côté, un Arduino Leonardo est un choix presque évident : son microcontrôleur (ATmega32U4) gère l’USB nativement en matériel, ce qui permet de l’énumérer comme un vrai périphérique HID générique — contrairement à un Uno ou un Nano classique, qui ont besoin d’une puce USB-série séparée et ne peuvent pas facilement se faire passer pour autre chose qu’un port COM.
Le protocole : des rapports HID de 64 octets
Le sketch Arduino s’appuie sur la bibliothèque HID-Project (NicoHood) et son mode RawHID, qui déclare des rapports de 64 octets dans les deux sens. Le protocole maison tient sur un octet de commande suivi de quelques paramètres :
0x00 PING— renvoie la trame telle quelle, pour vérifier que la liaison répond.0x01 READ_ANALOG/0x02 READ_DIGITAL/0x03 WRITE_DIGITAL/0x04 PIN_MODE— l’essentiel des entrées-sorties classiques.0x05 I2C_READ/0x06 I2C_WRITE— accès générique au bus I2C (adresse 7 bits, registre, jusqu’à 6 octets de données).0x07 READ_DHT— lecture directe d’un capteur DHT22 (humidité + température), le protocole 1-fil étant bit-bangé côté Arduino.0x10 START_SCAN/0x11 STOP_SCAN— un mode « scan » où l’Arduino pousse de lui-même, sans qu’on lui demande rien, un rapport analogique toutes les 30 ms en boucle sur A0-A5 (marqueur0xA0), pratique pour un tracé en direct.
Un détail qui a son importance : le sketch déclare des rapports pleins de 64 octets (RawHID.begin(rawhidData, 64)), et il faut vraiment envoyer des trames de cette taille complète — une trame tronquée à 8-9 octets désynchronise le firmware HID-Project côté Arduino, qui attend son compte. Toute la couche PPL côté Prime (fonction buildframe() ci-dessous) existe précisément pour ça : elle complète systématiquement chaque commande à 64 octets avant de l’envoyer.
Le sketch Arduino : ArduinoSensorBridge.ino
Voici le sketch flashé sur le Leonardo, tel qu’il tourne réellement à la maison. Il gère les commandes ci-dessus, plus le bit-banging du DHT22 et le mode scan automatique :
La zone ci-dessous se parcourt avec son propre ascenseur (ou s’agrandit avec la poignée en bas à droite). Pour récupérer le code : cliquez dedans, sélectionnez tout (Ctrl+A ou Cmd+A), puis copiez (Ctrl+C).
*/
/*
ArduinoSensorBridge — pont USB-HID brut entre la HP Prime (USBOpen/USBSend/
USBRecv) et un Arduino Leonardo, pour piloter des capteurs generiques
(analogique, digital, I2C).
Base sur HID-Project (NicoHood), exemple RawHID officiel verifie :
https://github.com/NicoHood/HID/blob/master/examples/RawHID/RawHID/RawHID.ino
Transport brut valide (echo octet-pour-octet) avant d'ajouter ce protocole.
Protocole : rapports de 64 octets, [0]=commande, [1..7]=parametres/donnees
(le reste du rapport est ignore/complete a 0). Reponse : meme format.
CMD_PING 0x00 -> renvoie la trame telle quelle
CMD_READ_ANALOG 0x01 [1]=broche A0..A5 (0..5) -> [1]=octet bas,[2]=octet haut (ADC 0-1023)
CMD_READ_DIGITAL 0x02 [1]=broche -> [1]=0/1
CMD_WRITE_DIGITAL 0x03 [1]=broche [2]=valeur(0/1) -> [1]=1 (ack)
CMD_PIN_MODE 0x04 [1]=broche [2]=mode(0=INPUT,1=OUTPUT,2=INPUT_PULLUP) -> [1]=1 (ack)
CMD_I2C_READ 0x05 [1]=adresse7bits [2]=registre [3]=nb octets(<=6) -> [1..6]=donnees (ou [0]=0xFF si erreur)
CMD_I2C_WRITE 0x06 [1]=adresse7bits [2]=registre [3]=nb octets(<=5) [4..]=donnees -> [1]=1 si ok
CMD_READ_DHT 0x07 [1]=broche -> [1..2]=humidite*10 (u16), [3..4]=temperature*10 (s16)
(ou [0]=0xFE si timeout/checksum invalide — reessayer)
CMD_START_SCAN 0x10 -> [1]=1 (ack), demarre le scan automatique (voir plus bas)
CMD_STOP_SCAN 0x11 -> [1]=1 (ack), arrete le scan automatique
Mode SCAN (CMD_START_SCAN) : une fois active, l'Arduino pousse de lui-meme,
toutes les ~30ms et SANS attendre de demande, un rapport de lecture
analogique en boucle sur A0..A5 :
[0]=0xA0 (marqueur "donnee de scan", different d'une reponse de commande)
[1]=index de broche (0=A0 .. 5=A5)
[2]=octet bas ADC, [3]=octet haut ADC
Le mode question/reponse (CMD_PING, CMD_READ_ANALOG, etc.) reste utilisable
en parallele — envoyer CMD_STOP_SCAN pour revenir a un mode purement
question/reponse si besoin.
VID:PID Leonardo (inchange par HID-Project) : 2341:8036 (verifie via lsusb).
*/
#include "HID-Project.h"
#include <Wire.h>
enum : uint8_t {
CMD_PING = 0x00,
CMD_READ_ANALOG = 0x01,
CMD_READ_DIGITAL = 0x02,
CMD_WRITE_DIGITAL = 0x03,
CMD_PIN_MODE = 0x04,
CMD_I2C_READ = 0x05,
CMD_I2C_WRITE = 0x06,
CMD_READ_DHT = 0x07,
CMD_START_SCAN = 0x10,
CMD_STOP_SCAN = 0x11,
};
const uint8_t SCAN_MARKER = 0xA0;
const unsigned long SCAN_INTERVAL_MS = 30;
uint8_t rawhidData[64];
const uint8_t analogPins[] = { A0, A1, A2, A3, A4, A5 };
bool scanning = false;
uint8_t scanPin = 0;
unsigned long lastScanMs = 0;
bool ledState = false;
unsigned long lastBlinkMs = 0;
const unsigned long BLINK_INTERVAL_MS = 150;
void setup() {
RawHID.begin(rawhidData, sizeof(rawhidData));
Wire.begin();
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
}
void loop() {
handleIncomingCommand();
handleScan();
}
// Lecture DHT22 (protocole 1 fil, bit-banging manuel — pas de bibliotheque
// externe requise). Renvoie true si les 40 bits + checksum sont valides.
bool readDHT22(uint8_t pin, int16_t &humidity10, int16_t &temp10) {
uint8_t data[5] = {0, 0, 0, 0, 0};
// Signal de depart : broche a LOW >= 1ms puis relachee (pull-up interne)
pinMode(pin, OUTPUT);
digitalWrite(pin, LOW);
delay(2);
digitalWrite(pin, HIGH);
pinMode(pin, INPUT_PULLUP);
delayMicroseconds(30);
// Reponse du capteur : LOW ~80us puis HIGH ~80us
unsigned long t0 = micros();
while (digitalRead(pin) == HIGH) { if (micros() – t0 > 200) return false; }
t0 = micros();
while (digitalRead(pin) == LOW) { if (micros() – t0 > 200) return false; }
t0 = micros();
while (digitalRead(pin) == HIGH) { if (micros() – t0 > 200) return false; }
// 40 bits de donnees : chaque bit commence par ~50us LOW, puis HIGH
// ~26-28us (bit 0) ou ~70us (bit 1)
for (uint8_t i = 0; i < 40; i++) {
t0 = micros();
while (digitalRead(pin) == LOW) { if (micros() – t0 > 200) return false; }
unsigned long highStart = micros();
t0 = micros();
while (digitalRead(pin) == HIGH) { if (micros() – t0 > 200) return false; }
data[i / 8] <<= 1;
if (micros() – highStart > 40) data[i / 8] |= 1;
}
if (((data[0] + data[1] + data[2] + data[3]) & 0xFF) != data[4]) return false;
humidity10 = (int16_t)((data[0] << 8) | data[1]);
int16_t rawTemp = ((data[2] & 0x7F) << 8) | data[3];
temp10 = (data[2] & 0x80) ? -rawTemp : rawTemp;
return true;
}
void handleIncomingCommand() {
if (!RawHID.available()) return;
// Lit tout le rapport recu (meme pattern que le test d'echo qui a fonctionne)
uint8_t in[64] = {0};
uint8_t n = 0;
while (RawHID.available() && n < 64) in[n++] = RawHID.read();
Serial.print("recu n="); Serial.print(n); Serial.print(" : ");
for (uint8_t i = 0; i < 8; i++) { Serial.print(in[i], HEX); Serial.print(" "); }
Serial.println();
uint8_t resp[64] = {0};
resp[0] = in[0];
switch (in[0]) {
case CMD_PING:
memcpy(resp, in, 8);
break;
case CMD_READ_ANALOG: {
uint8_t pin = in[1];
if (pin < 6) {
uint16_t v = analogRead(analogPins[pin]);
resp[1] = v & 0xFF;
resp[2] = (v >> 8) & 0xFF;
}
break;
}
case CMD_READ_DIGITAL:
resp[1] = digitalRead(in[1]) ? 1 : 0;
break;
case CMD_WRITE_DIGITAL:
digitalWrite(in[1], in[2] ? HIGH : LOW);
resp[1] = 1;
break;
case CMD_PIN_MODE: {
uint8_t mode = in[2];
pinMode(in[1], mode == 1 ? OUTPUT : mode == 2 ? INPUT_PULLUP : INPUT);
resp[1] = 1;
break;
}
case CMD_I2C_READ: {
uint8_t addr = in[1], reg = in[2], cnt = in[3];
if (cnt > 6) cnt = 6;
Wire.beginTransmission(addr);
Wire.write(reg);
if (Wire.endTransmission(false) == 0) {
Wire.requestFrom((int)addr, (int)cnt);
for (uint8_t i = 0; i < cnt && Wire.available(); i++) resp[1 + i] = Wire.read();
} else {
resp[0] = 0xFF;
}
break;
}
case CMD_I2C_WRITE: {
uint8_t addr = in[1], reg = in[2], cnt = in[3];
if (cnt > 5) cnt = 5;
Wire.beginTransmission(addr);
Wire.write(reg);
for (uint8_t i = 0; i < cnt; i++) Wire.write(in[4 + i]);
resp[1] = (Wire.endTransmission() == 0) ? 1 : 0;
break;
}
case CMD_READ_DHT: {
int16_t h10, t10;
if (readDHT22(in[1], h10, t10)) {
resp[1] = h10 & 0xFF;
resp[2] = (h10 >> 8) & 0xFF;
resp[3] = t10 & 0xFF;
resp[4] = (t10 >> 8) & 0xFF;
} else {
resp[0] = 0xFE;
}
break;
}
case CMD_START_SCAN:
scanning = true;
scanPin = 0;
resp[1] = 1;
break;
case CMD_STOP_SCAN:
scanning = false;
digitalWrite(LED_BUILTIN, LOW);
resp[1] = 1;
break;
default:
resp[0] = 0xFF;
break;
}
RawHID.write(resp, 64);
}
void handleScan() {
if (!scanning) return;
unsigned long now = millis();
if (now – lastBlinkMs >= BLINK_INTERVAL_MS) {
lastBlinkMs = now;
ledState = !ledState;
digitalWrite(LED_BUILTIN, ledState ? HIGH : LOW);
}
if (now – lastScanMs < SCAN_INTERVAL_MS) return;
lastScanMs = now;
uint16_t v = analogRead(analogPins[scanPin]);
uint8_t report[64] = {0};
report[0] = SCAN_MARKER;
report[1] = scanPin;
report[2] = v & 0xFF;
report[3] = (v >> 8) & 0xFF;
RawHID.write(report, 64);
scanPin = (scanPin + 1) % 6;
}
Le programme HP Prime : ArduinoBridge.hpprgm
Côté calculatrice, le programme PPL expose une petite API bien pratique (ArduinoOpen(), ArduinoReadAnalog(), ArduinoI2cRead(), ArduinoReadDHT()…) au-dessus du protocole brut. Il inclut aussi un pilote BMP280/BME280 complet (pression + température), avec les formules de compensation flottantes officielles du constructeur Bosch appliquées directement en PPL à partir des coefficients de calibration lus en I2C :
La zone ci-dessous se parcourt avec son propre ascenseur (ou s’agrandit avec la poignée en bas à droite). Pour récupérer le code : cliquez dedans, sélectionnez tout (Ctrl+A ou Cmd+A), puis copiez (Ctrl+C).
// Mettre a 1 si les reponses semblent decalees d'un octet (voir remarque ci-dessus)
USE_ID_PREFIX;
VID;
PID;
// Construit une trame COMPLETE de 64 octets [cmd, p1, p2, …, 0…] a partir
// d'une liste — le sketch declare RawHID.begin(rawhidData, 64) : un rapport
// tronque (8-9 octets, comme avant) desynchronise le firmware.
buildframe(cmd, params)
BEGIN
LOCAL f, i, n;
f:=MAKELIST(0,i,1,64,1);
f(1):=cmd;
n:=SIZE(params);
FOR i FROM 1 TO n DO
IF i<64 THEN f(i+1):=params(i); END;
END;
RETURN f;
END;
// Envoie une trame de 64 octets, renvoie la reponse (liste, jusqu'a 64 octets).
// USBRecv() est non bloquant (peut renvoyer une liste vide ou partielle) —
// on interroge donc en boucle jusqu'a avoir au moins 8 octets utiles ou expirer.
// Timeout base sur le temps reel ecoule (TICKS(), en ms) et non sur un nombre
// fixe d'iterations — un compteur d'essais fixe correspond a un temps reel
// variable selon la vitesse d'execution du moment, ce qui rendait le timeout
// parfois trop court (PING instable).
sendframe(f)
BEGIN
LOCAL toSend, resp, i, j, chunk, respLen, t0;
IF USE_ID_PREFIX==1 THEN
toSend:=MAKELIST(0,i,1,65,1);
FOR i FROM 1 TO 64 DO toSend(i+1):=f(i); END;
ELSE
toSend:=f;
END;
USBSend(toSend);
resp:=MAKELIST(0,i,1,64,1);
respLen:=0;
t0:=TICKS();
WHILE respLen<8 AND (TICKS()-t0)<2000 DO
chunk:=USBRecv();
IF SIZE(chunk)>0 THEN
FOR j FROM 1 TO SIZE(chunk) DO
IF respLen<64 THEN
respLen:=respLen+1;
resp(respLen):=chunk(j);
END;
END;
END;
END;
RETURN resp;
END;
EXPORT ArduinoOpen()
BEGIN
VID:=9025; // 0x2341, Arduino SA
PID:=32822; // 0x8036, Leonardo — verifie via lsusb sur le vrai materiel
USE_ID_PREFIX:=0; // confirme sur le vrai materiel : la Prime n'a pas besoin du prefixe
USBOpen(VID,PID);
MSGBOX("HP Prime <-> Arduino : ouvert (VID "+STRING(VID)+" PID "+STRING(PID)+").");
END;
EXPORT ArduinoPing()
BEGIN
LOCAL f, r;
f:=buildframe(0,{17,34,51,68,85,102,119});
r:=sendframe(f);
IF r(1)==0 AND r(2)==17 THEN
MSGBOX("PING OK — Arduino repond correctement.");
ELSE
MSGBOX("PING echec — reponse : "+STRING(r)+
CHAR(10)+"Essaie de changer USE_ID_PREFIX (0 <-> 1).");
END;
END;
// Lit une broche analogique A0..A5 (0..5), renvoie 0-1023
EXPORT ArduinoReadAnalog(pin)
BEGIN
LOCAL f, r;
f:=buildframe(1,{pin});
r:=sendframe(f);
RETURN r(2)+256*r(3);
END;
// Lit une broche digitale, renvoie 0 ou 1
EXPORT ArduinoReadDigital(pin)
BEGIN
LOCAL f, r;
f:=buildframe(2,{pin});
r:=sendframe(f);
RETURN r(2);
END;
// Ecrit une broche digitale (val=0 ou 1)
EXPORT ArduinoWriteDigital(pin, val)
BEGIN
LOCAL f, r;
f:=buildframe(3,{pin,val});
r:=sendframe(f);
RETURN r(2); // 1 = ack recu
END;
// Configure le mode d'une broche : mode=0 INPUT, 1 OUTPUT, 2 INPUT_PULLUP
EXPORT ArduinoPinMode(pin, mode)
BEGIN
LOCAL f, r;
f:=buildframe(4,{pin,mode});
r:=sendframe(f);
RETURN r(2);
END;
// Lit "n" octets (<=6) depuis un capteur I2C, a partir du registre "reg".
// Cablage par torsades (pas de breadboard) -> memes coupures de contact
// possibles que pour le DHT22 ; on reessaie donc silencieusement (5 fois,
// court delai) avant d'abandonner et de renvoyer {}.
EXPORT ArduinoI2cRead(addr, reg, n)
BEGIN
LOCAL f, r, i, out, tries;
f:=buildframe(5,{addr,reg,n});
FOR tries FROM 1 TO 5 DO
IF tries>1 THEN WAIT(0.2); END;
r:=sendframe(f);
IF r(1)<>255 THEN
out:=MAKELIST(0,i,1,n,1);
FOR i FROM 1 TO n DO out(i):=r(i+1); END;
RETURN out;
END;
END;
RETURN {}; // echec apres 5 essais (pas d'accuse de l'esclave 0x"+STRING(addr)+")
END;
// Ecrit une liste "data" (<=5 octets) dans un capteur I2C, registre "reg".
// Meme logique de reessai silencieux que ArduinoI2cRead.
EXPORT ArduinoI2cWrite(addr, reg, data)
BEGIN
LOCAL f, params, i, n, r, tries;
n:=SIZE(data);
params:={0,0,0,0,0,0,0}; // preallouee (evite de compter sur une croissance dynamique de liste)
params(1):=addr; params(2):=reg; params(3):=n;
FOR i FROM 1 TO n DO params(3+i):=data(i); END;
f:=buildframe(6,params);
FOR tries FROM 1 TO 5 DO
IF tries>1 THEN WAIT(0.2); END;
r:=sendframe(f);
IF r(2)==1 THEN RETURN 1; END;
END;
RETURN 0; // echec apres 5 essais
END;
// — Mode SCAN automatique (CMD_START_SCAN/CMD_STOP_SCAN, voir .ino) ——-
// Une fois demarre, l'Arduino pousse tout seul, sans requete, un rapport
// [0xA0, broche, valLo, valHi] toutes les ~30ms en boucle sur A0..A5.
// Lit un rapport brut (4 octets utiles) sans envoyer de commande — pour les
// rapports non sollicites du mode scan. Renvoie {} si rien recu.
readRawReport(maxTries)
BEGIN
LOCAL resp, respLen, chunk, j, tries;
resp:=MAKELIST(0,j,1,4,1);
respLen:=0;
tries:=0;
WHILE respLen<4 AND tries<maxTries DO
chunk:=USBRecv();
IF SIZE(chunk)>0 THEN
FOR j FROM 1 TO SIZE(chunk) DO
IF respLen<4 THEN
respLen:=respLen+1;
resp(respLen):=chunk(j);
END;
END;
END;
tries:=tries+1;
END;
IF respLen<4 THEN RETURN {}; END;
RETURN resp;
END;
EXPORT ArduinoStartScan()
BEGIN
LOCAL f, r;
f:=buildframe(16,{}); // CMD_START_SCAN = 0x10
r:=sendframe(f);
IF r(1)==16 AND r(2)==1 THEN
MSGBOX("Scan demarre.");
ELSE
MSGBOX("Echec demarrage scan — reponse : "+STRING(r));
END;
END;
EXPORT ArduinoStopScan()
BEGIN
LOCAL f, r;
f:=buildframe(17,{}); // CMD_STOP_SCAN = 0x11
r:=sendframe(f);
IF r(1)==17 AND r(2)==1 THEN
MSGBOX("Scan arrete.");
ELSE
MSGBOX("Echec arret scan — reponse : "+STRING(r));
END;
END;
// Lit un echantillon de scan (mode SCAN deja demarre cote Arduino).
// Renvoie {broche, valeur} si un rapport de scan a ete recu, {} sinon
// (rien de nouveau, ou rapport residuel qui n'est pas un marqueur de scan).
EXPORT ArduinoScanRead()
BEGIN
LOCAL r;
r:=readRawReport(5);
IF SIZE(r)<4 THEN RETURN {}; END;
IF r(1)==160 THEN // 0xA0
RETURN {r(2), r(3)+256*r(4)};
END;
RETURN {};
END;
// Demo : demarre le scan, affiche en direct la derniere broche/valeur lue,
// jusqu'a l'appui sur la touche ANNUL (ISKEYDOWN(4)), puis arrete le scan.
EXPORT ArduinoScanDemo()
BEGIN
LOCAL s;
ArduinoStartScan();
REPEAT
s:=ArduinoScanRead();
IF SIZE(s)==2 THEN
RECT_P();
TEXTOUT_P("A"+STRING(s(1))+" = "+STRING(s(2)),10,10,2,RGB(0,0,0));
END;
UNTIL ISKEYDOWN(4);
ArduinoStopScan();
END;
// — DHT22 (temperature + humidite, protocole 1 fil, CMD_READ_DHT) ——–
// Lit un DHT22 branche sur "pin". Renvoie {temperature en C, humidite en %},
// ou {} si la lecture a echoue apres plusieurs essais.
//
// Le checksum du protocole DHT (simple somme sur 8 bits) ne detecte pas
// toutes les erreurs — une lecture peut "reussir" avec une valeur
// impossible (ex. humidite > 100%) si un pull-up trop faible fait deraper
// le timing d'un seul bit. On rejette donc aussi les valeurs hors plage
// physique du capteur, et on reessaie (jusqu'a 5 fois, 2s d'ecart minimum
// entre deux lectures DHT22 — respecte le repos exige par le capteur).
EXPORT ArduinoReadDHT(pin)
BEGIN
LOCAL f, r, h, t, tries;
FOR tries FROM 1 TO 5 DO
IF tries>1 THEN WAIT(2); END; // respecte le repos exige par le DHT22 entre deux lectures
f:=buildframe(7,{pin});
r:=sendframe(f);
IF r(1)<>254 THEN // 0xFE = echec (timeout ou checksum invalide) -> on reessaie
h:=(r(2)+256*r(3))/10;
t:=r(4)+256*r(5);
IF t>=32768 THEN t:=t-65536; END;
t:=t/10;
IF h>=0 AND h<=100 AND t>=-40 AND t<=80 THEN // filtre les valeurs hors plage physique
RETURN {t, h};
END;
END;
END;
RETURN {};
END;
// Trace en direct les courbes temperature (rouge) et humidite (bleue) d'un
// DHT22 branche sur "pin". Un nouvel echantillon = un appel a ArduinoReadDHT
// (qui gere deja ses propres reessais) ; les echecs ({}) sont ignores sans
// interrompre le trace. Arret avec la touche ANNUL.
//
// Echelle fixe : 0-40 C et 0-100 %HR (couvre les conditions interieures
// habituelles) — ajuster TPLOT_MIN/TPLOT_MAX ci-dessous si besoin.
EXPORT ArduinoDHTPlot(pin)
BEGIN
LOCAL serieT, serieH, r, i, n;
LOCAL px0, px1, py0, py1, maxPoints, tmin, tmax, count;
maxPoints:=140;
serieT:=MAKELIST(0,i,1,maxPoints,1); // buffer pre-alloue (meme style que buildframe/ArduinoI2cRead)
serieH:=MAKELIST(0,i,1,maxPoints,1);
count:=0;
tmin:=0; tmax:=40;
px0:=30; px1:=310; py0:=24; py1:=210; // zone de trace en pixels (ecran 320×240)
RECT_P();
TEXTOUT_P("T (rouge, 0-40C) H (bleu, 0-100%)",30,4,1,RGB(0,0,0));
REPEAT
r:=ArduinoReadDHT(pin);
IF SIZE(r)==2 THEN
IF count<maxPoints THEN
count:=count+1;
serieT(count):=r(1);
serieH(count):=r(2);
ELSE
FOR i FROM 1 TO maxPoints-1 DO // buffer plein : decale tout d'un cran (defilement)
serieT(i):=serieT(i+1);
serieH(i):=serieH(i+1);
END;
serieT(maxPoints):=r(1);
serieH(maxPoints):=r(2);
END;
RECT_P();
TEXTOUT_P("T (rouge, 0-40C) H (bleu, 0-100%)",30,4,1,RGB(0,0,0));
LINE_P(px0,py0,px0,py1,RGB(0,0,0));
LINE_P(px0,py1,px1,py1,RGB(0,0,0));
n:=count;
IF n>1 THEN
FOR i FROM 1 TO n-1 DO
LINE_P(px0+(i-1)*(px1-px0)/maxPoints, py1-(serieT(i)-tmin)/(tmax-tmin)*(py1-py0),
px0+i*(px1-px0)/maxPoints, py1-(serieT(i+1)-tmin)/(tmax-tmin)*(py1-py0),
RGB(220,0,0));
LINE_P(px0+(i-1)*(px1-px0)/maxPoints, py1-serieH(i)/100*(py1-py0),
px0+i*(px1-px0)/maxPoints, py1-serieH(i+1)/100*(py1-py0),
RGB(0,80,220));
END;
END;
TEXTOUT_P("T="+STRING(serieT(n))+"C H="+STRING(serieH(n))+"%",30,py1+6,1,RGB(0,0,0));
END;
UNTIL ISKEYDOWN(4);
END;
// — BMP280/BME280 (pression + temperature, I2C generique) —————-
// Utilise ArduinoI2cRead/ArduinoI2cWrite (deja transportes tels quels par le
// firmware) — la calibration et la formule de compensation (datasheet
// Bosch, version flottante officielle) sont faites cote HP Prime.
// Adresse I2C : 0x76 (118) ou 0x77 (119) selon le module.
BMP_ADDR;
BMP_T1; BMP_T2; BMP_T3;
BMP_P1; BMP_P2; BMP_P3; BMP_P4; BMP_P5; BMP_P6; BMP_P7; BMP_P8; BMP_P9;
BMP_TFINE;
s16(lo, hi)
BEGIN
LOCAL v;
v:=lo+256*hi;
IF v>=32768 THEN v:=v-65536; END;
RETURN v;
END;
u16(lo, hi)
BEGIN
RETURN lo+256*hi;
END;
// Lit les coefficients de calibration et configure le capteur en mode normal.
// addr : 118 (0x76) ou 119 (0x77).
EXPORT ArduinoBmpInit(addr)
BEGIN
LOCAL c1, c2, c3, c4;
BMP_ADDR:=addr;
c1:=ArduinoI2cRead(addr,136,6); // 0x88 : dig_T1(u16) dig_T2(s16) dig_T3(s16)
BMP_T1:=u16(c1(1),c1(2));
BMP_T2:=s16(c1(3),c1(4));
BMP_T3:=s16(c1(5),c1(6));
c2:=ArduinoI2cRead(addr,142,6); // 0x8E : dig_P1(u16) dig_P2(s16) dig_P3(s16)
BMP_P1:=u16(c2(1),c2(2));
BMP_P2:=s16(c2(3),c2(4));
BMP_P3:=s16(c2(5),c2(6));
c3:=ArduinoI2cRead(addr,148,6); // 0x94 : dig_P4(s16) dig_P5(s16) dig_P6(s16)
BMP_P4:=s16(c3(1),c3(2));
BMP_P5:=s16(c3(3),c3(4));
BMP_P6:=s16(c3(5),c3(6));
c4:=ArduinoI2cRead(addr,154,6); // 0x9A : dig_P7(s16) dig_P8(s16) dig_P9(s16)
BMP_P7:=s16(c4(1),c4(2));
BMP_P8:=s16(c4(3),c4(4));
BMP_P9:=s16(c4(5),c4(6));
ArduinoI2cWrite(addr,244,{39}); // 0xF4 = 0x27 : temp/press oversampling x1, mode normal
ArduinoI2cWrite(addr,245,{0}); // 0xF5 = 0x00 : pas de filtre IIR, standby min
MSGBOX("BMP280 initialise (addr "+STRING(addr)+").");
END;
// Renvoie {temperature en degres C, pression en Pa}. Appeler ArduinoBmpInit()
// une fois avant.
EXPORT ArduinoBmpRead()
BEGIN
LOCAL d, rawT, rawP, var1, var2, t, p;
d:=ArduinoI2cRead(BMP_ADDR,247,6); // 0xF7 : press_msb,lsb,xlsb, temp_msb,lsb,xlsb
rawP:=d(1)*4096+d(2)*16+IP(d(3)/16);
rawT:=d(4)*4096+d(5)*16+IP(d(6)/16);
// Compensation temperature (formule flottante officielle Bosch)
var1:=(rawT/16384-BMP_T1/1024)*BMP_T2;
var2:=(rawT/131072-BMP_T1/8192)*(rawT/131072-BMP_T1/8192)*BMP_T3;
BMP_TFINE:=var1+var2;
t:=BMP_TFINE/5120;
// Compensation pression
var1:=BMP_TFINE/2-64000;
var2:=var1*var1*BMP_P6/32768;
var2:=var2+var1*BMP_P5*2;
var2:=var2/4+BMP_P4*65536;
var1:=(BMP_P3*var1*var1/524288+BMP_P2*var1)/524288;
var1:=(1+var1/32768)*BMP_P1;
IF var1==0 THEN RETURN {t,0}; END; // evite une division par zero
p:=1048576-rawP;
p:=(p-var2/4096)*6250/var1;
var1:=BMP_P9*p*p/2147483648;
var2:=p*BMP_P8/32768;
p:=p+(var1+var2+BMP_P7)/16;
RETURN {t,p};
END;
EXPORT ArduinoClose()
BEGIN
// USBClose() n'existe pas en PPL (seuls USBOpen/USBSend/USBRecv sont
// documentes) — la connexion USB-HID ne se ferme pas explicitement.
MSGBOX("Connexion Arduino fermee.");
END;
Le capteur d’humidité en pratique : ArduinoReadDHT() et le tracé en direct
La fonction ArduinoReadDHT(pin) envoie la commande 0x07, récupère température et humidité (encodées en dixièmes sur 16 bits), et filtre les valeurs hors plage physique (humidité > 100 %, par exemple) — le checksum du protocole DHT, une simple somme sur 8 bits, ne détecte pas toutes les erreurs de timing. En cas d’échec, elle réessaie jusqu’à 5 fois en respectant les 2 secondes de repos minimum exigées par le capteur entre deux lectures.
ArduinoDHTPlot(pin) va plus loin : elle boucle sur ArduinoReadDHT() et trace en direct, à l’écran de la Prime, deux courbes qui défilent — la température en rouge, l’humidité en bleu — jusqu’à l’appui sur la touche Annul. De quoi transformer la calculatrice en petit enregistreur de données climatiques de bureau, sans écrire une seule ligne de code supplémentaire côté Arduino.
Débogage : isoler le problème avant même d’allumer la Prime
Petite leçon apprise en cours de route, et qui vaut pour n’importe quel projet USB-HID maison : avant de soupçonner le protocole PPL, il vaut mieux valider le firmware Arduino tout seul, depuis un PC Linux, avec la bibliothèque Python hidapi. Même taille de rapport déclarée (64 octets), même test d’echo — si ça boucle proprement là, le firmware est hors de cause et le problème ne peut venir que du côté HP Prime. Ce test au banc a d’ailleurs débusqué un bug amusant : une trame envoyée trop courte faisait parfois remonter, à la place des données attendues, la chaîne de description USB du périphérique en ASCII (littéralement le mot « arduino ») — signe que le firmware HID-Project, désynchronisé, se mettait à répondre avec autre chose que le protocole attendu.
Autre piège typiquement PPL : les listes sont indexées à partir de 1, donc l’octet 0 d’un rapport HID (
resp[0]côté C) devientr(1)en PPL, l’octet 1 devientr(2), etc. Un classique décalage d’un cran qui ne pardonne pas sur un protocole binaire.
Et pour être tout à fait honnête : le transport brut a été validé de bout en bout sur PC Linux (echo exact, lecture analogique, écriture digitale), mais la syntaxe précise d’USBOpen/USBSend/USBRecv côté HP Prime n’a pas pu être confirmée sur une documentation officielle accessible — ce programme est écrit à partir de recherches, pas d’un exemple garanti par HP. Reste donc un point resté en suspens dans le code lui-même : faut-il préfixer chaque trame envoyée d’un octet factice 0x00 (le fameux « ID de rapport » des périphériques HID non numérotés) ? Le programme garde les deux variantes disponibles via un simple interrupteur (USE_ID_PREFIX), à corriger au besoin une fois la calculatrice vraiment branchée sur l’Arduino — exactement le même esprit « à tester et ajuster ensemble sur le matériel réel » que pour mes précédents programmes PPL maison.