mirror of
https://github.com/RobTillaart/Arduino.git
synced 2024-10-03 18:09:02 -04:00
0.2.8 PCA9634
This commit is contained in:
parent
8aba6bb95d
commit
1e88244dfc
@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
|
||||
## [0.2.8] - 2023-05-24
|
||||
- renaming #defines PCA963X... to prepare merge with PCA9634.
|
||||
- old defines will work until next major release
|
||||
- update keywords.txt
|
||||
- update readme.md
|
||||
|
||||
|
||||
## [0.2.7] - 2023-05-01
|
||||
- add **writeLedOut(reg, mask)** experimental
|
||||
- add **readLedOut(reg)** experimental
|
||||
@ -19,7 +26,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- update readme.md
|
||||
- minor edits
|
||||
|
||||
|
||||
## [0.2.6] - 2023-03-07
|
||||
- add OutputEnable control functions.
|
||||
- add example **PCA9634_OE_control.ino**
|
||||
|
@ -2,7 +2,7 @@
|
||||
// FILE: PCA9634.cpp
|
||||
// AUTHOR: Rob Tillaart
|
||||
// DATE: 2022-01-03
|
||||
// VERSION: 0.2.7
|
||||
// VERSION: 0.2.8
|
||||
// PURPOSE: Arduino library for PCA9634 I2C LED driver
|
||||
// URL: https://github.com/RobTillaart/PCA9634
|
||||
|
||||
@ -310,9 +310,9 @@ bool PCA9634::enableSubCall(uint8_t nr)
|
||||
if ((nr == 0) || (nr > 3)) return false;
|
||||
uint8_t prev = getMode1();
|
||||
uint8_t mask = prev;
|
||||
if (nr == 1) mask |= PCA9634_MODE1_SUB1;
|
||||
else if (nr == 2) mask |= PCA9634_MODE1_SUB2;
|
||||
else mask |= PCA9634_MODE1_SUB3;
|
||||
if (nr == 1) mask |= PCA963X_MODE1_SUB1;
|
||||
else if (nr == 2) mask |= PCA963X_MODE1_SUB2;
|
||||
else mask |= PCA963X_MODE1_SUB3;
|
||||
// only update if changed.
|
||||
if (mask != prev)
|
||||
{
|
||||
@ -328,9 +328,9 @@ bool PCA9634::disableSubCall(uint8_t nr)
|
||||
if ((nr == 0) || (nr > 3)) return false;
|
||||
uint8_t prev = getMode1();
|
||||
uint8_t mask = prev;
|
||||
if (nr == 1) mask &= ~PCA9634_MODE1_SUB1;
|
||||
else if (nr == 2) mask &= ~PCA9634_MODE1_SUB2;
|
||||
else mask &= ~PCA9634_MODE1_SUB3;
|
||||
if (nr == 1) mask &= ~PCA963X_MODE1_SUB1;
|
||||
else if (nr == 2) mask &= ~PCA963X_MODE1_SUB2;
|
||||
else mask &= ~PCA963X_MODE1_SUB3;
|
||||
// only update if changed.
|
||||
if (mask != prev)
|
||||
{
|
||||
@ -345,9 +345,9 @@ bool PCA9634::isEnabledSubCall(uint8_t nr)
|
||||
{
|
||||
if ((nr == 0) || (nr > 3)) return false;
|
||||
uint8_t mask = getMode1();
|
||||
if (nr == 1) return (mask & PCA9634_MODE1_SUB1) > 0;
|
||||
if (nr == 2) return (mask & PCA9634_MODE1_SUB2) > 0;
|
||||
return (mask & PCA9634_MODE1_SUB3) > 0;
|
||||
if (nr == 1) return (mask & PCA963X_MODE1_SUB1) > 0;
|
||||
if (nr == 2) return (mask & PCA963X_MODE1_SUB2) > 0;
|
||||
return (mask & PCA963X_MODE1_SUB3) > 0;
|
||||
}
|
||||
|
||||
|
||||
@ -358,7 +358,7 @@ bool PCA9634::setSubCallAddress(uint8_t nr, uint8_t address)
|
||||
// _error = ?? TODO
|
||||
return false;
|
||||
}
|
||||
writeReg(PCA9634_SUBADR(nr), address);
|
||||
writeReg(PCA963X_SUBADR(nr), address);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -370,7 +370,7 @@ uint8_t PCA9634::getSubCallAddress(uint8_t nr)
|
||||
// _error = ?? TODO
|
||||
return 0;
|
||||
}
|
||||
uint8_t address = readReg(PCA9634_SUBADR(nr));
|
||||
uint8_t address = readReg(PCA963X_SUBADR(nr));
|
||||
return address;
|
||||
}
|
||||
|
||||
@ -378,7 +378,7 @@ uint8_t PCA9634::getSubCallAddress(uint8_t nr)
|
||||
bool PCA9634::enableAllCall()
|
||||
{
|
||||
uint8_t prev = getMode1();
|
||||
uint8_t mask = prev | PCA9634_MODE1_ALLCALL;
|
||||
uint8_t mask = prev | PCA963X_MODE1_ALLCALL;
|
||||
// only update if changed.
|
||||
if (mask != prev)
|
||||
{
|
||||
@ -392,7 +392,7 @@ bool PCA9634::enableAllCall()
|
||||
bool PCA9634::disableAllCall()
|
||||
{
|
||||
uint8_t prev = getMode1();
|
||||
uint8_t mask = prev & ~PCA9634_MODE1_ALLCALL;
|
||||
uint8_t mask = prev & ~PCA963X_MODE1_ALLCALL;
|
||||
// only update if changed.
|
||||
if (mask != prev)
|
||||
{
|
||||
@ -406,20 +406,20 @@ bool PCA9634::disableAllCall()
|
||||
bool PCA9634::isEnabledAllCall()
|
||||
{
|
||||
uint8_t mask = getMode1();
|
||||
return mask & PCA9634_MODE1_ALLCALL;
|
||||
return mask & PCA963X_MODE1_ALLCALL;
|
||||
}
|
||||
|
||||
|
||||
bool PCA9634::setAllCallAddress(uint8_t address)
|
||||
{
|
||||
writeReg(PCA9634_ALLCALLADR, address);
|
||||
writeReg(PCA963X_ALLCALLADR, address);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
uint8_t PCA9634::getAllCallAddress()
|
||||
{
|
||||
uint8_t address = readReg(PCA9634_ALLCALLADR);
|
||||
uint8_t address = readReg(PCA963X_ALLCALLADR);
|
||||
return address;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
// FILE: PCA9634.h
|
||||
// AUTHOR: Rob Tillaart
|
||||
// DATE: 2022-01-03
|
||||
// VERSION: 0.2.7
|
||||
// VERSION: 0.2.8
|
||||
// PURPOSE: Arduino library for PCA9634 I2C LED driver, 8 channel
|
||||
// URL: https://github.com/RobTillaart/PCA9634
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
#include "Wire.h"
|
||||
|
||||
|
||||
#define PCA9634_LIB_VERSION (F("0.2.7"))
|
||||
#define PCA9634_LIB_VERSION (F("0.2.8"))
|
||||
|
||||
|
||||
// mode codes
|
||||
@ -50,6 +50,7 @@
|
||||
#define PCA9634_LEDPWM 0x02
|
||||
#define PCA9634_LEDGRPPWM 0x03
|
||||
|
||||
|
||||
// Error codes
|
||||
// NEW
|
||||
#define PCA963X_OK 0x00
|
||||
@ -69,10 +70,18 @@
|
||||
#define PCA9634_ERR_I2C 0xFA
|
||||
|
||||
|
||||
// TODO PCA963X_ constants below here
|
||||
|
||||
|
||||
// Configuration bits MODE1 register
|
||||
// NEW
|
||||
#define PCA963X_MODE1_AUTOINCR2 0x80 // ReadOnly, 0 = disable 1 = enable
|
||||
#define PCA963X_MODE1_AUTOINCR1 0x40 // ReadOnly, bit1
|
||||
#define PCA963X_MODE1_AUTOINCR0 0x20 // ReadOnly, bit0
|
||||
#define PCA963X_MODE1_SLEEP 0x10 // 0 = normal 1 = sleep
|
||||
#define PCA963X_MODE1_SUB1 0x08 // 0 = disable 1 = enable
|
||||
#define PCA963X_MODE1_SUB2 0x04 // 0 = disable 1 = enable
|
||||
#define PCA963X_MODE1_SUB3 0x02 // 0 = disable 1 = enable
|
||||
#define PCA963X_MODE1_ALLCALL 0x01 // 0 = disable 1 = enable
|
||||
#define PCA963X_MODE1_NONE 0x00
|
||||
// OLD
|
||||
#define PCA9634_MODE1_AUTOINCR2 0x80 // ReadOnly, 0 = disable 1 = enable
|
||||
#define PCA9634_MODE1_AUTOINCR1 0x40 // ReadOnly, bit1
|
||||
#define PCA9634_MODE1_AUTOINCR0 0x20 // ReadOnly, bit0
|
||||
@ -85,6 +94,13 @@
|
||||
|
||||
|
||||
// Configuration bits MODE2 register
|
||||
// NEW
|
||||
#define PCA963X_MODE2_BLINK 0x20 // 0 = dim 1 = blink
|
||||
#define PCA963X_MODE2_INVERT 0x10 // 0 = normal 1 = inverted
|
||||
#define PCA963X_MODE2_ACK 0x08 // 0 = on STOP 1 = on ACK
|
||||
#define PCA963X_MODE2_TOTEMPOLE 0x04 // 0 = open drain 1 = totem-pole
|
||||
#define PCA963X_MODE2_NONE 0x00
|
||||
// OLD
|
||||
#define PCA9634_MODE2_BLINK 0x20 // 0 = dim 1 = blink
|
||||
#define PCA9634_MODE2_INVERT 0x10 // 0 = normal 1 = inverted
|
||||
#define PCA9634_MODE2_ACK 0x08 // 0 = on STOP 1 = on ACK
|
||||
@ -93,11 +109,23 @@
|
||||
|
||||
|
||||
// Registers in which the ALLCALL and sub-addresses are stored
|
||||
#define PCA9634_SUBADR(x) (0x0D +(x)) // x = 1..3
|
||||
// NEW
|
||||
#define PCA963X_SUBADR(x) (0x0D +(x)) // x = 1..3
|
||||
#define PCA963X_ALLCALLADR 0x11
|
||||
// OLD
|
||||
#define PCA9634_SUBADR(x) (0x0D +(x)) // x = 1..3
|
||||
#define PCA9634_ALLCALLADR 0x11
|
||||
|
||||
|
||||
// Standard ALLCALL and sub-addresses --> only work for write commands and NOT for read commands
|
||||
// NEW
|
||||
#define PCA963X_ALLCALL 0x70 // TDS of chip says 0xE0, however,
|
||||
// in this library the LSB is added during the write command
|
||||
// (0xE0 --> 0b11100000, 0x70 --> 0b1110000)
|
||||
#define PCA963X_SUB1 0x71 // see line above (0xE2 --> 0x71)
|
||||
#define PCA963X_SUB2 0x72 // see line above (0xE4 --> 0x72)
|
||||
#define PCA963X_SUB3 0x74 // see line above (0xE8 --> 0x74)
|
||||
// OLD
|
||||
#define PCA9634_ALLCALL 0x70 // TDS of chip says 0xE0, however,
|
||||
// in this library the LSB is added during the write command
|
||||
// (0xE0 --> 0b11100000, 0x70 --> 0b1110000)
|
||||
@ -117,11 +145,11 @@ public:
|
||||
|
||||
#if defined (ESP8266) || defined(ESP32)
|
||||
bool begin(int sda, int scl,
|
||||
uint8_t mode1_mask = PCA9634_MODE1_ALLCALL,
|
||||
uint8_t mode2_mask = PCA9634_MODE2_NONE);
|
||||
uint8_t mode1_mask = PCA963X_MODE1_ALLCALL,
|
||||
uint8_t mode2_mask = PCA963X_MODE2_NONE);
|
||||
#endif
|
||||
bool begin(uint8_t mode1_mask = PCA9634_MODE1_ALLCALL,
|
||||
uint8_t mode2_mask = PCA9634_MODE2_NONE);
|
||||
bool begin(uint8_t mode1_mask = PCA963X_MODE1_ALLCALL,
|
||||
uint8_t mode2_mask = PCA963X_MODE2_NONE);
|
||||
bool isConnected();
|
||||
|
||||
|
||||
|
@ -37,10 +37,10 @@ of the PWM signal.
|
||||
|
||||
- **PCA9634(uint8_t deviceAddress, TwoWire \*wire = &Wire)** Constructor with I2C device address,
|
||||
and optional the Wire interface as parameter.
|
||||
- **bool begin(uint8_t mode1_mask = PCA9634_MODE1_ALLCALL, uint8_t mode2_mask = PCA9634_MODE2_NONE)**
|
||||
- **bool begin(uint8_t mode1_mask = PCA963X_MODE1_ALLCALL, uint8_t mode2_mask = PCA963X_MODE2_NONE)**
|
||||
initializes the library after startup. Optionally setting the MODE1 and MODE2 configuration registers.
|
||||
See PCA9634.h and datasheet for settings possible.
|
||||
- **bool begin(int sda, int scl, uint8_t mode1_mask = PCA9634_MODE1_ALLCALL, uint8_t mode2_mask = PCA9634_MODE2_NONE)**
|
||||
- **bool begin(int sda, int scl, uint8_t mode1_mask = PCA963X_MODE1_ALLCALL, uint8_t mode2_mask = PCA963X_MODE2_NONE)**
|
||||
idem, ESP32 ESP8266 only.
|
||||
- **void configure(uint8_t mode1_mask, uint8_t mode2_mask)**
|
||||
To configure the library after startup one can set the MODE1 and MODE2 configuration registers.
|
||||
@ -101,25 +101,24 @@ useful to add or remove a single flag (bit masking).
|
||||
|
||||
#### Constants for mode registers
|
||||
|
||||
(added 0.1.2)
|
||||
|
||||
| Name | Value | Description |
|
||||
|:--------------------------|:-------:|:-------------------------------------|
|
||||
| PCA9634_MODE1_AUTOINCR2 | 0x80 | Read Only, 0 = disable 1 = enable |
|
||||
| PCA9634_MODE1_AUTOINCR1 | 0x40 | Read Only, bit1 |
|
||||
| PCA9634_MODE1_AUTOINCR0 | 0x20 | Read Only, bit0 |
|
||||
| PCA9634_MODE1_SLEEP | 0x10 | 0 = normal 1 = sleep |
|
||||
| PCA9634_MODE1_SUB1 | 0x08 | 0 = disable 1 = enable |
|
||||
| PCA9634_MODE1_SUB2 | 0x04 | 0 = disable 1 = enable |
|
||||
| PCA9634_MODE1_SUB3 | 0x02 | 0 = disable 1 = enable |
|
||||
| PCA9634_MODE1_ALLCALL | 0x01 | 0 = disable 1 = enable |
|
||||
| PCA9634_MODE1_NONE | 0x00 | |
|
||||
| PCA963X_MODE1_AUTOINCR2 | 0x80 | Read Only, 0 = disable 1 = enable |
|
||||
| PCA963X_MODE1_AUTOINCR1 | 0x40 | Read Only, bit1 |
|
||||
| PCA963X_MODE1_AUTOINCR0 | 0x20 | Read Only, bit0 |
|
||||
| PCA963X_MODE1_SLEEP | 0x10 | 0 = normal 1 = sleep |
|
||||
| PCA963X_MODE1_SUB1 | 0x08 | 0 = disable 1 = enable |
|
||||
| PCA963X_MODE1_SUB2 | 0x04 | 0 = disable 1 = enable |
|
||||
| PCA963X_MODE1_SUB3 | 0x02 | 0 = disable 1 = enable |
|
||||
| PCA963X_MODE1_ALLCALL | 0x01 | 0 = disable 1 = enable |
|
||||
| PCA963X_MODE1_NONE | 0x00 | |
|
||||
| ---- | | |
|
||||
| PCA9634_MODE2_BLINK | 0x20 | 0 = dim 1 = blink |
|
||||
| PCA9634_MODE2_INVERT | 0x10 | 0 = normal 1 = inverted |
|
||||
| PCA9634_MODE2_STOP | 0x08 | 0 = on STOP 1 = on ACK |
|
||||
| PCA9634_MODE2_TOTEMPOLE | 0x04 | 0 = open drain 1 = totem-pole |
|
||||
| PCA9634_MODE2_NONE | 0x00 | |
|
||||
| PCA963X_MODE2_BLINK | 0x20 | 0 = dim 1 = blink |
|
||||
| PCA963X_MODE2_INVERT | 0x10 | 0 = normal 1 = inverted |
|
||||
| PCA963X_MODE2_STOP | 0x08 | 0 = on STOP 1 = on ACK |
|
||||
| PCA963X_MODE2_TOTEMPOLE | 0x04 | 0 = open drain 1 = totem-pole |
|
||||
| PCA963X_MODE2_NONE | 0x00 | |
|
||||
|
||||
|
||||
These constants makes it easier to set modes without using a non descriptive
|
||||
@ -130,12 +129,12 @@ ledArray.writeMode(PCA963X_MODE2, 0b00110100);
|
||||
|
||||
// would become
|
||||
|
||||
uint8_t mode2_mask = PCA9634_MODE2_BLINK | PCA9634_MODE2_INVERT | PCA9634_MODE2_TOTEMPOLE;
|
||||
uint8_t mode2_mask = PCA963X_MODE2_BLINK | PCA963X_MODE2_INVERT | PCA963X_MODE2_TOTEMPOLE;
|
||||
ledArray.writeMode(PCA963X_MODE2, mode2_mask);
|
||||
|
||||
// or even
|
||||
|
||||
ledArray.setMode2(PCA9634_MODE2_BLINK | PCA9634_MODE2_INVERT | PCA9634_MODE2_TOTEMPOLE);
|
||||
ledArray.setMode2(PCA963X_MODE2_BLINK | PCA963X_MODE2_INVERT | PCA963X_MODE2_TOTEMPOLE);
|
||||
```
|
||||
|
||||
|
||||
@ -210,13 +209,13 @@ The functions to enable all/sub-addresses are straightforward:
|
||||
|
||||
Since 0.2.6 (experimental) support to control the OE (Output Enable) pin of the PCA9634.
|
||||
This OE pin can control all LEDs simultaneously.
|
||||
It also allows to control multiple PCA9634 modules by connecting the OE pins.
|
||||
It also allows to control multiple devices by connecting the OE pins.
|
||||
Think of simultaneous switching ON/OFF or get dimming with a high frequency PWM.
|
||||
Or use 2 modules alternatively by placing an inverter in between.
|
||||
|
||||
See datasheet for the details
|
||||
|
||||
- **bool setOutputEnablePin(uint8_t pin = 255)** sets the IO pin to connect to the OE pin of the PCA9634.
|
||||
- **bool setOutputEnablePin(uint8_t pin = 255)** sets the IO pin to connect to the OE pin of the device.
|
||||
A value of 255 indicates no pin set/selected.
|
||||
Sets the OE pin to HIGH.
|
||||
Returns true on success.
|
||||
@ -298,7 +297,7 @@ PCA.writeLedOut(1, mask);
|
||||
In scenarios in which multiple LED states should be updated synchronously, the datasheet specifies a specific sequence.
|
||||
Therefore, it is possible to update multiple LED registers in one or more PCA963x chips synchronously and have them
|
||||
change their state at a final STOP command. For this only a few configurations are required.
|
||||
1. Make sure, the `PCA9634_MODE2_ACK` bit in the `MODE2` register is not set (it should be set to 0).
|
||||
1. Make sure, the `PCA963X_MODE2_ACK` bit in the `MODE2` register is not set (it should be set to 0).
|
||||
Therefore, the LED states will be updated at the STOP command of the I2C master and not during the ACK command of the PCA963x slave
|
||||
2. Do NOT use the `write1()`, `write3()` or `writeN()` functions for changing LED states.
|
||||
These commands already include a STOP command.
|
||||
@ -340,19 +339,21 @@ when all previously send commands since the last STOP command will be executed.
|
||||
- read/writeLedOut()
|
||||
- **setOutputEnablePWM(uint16_t value)** PWM support ?
|
||||
- getter?
|
||||
- merge with PCA9635 and a PCA963X base class if possible
|
||||
- restructure function groups
|
||||
- in .cpp to match .h
|
||||
- readme.md
|
||||
- **setGroupPWM()**
|
||||
- PWM also in %% ?
|
||||
- **setGroupFreq()**
|
||||
- set time in milliseconds and round to nearest value?
|
||||
|
||||
|
||||
#### Wont
|
||||
|
||||
- consider implementing
|
||||
- clearMode1() and clearMode2() functions.
|
||||
- only upon request.
|
||||
- merge with PCA9634/5 and a PCA963X base class if possible
|
||||
- not easy, more alignment is desirable.
|
||||
- only upon request.
|
||||
- **setGroupPWM()**
|
||||
- PWM also in %% ? (trivial for user)
|
||||
|
||||
|
BIN
libraries/PCA9634/documents/PCA9635_2021.07.27.pdf
Normal file
BIN
libraries/PCA9634/documents/PCA9635_2021.07.27.pdf
Normal file
Binary file not shown.
@ -61,21 +61,21 @@ PCA963X_ERR_MODE LITERAL1
|
||||
PCA963X_ERR_REG LITERAL1
|
||||
PCA963X_ERR_I2C LITERAL1
|
||||
|
||||
PCA9634_MODE1_AUTOINCR2 LITERAL1
|
||||
PCA9634_MODE1_AUTOINCR1 LITERAL1
|
||||
PCA9634_MODE1_AUTOINCR0 LITERAL1
|
||||
PCA9634_MODE1_SLEEP LITERAL1
|
||||
PCA9634_MODE1_SUB1 LITERAL1
|
||||
PCA9634_MODE1_SUB2 LITERAL1
|
||||
PCA9634_MODE1_SUB3 LITERAL1
|
||||
PCA9634_MODE1_ALLCALL LITERAL1
|
||||
PCA9634_MODE1_NONE LITERAL1
|
||||
PCA963X_MODE1_AUTOINCR2 LITERAL1
|
||||
PCA963X_MODE1_AUTOINCR1 LITERAL1
|
||||
PCA963X_MODE1_AUTOINCR0 LITERAL1
|
||||
PCA963X_MODE1_SLEEP LITERAL1
|
||||
PCA963X_MODE1_SUB1 LITERAL1
|
||||
PCA963X_MODE1_SUB2 LITERAL1
|
||||
PCA963X_MODE1_SUB3 LITERAL1
|
||||
PCA963X_MODE1_ALLCALL LITERAL1
|
||||
PCA963X_MODE1_NONE LITERAL1
|
||||
|
||||
PCA9634_MODE2_BLINK LITERAL1
|
||||
PCA9634_MODE2_INVERT LITERAL1
|
||||
PCA9634_MODE2_ACK LITERAL1
|
||||
PCA9634_MODE2_TOTEMPOLE LITERAL1
|
||||
PCA9634_MODE2_NONE LITERAL1
|
||||
PCA963X_MODE2_BLINK LITERAL1
|
||||
PCA963X_MODE2_INVERT LITERAL1
|
||||
PCA963X_MODE2_ACK LITERAL1
|
||||
PCA963X_MODE2_TOTEMPOLE LITERAL1
|
||||
PCA963X_MODE2_NONE LITERAL1
|
||||
|
||||
PCA963X_LEDOFF LITERAL1
|
||||
PCA963X_LEDON LITERAL1
|
||||
|
@ -15,7 +15,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/RobTillaart/PCA9634.git"
|
||||
},
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.8",
|
||||
"license": "MIT",
|
||||
"frameworks": "arduino",
|
||||
"platforms": "*",
|
||||
|
@ -1,5 +1,5 @@
|
||||
name=PCA9634
|
||||
version=0.2.7
|
||||
version=0.2.8
|
||||
author=Rob Tillaart <rob.tillaart@gmail.com>
|
||||
maintainer=Rob Tillaart <rob.tillaart@gmail.com>
|
||||
sentence=Arduino library for PCA9634 I2C LED driver 8 channel
|
||||
|
Loading…
Reference in New Issue
Block a user