Compare commits
15 Commits
main
...
split-into
| Author | SHA1 | Date |
|---|---|---|
|
|
dd2f801eb2 | |
|
|
ae87a11ebb | |
|
|
16eb93c947 | |
|
|
0acda6922c | |
|
|
9febe791c4 | |
|
|
deae1be027 | |
|
|
93aebd2314 | |
|
|
f1fd19c7be | |
|
|
c370ae3727 | |
|
|
4ae98d066f | |
|
|
cf4dc494ec | |
|
|
87edefdcef | |
|
|
b5b1d08fe2 | |
|
|
ab22426a68 | |
|
|
17de3e63df |
|
|
@ -2,6 +2,4 @@
|
||||||
.vscode
|
.vscode
|
||||||
*.tmp.*
|
*.tmp.*
|
||||||
node_modules
|
node_modules
|
||||||
cardiograph.code-workspace
|
cardiograph.code-workspace
|
||||||
*venv*
|
|
||||||
*__pycache__
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
[submodule "src/argparser"]
|
[submodule "src/argparser"]
|
||||||
path = src/opter
|
path = micro/opter
|
||||||
url = https://git.nloewen.com/n/argv-parser.git
|
url = https://git.nloewen.com/n/argv-parser.git
|
||||||
[submodule "src/python/opter-py"]
|
|
||||||
path = src/python/opter-py
|
|
||||||
url = https://git.nloewen.com/n/opter-py.git
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
# [DRAFT] Specification for the _Cardiograph Architecture_
|
||||||
|
|
||||||
|
## CPU
|
||||||
|
|
||||||
|
### Registers
|
||||||
|
|
||||||
|
There are four 8-bit registers:
|
||||||
|
|
||||||
|
1. **A**, the accumulator (and the only general-purpose register)
|
||||||
|
2. **H**, an index register for 16-bit addressing
|
||||||
|
3. **IP**, the instruction pointer (aka program counter)
|
||||||
|
4. **IOD**, the ID of the current I/O device
|
||||||
|
5. **Status**
|
||||||
|
|
||||||
|
#### Status register
|
||||||
|
|
||||||
|
The *high byte* holds the state of the four Sense Switches. (TODO: is this easy enough to do in hardware?)
|
||||||
|
|
||||||
|
The *low byte* holds four flags:
|
||||||
|
|
||||||
|
- IO **E**rror
|
||||||
|
- **N**egative
|
||||||
|
- **Z**ero
|
||||||
|
- **C**arry
|
||||||
|
|
||||||
|
These are all addressed by number:*
|
||||||
|
|
||||||
|
| S1 | S2 | S3 | S4 | | E | N | Z | C |
|
||||||
|
|----|----|----|----|-|----|----|----|----|
|
||||||
|
| 80 | 40 | 20 | 10 | | 08 | 04 | 02 | 01 |
|
||||||
|
|
||||||
|
\* (Because the core instruction set doesn't include bitwise operations)
|
||||||
|
|
||||||
|
### Instruction set
|
||||||
|
|
||||||
|
- Instructions are two bytes long:
|
||||||
|
one byte for the opcode, one for the operand
|
||||||
|
|
||||||
|
- Opcode format is ```GGMM OOOO``` — **G**roup, **M**ode, **O**peration
|
||||||
|
|
||||||
|
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | A | B | C | D | E | F |
|
||||||
|
|-------|---------|---------|---------|---------|---------|---------|---------|---------|-|-----------|-----------|-----------|-----------|-----------|-----------|----------|-----------|
|
||||||
|
| **0** | NOP | HLT | | | | | | | | | | | | | | | |
|
||||||
|
| **5** | LDA # | STA # | ADD # | SUB # | JMP # | SEQ # | SFL # | TGF # | | _MUL #_ | _DIV #_ | _SLT #_ | _SGT #_ | _NOT #_ | _AND #_ | _OR #_ | _XOR #_ |
|
||||||
|
| **6** | LDA ind | STA ind | ADD ind | SUB ind | JMP ind | SEQ ind | SFL ind | TGF ind | | _MUL ind_ | _DIV ind_ | _SLT ind_ | _SGT ind_ | _NOT ind_ | _AND ind_ | _OR ind_ | _XOR ind_ |
|
||||||
|
| **9** | DEV # | INP # | OUT # | NXT | | | | | | | | | | | | | |
|
||||||
|
| **A** | DEV ind | INP ind | OUT ind | NXT | | | | | | | | | | | | | |
|
||||||
|
| **F** | _RSL A_ | _RSR A_ | _ASL A_ | _ASR A_ | | | | | | | | | | | | | |
|
||||||
|
|
||||||
|
<mark>LDH, LDH, STH, STH</mark>
|
||||||
|
|
||||||
|
Operations in italics are extensions to the core set of operations.
|
||||||
|
|
||||||
|
High byte reference:
|
||||||
|
|
||||||
|
| g, m | bin | hex |
|
||||||
|
|------|------|-----|
|
||||||
|
| 0, 0 | 0000 | 0 |
|
||||||
|
| 1, 1 | 0101 | 5 |
|
||||||
|
| 1, 2 | 0110 | 6 |
|
||||||
|
| 2, 1 | 1001 | 9 |
|
||||||
|
| 2, 2 | 1010 | A |
|
||||||
|
| 3, 3 | 1111 | F |
|
||||||
|
|
||||||
|
Brief legend for mnemonics:
|
||||||
|
|
||||||
|
- RSL/RSR: Ring Shift Left/Right
|
||||||
|
- HLT/HGT: Jump Less/Greater Than
|
||||||
|
- DEV: select IO device
|
||||||
|
- NXT: "next" - move to next line / card
|
||||||
|
|
||||||
|
TODO: format/document better:
|
||||||
|
|
||||||
|
1. core computational operations: low nibbles of 0x, 5x, 6x
|
||||||
|
2. arithmetic extension (optional): MUL, DIV
|
||||||
|
3. IO extension (optional): 9x, Ax
|
||||||
|
4. bitwise arithmetic extension (optional): NOT, AND, OR, XOR and RSL, RSR, ASL, ASR
|
||||||
|
5. control flow extension (optional): JLT, JGT
|
||||||
|
|
||||||
|
- The mainframe system implements at least 1, 2, and 3
|
||||||
|
- The microprocessor trainer implements 1
|
||||||
|
- (see note dated 2023-09-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Connections (pinout)
|
||||||
|
|
||||||
|
<mark>TBC</mark>
|
||||||
|
|
||||||
|
| name | in/out? | description |
|
||||||
|
|-----------|---------|---------------|
|
||||||
|
| RST | in | *reset* |
|
||||||
|
| VCC | in | *power* |
|
||||||
|
| GND | in | *ground* |
|
||||||
|
| CLK | in | *clock* |
|
||||||
|
| A0 - A7 | out | *address bus* |
|
||||||
|
| D0 - D7 | out | *data bus* |
|
||||||
|
| ABE | out | *address bus enable*: <br> low when the CPU is using the address bus |
|
||||||
|
| DBE | out | *data bus enable*: <br> low when the CPU is using the data bus |
|
||||||
|
| WAIT | in | *wait* — when pulled low, <br> the current operation is completed <br> and then execution pauses |
|
||||||
|
| /RD | out | TODO |
|
||||||
|
| /WR | out | |
|
||||||
|
| M/IO | out | |
|
||||||
|
|
||||||
|
|
||||||
|
### Start/Reset behaviour
|
||||||
|
|
||||||
|
When starting up, the CPU reads the program counter from $FF.
|
||||||
|
(Effectively executing a `JMP $FF`.)
|
||||||
|
|
||||||
|
<mark>TODO: currently the simulator doesn't actually do this</mark>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Assembly language
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
ADD $01 ; comments follow a `;`
|
||||||
|
|
||||||
|
ADD $FF ; this is direct addressing
|
||||||
|
ADD ($CC) ; this is indirect addressing
|
||||||
|
|
||||||
|
END ; END and NOP don't require operands
|
||||||
|
; (the assembler will fill in a default value of 0)
|
||||||
|
|
||||||
|
@subroutine ; create a label
|
||||||
|
ADD $01 ; (it must be on the line before the code it names)
|
||||||
|
ADD $02
|
||||||
|
|
||||||
|
JMP @subroutine ; use a label as operand
|
||||||
|
; the label will be replaced with
|
||||||
|
; the address of the label
|
||||||
|
|
||||||
|
#foo $FF ; define a constant
|
||||||
|
; (must be defined before it is referenced)
|
||||||
|
|
||||||
|
ADD #foo ; use a constant as an operand
|
||||||
|
|
||||||
|
LDA * ; `*` is a special label referencing the memory address
|
||||||
|
; where the current line will be stored after assembly
|
||||||
|
|
||||||
|
- Prefix hexadecimal numbers with `$` (or `0x`)
|
||||||
|
- Prefix binary numbers with `0b`
|
||||||
|
- Whitespace is ignored
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
## [DRAFT] Design of the _Cardiograph Mark I_ mainframe computer
|
||||||
|
|
||||||
|
The components of a Mark I are:
|
||||||
|
|
||||||
|
- a CG 101 Central Processing Unit
|
||||||
|
- a CG 102 Core Memory Unit
|
||||||
|
- a CG 103 Print-Key-Punch
|
||||||
|
- a CG 104 Matrix Display
|
||||||
|
|
||||||
|
### Operator console
|
||||||
|
|
||||||
|
<mark>TBC TBC TBC</mark>
|
||||||
|
|
||||||
|
## Basic controls
|
||||||
|
|
||||||
|
- Power
|
||||||
|
- Load
|
||||||
|
- Run
|
||||||
|
- Halt
|
||||||
|
- 4 Sense Switches
|
||||||
|
|
||||||
|
## Status lights
|
||||||
|
|
||||||
|
- 8 Accumulator lights
|
||||||
|
- 8 Address lights
|
||||||
|
- 8 Data lights
|
||||||
|
- 8 Instruction Pointer lights (<mark>review IP size?</mark>)
|
||||||
|
- 8 Status Register lights
|
||||||
|
|
||||||
|
## Debugging controls
|
||||||
|
|
||||||
|
- Run Single Step
|
||||||
|
- Memory Read
|
||||||
|
- Memory Read Next
|
||||||
|
- Memory Write
|
||||||
|
- Memory Write Next
|
||||||
|
|
||||||
|
## IO programming
|
||||||
|
|
||||||
|
Only one input or output device can be accessed at a time.
|
||||||
|
|
||||||
|
### Reading data
|
||||||
|
|
||||||
|
1. Use `DEV xx` to select input device _xx_
|
||||||
|
2. Use `INP yy` to read one byte into memory at address _yy_
|
||||||
|
|
||||||
|
<mark>TODO: find a way to allow the input device to refuse to provide input</mark>
|
||||||
|
|
||||||
|
### Writing data
|
||||||
|
|
||||||
|
1. Use `DEV xx` to select output device _xx_
|
||||||
|
2. Use `OUT yy` to write one byte from memory at address _yy_
|
||||||
|
3. Use `NXT xx` to...
|
||||||
|
- card punch: load a new card
|
||||||
|
- printer: begin a new line (CR, LF)
|
||||||
|
- display: begin a new line
|
||||||
|
|
||||||
|
### Punched card format
|
||||||
|
|
||||||
|
FIXME:
|
||||||
|
- ~~Cards are punched in EBCDIC~~
|
||||||
|
- ~~EBCDIC data is translated into binary by the card reader/punch~~
|
||||||
|
- Only columns 1-64 are used (for a maximum of 64 bytes of data per card)
|
||||||
|
|
||||||
|
### Printer format
|
||||||
|
|
||||||
|
- The printer format is the same as the card format
|
||||||
|
- One line of printing is equivalent to one card
|
||||||
|
- The printer can print up to 64 characters per line
|
||||||
|
|
||||||
|
### Matrix display format
|
||||||
|
|
||||||
|
- The display is a 5x5 grid of lights
|
||||||
|
- Each light has 16 possible brightness levels (0 = off, 15 = maximum)
|
||||||
|
- The display is written one line at a time
|
||||||
|
- After the display is selected with `DEV`, writing begins on the top line
|
||||||
|
- Writing wraps around and begins at the top again, if more than 5 lines are written
|
||||||
|
|
||||||
|
### Device numbers
|
||||||
|
|
||||||
|
1. card reader / typewriter
|
||||||
|
2. card punch / line printer
|
||||||
|
3. display
|
||||||
|
|
||||||
|
### Print-Key-Punch configurations
|
||||||
|
|
||||||
|
A dial allows you to select which input device to connect to the CPU:
|
||||||
|
|
||||||
|
1. none
|
||||||
|
2. card reader
|
||||||
|
3. keyboard
|
||||||
|
|
||||||
|
A similar dial selects the output device to connect:
|
||||||
|
|
||||||
|
1. none
|
||||||
|
2. card punch
|
||||||
|
3. printer
|
||||||
|
|
||||||
|
Thus, this all-in-one device allows the following configurations:
|
||||||
|
|
||||||
|
| | printer | card punch | none |
|
||||||
|
|-----------------|----------------------|------------------|--------------------------|
|
||||||
|
| **keyboard** | ***teletypewriter*** | ***auto punch*** | ***keypunch (offline)*** |
|
||||||
|
| **card reader** | (keys + print) | card duplicator | (card reader) |
|
||||||
|
| **none** | line printer | (auto punch) | (scrap metal) |
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# [DRAFT] Design for the _MicroCardiograph_ microprocessor trainer
|
||||||
|
|
||||||
|
The MicroCardiograph uses memory-mapped IO.
|
||||||
|
|
||||||
|
## Memory map
|
||||||
|
|
||||||
|
| Address | Used for... |
|
||||||
|
|----------|-----------------------------------------------|
|
||||||
|
| 00 to 19 | display (5x5) |
|
||||||
|
| 1A | pointer to display memory |
|
||||||
|
| 1B | keypad: value of latest key pressed |
|
||||||
|
| 1C | reserved for future use (bank switching flag) |
|
||||||
|
| 1D | initial IP |
|
||||||
|
| 1D to FE | free |
|
||||||
|
| FF | * ROM (unwriteable) pointer to initial IP |
|
||||||
|
|
||||||
|
\* Not implemented yet
|
||||||
|
|
||||||
|
|
||||||
|
## Peripherals
|
||||||
|
|
||||||
|
### Keypad
|
||||||
|
|
||||||
|
The value of the latest keypress on a hex keypad is stored at `$1B`.
|
||||||
|
|
||||||
|
The keypad uses the same layout as the COSMAC VIP (and CHIP-8). The CPU simulator maps those keys onto a Qwerty set:
|
||||||
|
|
||||||
|
`1` `2` `3` `C` = `1` `2` `3` `4`
|
||||||
|
`4` `5` `6` `D` = `Q` `W` `E` `R`
|
||||||
|
`7` `8` `9` `E` = `A` `S` `D` `F`
|
||||||
|
`A` `0` `B` `F` = `Z` `X` `C` `V`
|
||||||
|
|
||||||
|
The arrow keys are also mapped onto the hex keypad:
|
||||||
|
|
||||||
|
` ` `5` ` ` = ` ` `↑` ` `
|
||||||
|
`7` `8` `9` = `←` `↓` `→`
|
||||||
|
|
||||||
|
### Keypad as monitor input
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
# Cardiograph Mark I — simulator for an imaginary computer
|
|
||||||
|
|
||||||
Cardiograph is an imaginary computer. It has three main components:
|
|
||||||
|
|
||||||
1. the CPU, *Card* (short for 'Completely Analogue Risc Machine')
|
|
||||||
2. an input-output processor, *IO*
|
|
||||||
3. a display, *Graph*
|
|
||||||
|
|
||||||
## Simulator
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
Cardiograph is an imaginary computer. It has three main components:
|
|
||||||
|
|
||||||
1. the CPU, *Card* (short for 'Completely Analogue Risc Machine')
|
|
||||||
2. an input-output processor, *IO*
|
|
||||||
3. a display, *Graph*
|
|
||||||
|
|
||||||
## Simulator
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
- Node.js
|
|
||||||
|
|
||||||
|
|
||||||
### Quick examples
|
|
||||||
|
|
||||||
Assemble and run:
|
|
||||||
```./assembler.js -i <source.asm> | ./cardiograph.js```
|
|
||||||
|
|
||||||
Assemble to a file:
|
|
||||||
```./assembler.js -i <source.asm> -o <machinecode.out>```
|
|
||||||
|
|
||||||
Run from a file:
|
|
||||||
```./cardiograph.js -i <machinecode.out>```
|
|
||||||
|
|
||||||
|
|
||||||
### Assembler: assembler.js
|
|
||||||
|
|
||||||
```
|
|
||||||
Usage: ./assembler.js [-a] -i <input-file> [-o <output-file>]
|
|
||||||
|
|
||||||
-a, --annotate Output code with debugging annotations
|
|
||||||
-i, --in <file> Assembly-language input
|
|
||||||
-o, --out <file> Machine-code output
|
|
||||||
```
|
|
||||||
|
|
||||||
- If an output file is not provided, the output is printed to stdout
|
|
||||||
|
|
||||||
- If the `annotate` flag is not set, the machine code is returned as a string of space-separated decimal numbers
|
|
||||||
|
|
||||||
|
|
||||||
### Simulator: cardiograph.js
|
|
||||||
|
|
||||||
```
|
|
||||||
Usage: ./cardiograph.js [-i <file>]
|
|
||||||
|
|
||||||
-i, --in <file> Machine-code input
|
|
||||||
```
|
|
||||||
|
|
||||||
- If an input file is not provided, the input is read from stdin
|
|
||||||
|
|
||||||
|
|
||||||
## CPU
|
|
||||||
|
|
||||||
### Registers and Flags
|
|
||||||
|
|
||||||
There are three registers:
|
|
||||||
|
|
||||||
1. **A**, an 8-bit accumulator
|
|
||||||
2. **IP**, an 8-bit instruction pointer (aka program counter)
|
|
||||||
3. **flags**, a 4-bit flag register
|
|
||||||
|
|
||||||
The four flags are **O**verflow, **N**egative, **Z**ero, and **C**arry.
|
|
||||||
|
|
||||||
(Overflow is the high bit and carry is the low bit.)
|
|
||||||
|
|
||||||
In decimal:
|
|
||||||
|
|
||||||
| O | N | Z | C |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 3 | 2 | 1 | 0 |
|
|
||||||
|
|
||||||
|
|
||||||
### Instruction set
|
|
||||||
|
|
||||||
#### Operations
|
|
||||||
|
|
||||||
```
|
|
||||||
Hex Mnem. Operand Effect
|
|
||||||
|
|
||||||
00 END (ignored) Halt CPU
|
|
||||||
01 STO literal # mem[lit#] = A
|
|
||||||
02 STO address mem[mem[addr]] = A
|
|
||||||
03 LDA literal # A = lit#
|
|
||||||
04 LDA address A = addr
|
|
||||||
05 ADD literal # A = A + lit#
|
|
||||||
06 ADD address A = A + mem[addr]
|
|
||||||
07 SUB literal # A = A - lit#
|
|
||||||
08 SUB address A = A - mem[addr]
|
|
||||||
09 HOP literal # If A == lit#, skip next op (IP += 4)
|
|
||||||
0A HOP address If A == mem[addr], skip next instruction (IP += 4)
|
|
||||||
0B JMP literal # IP = lit#
|
|
||||||
0C JMP address IP = mem[addr]
|
|
||||||
0D FTG literal # Toggle flag, where flag number == lit#
|
|
||||||
0E FHP literal # Skip next op if flag is set, where flag number == lit#
|
|
||||||
0F NOP (ignored) None
|
|
||||||
```
|
|
||||||
|
|
||||||
- Instructions are two bytes long:
|
|
||||||
one byte for the opcode, one for the operand
|
|
||||||
|
|
||||||
|
|
||||||
#### Effects on memory, flags, registers
|
|
||||||
|
|
||||||
```
|
|
||||||
op mem flags IP
|
|
||||||
|
|
||||||
END +2
|
|
||||||
NOP +2
|
|
||||||
|
|
||||||
STO w +2
|
|
||||||
LDA r NZ +2
|
|
||||||
ADD ONZC +2
|
|
||||||
SUB ONZC +2
|
|
||||||
HOP +2/+4
|
|
||||||
JMP arg
|
|
||||||
FTG ONZC +2
|
|
||||||
FHP ONZC +2/+4
|
|
||||||
|
|
||||||
STO r,w +2
|
|
||||||
LDA r,r NZ +2
|
|
||||||
ADD r ONZC +2
|
|
||||||
SUB r ONZC +2
|
|
||||||
HOP r +2/+4
|
|
||||||
JMP r arg
|
|
||||||
FTG r ONZC +2
|
|
||||||
FHP r ONZC +2/+4
|
|
||||||
```
|
|
||||||
|
|
||||||
### Start-up
|
|
||||||
|
|
||||||
When starting up, the CPU executes a `JMP $FF`.
|
|
||||||
|
|
||||||
Put differently: it starts executing instructions at the address contained in `$FF`.
|
|
||||||
|
|
||||||
<mark>TODO: currently the simulator doesn't actually do this</mark>
|
|
||||||
|
|
||||||
|
|
||||||
### Assembly language
|
|
||||||
|
|
||||||
ADD $01 ; comments follow a `;`
|
|
||||||
|
|
||||||
ADD $FF ; this is direct addressing
|
|
||||||
ADD ($CC) ; this is indirect addressing
|
|
||||||
|
|
||||||
END ; END and NOP don't require operands
|
|
||||||
; (the assembler will fill in a default value of 0)
|
|
||||||
|
|
||||||
@subroutine ; create a label
|
|
||||||
ADD $01 ; (it must be on the line before the code it names)
|
|
||||||
ADD $02
|
|
||||||
|
|
||||||
JMP @subroutine ; use a label as operand
|
|
||||||
; the label will be replaced with
|
|
||||||
; the address of the label
|
|
||||||
|
|
||||||
#foo $FF ; define a constant
|
|
||||||
; (must be defined before it is referenced)
|
|
||||||
|
|
||||||
ADD #foo ; use a constant as an operand
|
|
||||||
|
|
||||||
LDA * ; `*` is a special label referencing the memory address
|
|
||||||
; where the current line will be stored after assembly
|
|
||||||
|
|
||||||
- Prefix hexadecimal numbers with `$` (or `0x`)
|
|
||||||
- Prefix binary numbers with `0b`
|
|
||||||
- Whitespace is ignored
|
|
||||||
|
|
||||||
## Cardiograph memory map
|
|
||||||
|
|
||||||
| Address | Used for... |
|
|
||||||
|----------|-----------------------------------------------|
|
|
||||||
| 00 to 19 | display (5x5) |
|
|
||||||
| 1A | pointer to display memory |
|
|
||||||
| 1B | keypad: value of latest key pressed |
|
|
||||||
| 1C | reserved for future use (bank switching flag) |
|
|
||||||
| 1D | initial IP |
|
|
||||||
| 1D to FE | free |
|
|
||||||
| FF | * ROM (unwriteable) pointer to initial IP |
|
|
||||||
|
|
||||||
\* Not implemented yet
|
|
||||||
|
|
||||||
|
|
||||||
## Cardiograph peripherals
|
|
||||||
|
|
||||||
### Keypad
|
|
||||||
|
|
||||||
The value of the latest keypress on a hex keypad is stored at `$1B`.
|
|
||||||
|
|
||||||
The keypad uses the same layout as the COSMAC VIP (and CHIP-8). The CPU simulator maps those keys onto a Qwerty set:
|
|
||||||
|
|
||||||
`1` `2` `3` `C` = `1` `2` `3` `4`
|
|
||||||
`4` `5` `6` `D` = `Q` `W` `E` `R`
|
|
||||||
`7` `8` `9` `E` = `A` `S` `D` `F`
|
|
||||||
`A` `0` `B` `F` = `Z` `X` `C` `V`
|
|
||||||
|
|
||||||
The arrow keys are also mapped onto the hex keypad:
|
|
||||||
|
|
||||||
` ` `5` ` ` = ` ` `↑` ` `
|
|
||||||
`7` `8` `9` = `←` `↓` `→`
|
|
||||||
|
|
@ -1,474 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2020 Tim C, 2021 Jeff Epler for Adafruit Industries
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
`adafruit_display_text`
|
|
||||||
=======================
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = "3.2.2"
|
|
||||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
|
||||||
|
|
||||||
from displayio import Group, Palette
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import Optional, List, Tuple
|
|
||||||
from fontio import FontProtocol
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_text_to_pixels(
|
|
||||||
string: str,
|
|
||||||
max_width: int,
|
|
||||||
font: Optional[FontProtocol] = None,
|
|
||||||
indent0: str = "",
|
|
||||||
indent1: str = "",
|
|
||||||
) -> List[str]:
|
|
||||||
# pylint: disable=too-many-branches, too-many-locals, too-many-nested-blocks, too-many-statements
|
|
||||||
|
|
||||||
"""wrap_text_to_pixels function
|
|
||||||
A helper that will return a list of lines with word-break wrapping.
|
|
||||||
Leading and trailing whitespace in your string will be removed. If
|
|
||||||
you wish to use leading whitespace see ``indent0`` and ``indent1``
|
|
||||||
parameters.
|
|
||||||
|
|
||||||
:param str string: The text to be wrapped.
|
|
||||||
:param int max_width: The maximum number of pixels on a line before wrapping.
|
|
||||||
:param font: The font to use for measuring the text.
|
|
||||||
:type font: ~fontio.FontProtocol
|
|
||||||
:param str indent0: Additional character(s) to add to the first line.
|
|
||||||
:param str indent1: Additional character(s) to add to all other lines.
|
|
||||||
|
|
||||||
:return: A list of the lines resulting from wrapping the
|
|
||||||
input text at ``max_width`` pixels size
|
|
||||||
:rtype: List[str]
|
|
||||||
|
|
||||||
"""
|
|
||||||
if font is None:
|
|
||||||
|
|
||||||
def measure(text):
|
|
||||||
return len(text)
|
|
||||||
|
|
||||||
else:
|
|
||||||
if hasattr(font, "load_glyphs"):
|
|
||||||
font.load_glyphs(string)
|
|
||||||
|
|
||||||
def measure(text):
|
|
||||||
total_len = 0
|
|
||||||
for char in text:
|
|
||||||
this_glyph = font.get_glyph(ord(char))
|
|
||||||
if this_glyph:
|
|
||||||
total_len += this_glyph.shift_x
|
|
||||||
return total_len
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
partial = [indent0]
|
|
||||||
width = measure(indent0)
|
|
||||||
swidth = measure(" ")
|
|
||||||
firstword = True
|
|
||||||
for line_in_input in string.split("\n"):
|
|
||||||
newline = True
|
|
||||||
for index, word in enumerate(line_in_input.split(" ")):
|
|
||||||
wwidth = measure(word)
|
|
||||||
word_parts = []
|
|
||||||
cur_part = ""
|
|
||||||
|
|
||||||
if wwidth > max_width:
|
|
||||||
for char in word:
|
|
||||||
if newline:
|
|
||||||
extraspace = 0
|
|
||||||
leadchar = ""
|
|
||||||
else:
|
|
||||||
extraspace = swidth
|
|
||||||
leadchar = " "
|
|
||||||
if (
|
|
||||||
measure("".join(partial))
|
|
||||||
+ measure(cur_part)
|
|
||||||
+ measure(char)
|
|
||||||
+ measure("-")
|
|
||||||
+ extraspace
|
|
||||||
> max_width
|
|
||||||
):
|
|
||||||
if cur_part:
|
|
||||||
word_parts.append(
|
|
||||||
"".join(partial) + leadchar + cur_part + "-"
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
word_parts.append("".join(partial))
|
|
||||||
cur_part = char
|
|
||||||
partial = [indent1]
|
|
||||||
newline = True
|
|
||||||
else:
|
|
||||||
cur_part += char
|
|
||||||
if cur_part:
|
|
||||||
word_parts.append(cur_part)
|
|
||||||
for line in word_parts[:-1]:
|
|
||||||
lines.append(line)
|
|
||||||
partial.append(word_parts[-1])
|
|
||||||
width = measure(word_parts[-1])
|
|
||||||
if firstword:
|
|
||||||
firstword = False
|
|
||||||
else:
|
|
||||||
if firstword:
|
|
||||||
partial.append(word)
|
|
||||||
firstword = False
|
|
||||||
width += wwidth
|
|
||||||
elif width + swidth + wwidth < max_width:
|
|
||||||
if index > 0:
|
|
||||||
partial.append(" ")
|
|
||||||
partial.append(word)
|
|
||||||
width += wwidth + swidth
|
|
||||||
else:
|
|
||||||
lines.append("".join(partial))
|
|
||||||
partial = [indent1, word]
|
|
||||||
width = measure(indent1) + wwidth
|
|
||||||
if newline:
|
|
||||||
newline = False
|
|
||||||
|
|
||||||
lines.append("".join(partial))
|
|
||||||
partial = [indent1]
|
|
||||||
width = measure(indent1)
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_text_to_lines(string: str, max_chars: int) -> List[str]:
|
|
||||||
"""wrap_text_to_lines function
|
|
||||||
A helper that will return a list of lines with word-break wrapping
|
|
||||||
|
|
||||||
:param str string: The text to be wrapped
|
|
||||||
:param int max_chars: The maximum number of characters on a line before wrapping
|
|
||||||
|
|
||||||
:return: A list of lines where each line is separated based on the amount
|
|
||||||
of ``max_chars`` provided
|
|
||||||
:rtype: List[str]
|
|
||||||
"""
|
|
||||||
|
|
||||||
def chunks(lst, n):
|
|
||||||
"""Yield successive n-sized chunks from lst."""
|
|
||||||
for i in range(0, len(lst), n):
|
|
||||||
yield lst[i : i + n]
|
|
||||||
|
|
||||||
string = string.replace("\n", "").replace("\r", "") # Strip confusing newlines
|
|
||||||
words = string.split(" ")
|
|
||||||
the_lines = []
|
|
||||||
the_line = ""
|
|
||||||
for w in words:
|
|
||||||
if len(w) > max_chars:
|
|
||||||
if the_line: # add what we had stored
|
|
||||||
the_lines.append(the_line)
|
|
||||||
parts = []
|
|
||||||
for part in chunks(w, max_chars - 1):
|
|
||||||
parts.append("{}-".format(part))
|
|
||||||
the_lines.extend(parts[:-1])
|
|
||||||
the_line = parts[-1][:-1]
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(the_line + " " + w) <= max_chars:
|
|
||||||
the_line += " " + w
|
|
||||||
elif not the_line and len(w) == max_chars:
|
|
||||||
the_lines.append(w)
|
|
||||||
else:
|
|
||||||
the_lines.append(the_line)
|
|
||||||
the_line = "" + w
|
|
||||||
if the_line: # Last line remaining
|
|
||||||
the_lines.append(the_line)
|
|
||||||
# Remove any blank lines
|
|
||||||
while not the_lines[0]:
|
|
||||||
del the_lines[0]
|
|
||||||
# Remove first space from first line:
|
|
||||||
if the_lines[0][0] == " ":
|
|
||||||
the_lines[0] = the_lines[0][1:]
|
|
||||||
return the_lines
|
|
||||||
|
|
||||||
|
|
||||||
class LabelBase(Group):
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
|
|
||||||
"""Superclass that all other types of labels will extend. This contains
|
|
||||||
all of the properties and functions that work the same way in all labels.
|
|
||||||
|
|
||||||
**Note:** This should be treated as an abstract base class.
|
|
||||||
|
|
||||||
Subclasses should implement ``_set_text``, ``_set_font``, and ``_set_line_spacing`` to
|
|
||||||
have the correct behavior for that type of label.
|
|
||||||
|
|
||||||
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
|
||||||
Must include a capital M for measuring character size.
|
|
||||||
:type font: ~fontio.FontProtocol
|
|
||||||
:param str text: Text to display
|
|
||||||
:param int color: Color of all text in RGB hex
|
|
||||||
:param int background_color: Color of the background, use `None` for transparent
|
|
||||||
:param float line_spacing: Line spacing of text to display
|
|
||||||
:param bool background_tight: Set `True` only if you want background box to tightly
|
|
||||||
surround text. When set to 'True' Padding parameters will be ignored.
|
|
||||||
:param int padding_top: Additional pixels added to background bounding box at top
|
|
||||||
:param int padding_bottom: Additional pixels added to background bounding box at bottom
|
|
||||||
:param int padding_left: Additional pixels added to background bounding box at left
|
|
||||||
:param int padding_right: Additional pixels added to background bounding box at right
|
|
||||||
:param (float,float) anchor_point: Point that anchored_position moves relative to.
|
|
||||||
Tuple with decimal percentage of width and height.
|
|
||||||
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
|
||||||
:param (int,int) anchored_position: Position relative to the anchor_point. Tuple
|
|
||||||
containing x,y pixel coordinates.
|
|
||||||
:param int scale: Integer value of the pixel scaling
|
|
||||||
:param bool base_alignment: when True allows to align text label to the baseline.
|
|
||||||
This is helpful when two or more labels need to be aligned to the same baseline
|
|
||||||
:param (int,str) tab_replacement: tuple with tab character replace information. When
|
|
||||||
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
|
||||||
tab character
|
|
||||||
:param str label_direction: string defining the label text orientation. See the
|
|
||||||
subclass documentation for the possible values.
|
|
||||||
:param bool verbose: print debugging information in some internal functions. Default to False
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
font: FontProtocol,
|
|
||||||
x: int = 0,
|
|
||||||
y: int = 0,
|
|
||||||
text: str = "",
|
|
||||||
color: int = 0xFFFFFF,
|
|
||||||
background_color: int = None,
|
|
||||||
line_spacing: float = 1.25,
|
|
||||||
background_tight: bool = False,
|
|
||||||
padding_top: int = 0,
|
|
||||||
padding_bottom: int = 0,
|
|
||||||
padding_left: int = 0,
|
|
||||||
padding_right: int = 0,
|
|
||||||
anchor_point: Tuple[float, float] = None,
|
|
||||||
anchored_position: Tuple[int, int] = None,
|
|
||||||
scale: int = 1,
|
|
||||||
base_alignment: bool = False,
|
|
||||||
tab_replacement: Tuple[int, str] = (4, " "),
|
|
||||||
label_direction: str = "LTR",
|
|
||||||
verbose: bool = False,
|
|
||||||
) -> None:
|
|
||||||
# pylint: disable=too-many-arguments, too-many-locals
|
|
||||||
|
|
||||||
super().__init__(x=x, y=y, scale=1)
|
|
||||||
|
|
||||||
self._font = font
|
|
||||||
self._text = text
|
|
||||||
self._palette = Palette(2)
|
|
||||||
self._color = 0xFFFFFF
|
|
||||||
self._background_color = None
|
|
||||||
self._line_spacing = line_spacing
|
|
||||||
self._background_tight = background_tight
|
|
||||||
self._padding_top = padding_top
|
|
||||||
self._padding_bottom = padding_bottom
|
|
||||||
self._padding_left = padding_left
|
|
||||||
self._padding_right = padding_right
|
|
||||||
self._anchor_point = anchor_point
|
|
||||||
self._anchored_position = anchored_position
|
|
||||||
self._base_alignment = base_alignment
|
|
||||||
self._label_direction = label_direction
|
|
||||||
self._tab_replacement = tab_replacement
|
|
||||||
self._tab_text = self._tab_replacement[1] * self._tab_replacement[0]
|
|
||||||
self._verbose = verbose
|
|
||||||
|
|
||||||
self._ascent, self._descent = self._get_ascent_descent()
|
|
||||||
self._bounding_box = None
|
|
||||||
|
|
||||||
self.color = color
|
|
||||||
self.background_color = background_color
|
|
||||||
|
|
||||||
# local group will hold background and text
|
|
||||||
# the self group scale should always remain at 1, the self._local_group will
|
|
||||||
# be used to set the scale of the label
|
|
||||||
self._local_group = Group(scale=scale)
|
|
||||||
self.append(self._local_group)
|
|
||||||
|
|
||||||
self._baseline = -1.0
|
|
||||||
|
|
||||||
if self._base_alignment:
|
|
||||||
self._y_offset = 0
|
|
||||||
else:
|
|
||||||
self._y_offset = self._ascent // 2
|
|
||||||
|
|
||||||
def _get_ascent_descent(self) -> Tuple[int, int]:
|
|
||||||
"""Private function to calculate ascent and descent font values"""
|
|
||||||
if hasattr(self.font, "ascent") and hasattr(self.font, "descent"):
|
|
||||||
return self.font.ascent, self.font.descent
|
|
||||||
|
|
||||||
# check a few glyphs for maximum ascender and descender height
|
|
||||||
glyphs = "M j'" # choose glyphs with highest ascender and lowest
|
|
||||||
try:
|
|
||||||
self._font.load_glyphs(glyphs)
|
|
||||||
except AttributeError:
|
|
||||||
# Builtin font doesn't have or need load_glyphs
|
|
||||||
pass
|
|
||||||
# descender, will depend upon font used
|
|
||||||
ascender_max = descender_max = 0
|
|
||||||
for char in glyphs:
|
|
||||||
this_glyph = self._font.get_glyph(ord(char))
|
|
||||||
if this_glyph:
|
|
||||||
ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy)
|
|
||||||
descender_max = max(descender_max, -this_glyph.dy)
|
|
||||||
return ascender_max, descender_max
|
|
||||||
|
|
||||||
@property
|
|
||||||
def font(self) -> FontProtocol:
|
|
||||||
"""Font to use for text display."""
|
|
||||||
return self._font
|
|
||||||
|
|
||||||
def _set_font(self, new_font: FontProtocol) -> None:
|
|
||||||
raise NotImplementedError("{} MUST override '_set_font'".format(type(self)))
|
|
||||||
|
|
||||||
@font.setter
|
|
||||||
def font(self, new_font: FontProtocol) -> None:
|
|
||||||
self._set_font(new_font)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color(self) -> int:
|
|
||||||
"""Color of the text as an RGB hex number."""
|
|
||||||
return self._color
|
|
||||||
|
|
||||||
@color.setter
|
|
||||||
def color(self, new_color: int):
|
|
||||||
self._color = new_color
|
|
||||||
if new_color is not None:
|
|
||||||
self._palette[1] = new_color
|
|
||||||
self._palette.make_opaque(1)
|
|
||||||
else:
|
|
||||||
self._palette[1] = 0
|
|
||||||
self._palette.make_transparent(1)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def background_color(self) -> int:
|
|
||||||
"""Color of the background as an RGB hex number."""
|
|
||||||
return self._background_color
|
|
||||||
|
|
||||||
def _set_background_color(self, new_color):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"{} MUST override '_set_background_color'".format(type(self))
|
|
||||||
)
|
|
||||||
|
|
||||||
@background_color.setter
|
|
||||||
def background_color(self, new_color: int) -> None:
|
|
||||||
self._set_background_color(new_color)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def anchor_point(self) -> Tuple[float, float]:
|
|
||||||
"""Point that anchored_position moves relative to.
|
|
||||||
Tuple with decimal percentage of width and height.
|
|
||||||
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)"""
|
|
||||||
return self._anchor_point
|
|
||||||
|
|
||||||
@anchor_point.setter
|
|
||||||
def anchor_point(self, new_anchor_point: Tuple[float, float]) -> None:
|
|
||||||
if new_anchor_point[1] == self._baseline:
|
|
||||||
self._anchor_point = (new_anchor_point[0], -1.0)
|
|
||||||
else:
|
|
||||||
self._anchor_point = new_anchor_point
|
|
||||||
|
|
||||||
# update the anchored_position using setter
|
|
||||||
self.anchored_position = self._anchored_position
|
|
||||||
|
|
||||||
@property
|
|
||||||
def anchored_position(self) -> Tuple[int, int]:
|
|
||||||
"""Position relative to the anchor_point. Tuple containing x,y
|
|
||||||
pixel coordinates."""
|
|
||||||
return self._anchored_position
|
|
||||||
|
|
||||||
@anchored_position.setter
|
|
||||||
def anchored_position(self, new_position: Tuple[int, int]) -> None:
|
|
||||||
self._anchored_position = new_position
|
|
||||||
# Calculate (x,y) position
|
|
||||||
if (self._anchor_point is not None) and (self._anchored_position is not None):
|
|
||||||
self.x = int(
|
|
||||||
new_position[0]
|
|
||||||
- (self._bounding_box[0] * self.scale)
|
|
||||||
- round(self._anchor_point[0] * (self._bounding_box[2] * self.scale))
|
|
||||||
)
|
|
||||||
if self._anchor_point[1] == self._baseline:
|
|
||||||
self.y = int(new_position[1] - (self._y_offset * self.scale))
|
|
||||||
else:
|
|
||||||
self.y = int(
|
|
||||||
new_position[1]
|
|
||||||
- (self._bounding_box[1] * self.scale)
|
|
||||||
- round(self._anchor_point[1] * self._bounding_box[3] * self.scale)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def scale(self) -> int:
|
|
||||||
"""Set the scaling of the label, in integer values"""
|
|
||||||
return self._local_group.scale
|
|
||||||
|
|
||||||
@scale.setter
|
|
||||||
def scale(self, new_scale: int) -> None:
|
|
||||||
self._local_group.scale = new_scale
|
|
||||||
self.anchored_position = self._anchored_position # update the anchored_position
|
|
||||||
|
|
||||||
def _set_text(self, new_text: str, scale: int) -> None:
|
|
||||||
raise NotImplementedError("{} MUST override '_set_text'".format(type(self)))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text(self) -> str:
|
|
||||||
"""Text to be displayed."""
|
|
||||||
return self._text
|
|
||||||
|
|
||||||
@text.setter # Cannot set color or background color with text setter, use separate setter
|
|
||||||
def text(self, new_text: str) -> None:
|
|
||||||
if new_text == self._text:
|
|
||||||
return
|
|
||||||
self._set_text(new_text, self.scale)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bounding_box(self) -> Tuple[int, int]:
|
|
||||||
"""An (x, y, w, h) tuple that completely covers all glyphs. The
|
|
||||||
first two numbers are offset from the x, y origin of this group"""
|
|
||||||
return tuple(self._bounding_box)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def height(self) -> int:
|
|
||||||
"""The height of the label determined from the bounding box."""
|
|
||||||
return self._bounding_box[3]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def width(self) -> int:
|
|
||||||
"""The width of the label determined from the bounding box."""
|
|
||||||
return self._bounding_box[2]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def line_spacing(self) -> float:
|
|
||||||
"""The amount of space between lines of text, in multiples of the font's
|
|
||||||
bounding-box height. (E.g. 1.0 is the bounding-box height)"""
|
|
||||||
return self._line_spacing
|
|
||||||
|
|
||||||
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"{} MUST override '_set_line_spacing'".format(type(self))
|
|
||||||
)
|
|
||||||
|
|
||||||
@line_spacing.setter
|
|
||||||
def line_spacing(self, new_line_spacing: float) -> None:
|
|
||||||
self._set_line_spacing(new_line_spacing)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def label_direction(self) -> str:
|
|
||||||
"""Set the text direction of the label"""
|
|
||||||
return self._label_direction
|
|
||||||
|
|
||||||
def _set_label_direction(self, new_label_direction: str) -> None:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"{} MUST override '_set_label_direction'".format(type(self))
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"{} MUST override '_get_valid_label_direction'".format(type(self))
|
|
||||||
)
|
|
||||||
|
|
||||||
@label_direction.setter
|
|
||||||
def label_direction(self, new_label_direction: str) -> None:
|
|
||||||
"""Set the text direction of the label"""
|
|
||||||
if new_label_direction not in self._get_valid_label_directions():
|
|
||||||
raise RuntimeError("Please provide a valid text direction")
|
|
||||||
self._set_label_direction(new_label_direction)
|
|
||||||
|
|
||||||
def _replace_tabs(self, text: str) -> str:
|
|
||||||
return text if text.find("\t") < 0 else self._tab_text.join(text.split("\t"))
|
|
||||||
|
|
@ -1,597 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2020 Kevin Matocha
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
`adafruit_display_text.bitmap_label`
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Text graphics handling for CircuitPython, including text boxes
|
|
||||||
|
|
||||||
|
|
||||||
* Author(s): Kevin Matocha
|
|
||||||
|
|
||||||
Implementation Notes
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
**Hardware:**
|
|
||||||
|
|
||||||
**Software and Dependencies:**
|
|
||||||
|
|
||||||
* Adafruit CircuitPython firmware for the supported boards:
|
|
||||||
https://circuitpython.org/downloads
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = "3.2.2"
|
|
||||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
|
||||||
|
|
||||||
import displayio
|
|
||||||
from adafruit_display_text import LabelBase
|
|
||||||
|
|
||||||
try:
|
|
||||||
import bitmaptools
|
|
||||||
except ImportError:
|
|
||||||
# We have a slower fallback for bitmaptools
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
from fontio import FontProtocol
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class Label(LabelBase):
|
|
||||||
"""A label displaying a string of text that is stored in a bitmap.
|
|
||||||
Note: This ``bitmap_label.py`` library utilizes a :py:class:`~displayio.Bitmap`
|
|
||||||
to display the text. This method is memory-conserving relative to ``label.py``.
|
|
||||||
|
|
||||||
For further reduction in memory usage, set ``save_text=False`` (text string will not
|
|
||||||
be stored and ``line_spacing`` and ``font`` are immutable with ``save_text``
|
|
||||||
set to ``False``).
|
|
||||||
|
|
||||||
The origin point set by ``x`` and ``y``
|
|
||||||
properties will be the left edge of the bounding box, and in the center of a M
|
|
||||||
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
|
|
||||||
it will try to have it be center-left as close as possible.
|
|
||||||
|
|
||||||
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
|
||||||
Must include a capital M for measuring character size.
|
|
||||||
:type font: ~fontio.FontProtocol
|
|
||||||
:param str text: Text to display
|
|
||||||
:param int|Tuple(int, int, int) color: Color of all text in HEX or RGB
|
|
||||||
:param int|Tuple(int, int, int)|None background_color: Color of the background, use `None`
|
|
||||||
for transparent
|
|
||||||
:param float line_spacing: Line spacing of text to display
|
|
||||||
:param bool background_tight: Set `True` only if you want background box to tightly
|
|
||||||
surround text. When set to 'True' Padding parameters will be ignored.
|
|
||||||
:param int padding_top: Additional pixels added to background bounding box at top
|
|
||||||
:param int padding_bottom: Additional pixels added to background bounding box at bottom
|
|
||||||
:param int padding_left: Additional pixels added to background bounding box at left
|
|
||||||
:param int padding_right: Additional pixels added to background bounding box at right
|
|
||||||
:param Tuple(float, float) anchor_point: Point that anchored_position moves relative to.
|
|
||||||
Tuple with decimal percentage of width and height.
|
|
||||||
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
|
||||||
:param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple
|
|
||||||
containing x,y pixel coordinates.
|
|
||||||
:param int scale: Integer value of the pixel scaling
|
|
||||||
:param bool save_text: Set True to save the text string as a constant in the
|
|
||||||
label structure. Set False to reduce memory use.
|
|
||||||
:param bool base_alignment: when True allows to align text label to the baseline.
|
|
||||||
This is helpful when two or more labels need to be aligned to the same baseline
|
|
||||||
:param Tuple(int, str) tab_replacement: tuple with tab character replace information. When
|
|
||||||
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
|
||||||
tab character
|
|
||||||
:param str label_direction: string defining the label text orientation. There are 5
|
|
||||||
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
|
|
||||||
``UPD``-Upside Down ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``
|
|
||||||
:param bool verbose: print debugging information in some internal functions. Default to False
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# This maps label_direction to TileGrid's transpose_xy, flip_x, flip_y
|
|
||||||
_DIR_MAP = {
|
|
||||||
"UPR": (True, True, False),
|
|
||||||
"DWR": (True, False, True),
|
|
||||||
"UPD": (False, True, True),
|
|
||||||
"LTR": (False, False, False),
|
|
||||||
"RTL": (False, False, False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, font: FontProtocol, save_text: bool = True, **kwargs) -> None:
|
|
||||||
self._bitmap = None
|
|
||||||
self._tilegrid = None
|
|
||||||
self._prev_label_direction = None
|
|
||||||
|
|
||||||
super().__init__(font, **kwargs)
|
|
||||||
|
|
||||||
self._save_text = save_text
|
|
||||||
self._text = self._replace_tabs(self._text)
|
|
||||||
|
|
||||||
# call the text updater with all the arguments.
|
|
||||||
self._reset_text(
|
|
||||||
font=font,
|
|
||||||
text=self._text,
|
|
||||||
line_spacing=self._line_spacing,
|
|
||||||
scale=self.scale,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _reset_text(
|
|
||||||
self,
|
|
||||||
font: Optional[FontProtocol] = None,
|
|
||||||
text: Optional[str] = None,
|
|
||||||
line_spacing: Optional[float] = None,
|
|
||||||
scale: Optional[int] = None,
|
|
||||||
) -> None:
|
|
||||||
# pylint: disable=too-many-branches, too-many-statements, too-many-locals
|
|
||||||
|
|
||||||
# Store all the instance variables
|
|
||||||
if font is not None:
|
|
||||||
self._font = font
|
|
||||||
if line_spacing is not None:
|
|
||||||
self._line_spacing = line_spacing
|
|
||||||
|
|
||||||
# if text is not provided as a parameter (text is None), use the previous value.
|
|
||||||
if (text is None) and self._save_text:
|
|
||||||
text = self._text
|
|
||||||
|
|
||||||
if self._save_text: # text string will be saved
|
|
||||||
self._text = self._replace_tabs(text)
|
|
||||||
else:
|
|
||||||
self._text = None # save a None value since text string is not saved
|
|
||||||
|
|
||||||
# Check for empty string
|
|
||||||
if (text == "") or (
|
|
||||||
text is None
|
|
||||||
): # If empty string, just create a zero-sized bounding box and that's it.
|
|
||||||
self._bounding_box = (
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0, # zero width with text == ""
|
|
||||||
0, # zero height with text == ""
|
|
||||||
)
|
|
||||||
# Clear out any items in the self._local_group Group, in case this is an
|
|
||||||
# update to the bitmap_label
|
|
||||||
for _ in self._local_group:
|
|
||||||
self._local_group.pop(0)
|
|
||||||
|
|
||||||
# Free the bitmap and tilegrid since they are removed
|
|
||||||
self._bitmap = None
|
|
||||||
self._tilegrid = None
|
|
||||||
|
|
||||||
else: # The text string is not empty, so create the Bitmap and TileGrid and
|
|
||||||
# append to the self Group
|
|
||||||
|
|
||||||
# Calculate the text bounding box
|
|
||||||
|
|
||||||
# Calculate both "tight" and "loose" bounding box dimensions to match label for
|
|
||||||
# anchor_position calculations
|
|
||||||
(
|
|
||||||
box_x,
|
|
||||||
tight_box_y,
|
|
||||||
x_offset,
|
|
||||||
tight_y_offset,
|
|
||||||
loose_box_y,
|
|
||||||
loose_y_offset,
|
|
||||||
) = self._text_bounding_box(
|
|
||||||
text,
|
|
||||||
self._font,
|
|
||||||
) # calculate the box size for a tight and loose backgrounds
|
|
||||||
|
|
||||||
if self._background_tight:
|
|
||||||
box_y = tight_box_y
|
|
||||||
y_offset = tight_y_offset
|
|
||||||
self._padding_left = 0
|
|
||||||
self._padding_right = 0
|
|
||||||
self._padding_top = 0
|
|
||||||
self._padding_bottom = 0
|
|
||||||
|
|
||||||
else: # calculate the box size for a loose background
|
|
||||||
box_y = loose_box_y
|
|
||||||
y_offset = loose_y_offset
|
|
||||||
|
|
||||||
# Calculate the background size including padding
|
|
||||||
tight_box_x = box_x
|
|
||||||
box_x = box_x + self._padding_left + self._padding_right
|
|
||||||
box_y = box_y + self._padding_top + self._padding_bottom
|
|
||||||
|
|
||||||
# Create the Bitmap unless it can be reused
|
|
||||||
new_bitmap = None
|
|
||||||
if (
|
|
||||||
self._bitmap is None
|
|
||||||
or self._bitmap.width != box_x
|
|
||||||
or self._bitmap.height != box_y
|
|
||||||
):
|
|
||||||
new_bitmap = displayio.Bitmap(box_x, box_y, len(self._palette))
|
|
||||||
self._bitmap = new_bitmap
|
|
||||||
else:
|
|
||||||
self._bitmap.fill(0)
|
|
||||||
|
|
||||||
# Place the text into the Bitmap
|
|
||||||
self._place_text(
|
|
||||||
self._bitmap,
|
|
||||||
text if self._label_direction != "RTL" else "".join(reversed(text)),
|
|
||||||
self._font,
|
|
||||||
self._padding_left - x_offset,
|
|
||||||
self._padding_top + y_offset,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._base_alignment:
|
|
||||||
label_position_yoffset = 0
|
|
||||||
else:
|
|
||||||
label_position_yoffset = self._ascent // 2
|
|
||||||
|
|
||||||
# Create the TileGrid if not created bitmap unchanged
|
|
||||||
if self._tilegrid is None or new_bitmap:
|
|
||||||
self._tilegrid = displayio.TileGrid(
|
|
||||||
self._bitmap,
|
|
||||||
pixel_shader=self._palette,
|
|
||||||
width=1,
|
|
||||||
height=1,
|
|
||||||
tile_width=box_x,
|
|
||||||
tile_height=box_y,
|
|
||||||
default_tile=0,
|
|
||||||
x=-self._padding_left + x_offset,
|
|
||||||
y=label_position_yoffset - y_offset - self._padding_top,
|
|
||||||
)
|
|
||||||
# Clear out any items in the local_group Group, in case this is an update to
|
|
||||||
# the bitmap_label
|
|
||||||
for _ in self._local_group:
|
|
||||||
self._local_group.pop(0)
|
|
||||||
self._local_group.append(
|
|
||||||
self._tilegrid
|
|
||||||
) # add the bitmap's tilegrid to the group
|
|
||||||
|
|
||||||
# Set TileGrid properties based on label_direction
|
|
||||||
if self._label_direction != self._prev_label_direction:
|
|
||||||
tg1 = self._tilegrid
|
|
||||||
tg1.transpose_xy, tg1.flip_x, tg1.flip_y = self._DIR_MAP[
|
|
||||||
self._label_direction
|
|
||||||
]
|
|
||||||
|
|
||||||
# Update bounding_box values. Note: To be consistent with label.py,
|
|
||||||
# this is the bounding box for the text only, not including the background.
|
|
||||||
if self._label_direction in ("UPR", "DWR"):
|
|
||||||
if self._label_direction == "UPR":
|
|
||||||
top = self._padding_right
|
|
||||||
left = self._padding_top
|
|
||||||
if self._label_direction == "DWR":
|
|
||||||
top = self._padding_left
|
|
||||||
left = self._padding_bottom
|
|
||||||
self._bounding_box = (
|
|
||||||
self._tilegrid.x + left,
|
|
||||||
self._tilegrid.y + top,
|
|
||||||
tight_box_y,
|
|
||||||
tight_box_x,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._bounding_box = (
|
|
||||||
self._tilegrid.x + self._padding_left,
|
|
||||||
self._tilegrid.y + self._padding_top,
|
|
||||||
tight_box_x,
|
|
||||||
tight_box_y,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
scale is not None
|
|
||||||
): # Scale will be defined in local_group (Note: self should have scale=1)
|
|
||||||
self.scale = scale # call the setter
|
|
||||||
|
|
||||||
# set the anchored_position with setter after bitmap is created, sets the
|
|
||||||
# x,y positions of the label
|
|
||||||
self.anchored_position = self._anchored_position
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _line_spacing_ypixels(font: FontProtocol, line_spacing: float) -> int:
|
|
||||||
# Note: Scaling is provided at the Group level
|
|
||||||
return_value = int(line_spacing * font.get_bounding_box()[1])
|
|
||||||
return return_value
|
|
||||||
|
|
||||||
def _text_bounding_box(
|
|
||||||
self, text: str, font: FontProtocol
|
|
||||||
) -> Tuple[int, int, int, int, int, int]:
|
|
||||||
# pylint: disable=too-many-locals,too-many-branches
|
|
||||||
|
|
||||||
bbox = font.get_bounding_box()
|
|
||||||
if len(bbox) == 4:
|
|
||||||
ascender_max, descender_max = bbox[1], -bbox[3]
|
|
||||||
else:
|
|
||||||
ascender_max, descender_max = self._ascent, self._descent
|
|
||||||
|
|
||||||
lines = 1
|
|
||||||
|
|
||||||
# starting x and y position (left margin)
|
|
||||||
xposition = x_start = yposition = y_start = 0
|
|
||||||
|
|
||||||
left = None
|
|
||||||
right = x_start
|
|
||||||
top = bottom = y_start
|
|
||||||
|
|
||||||
y_offset_tight = self._ascent // 2
|
|
||||||
|
|
||||||
newlines = 0
|
|
||||||
line_spacing = self._line_spacing
|
|
||||||
|
|
||||||
for char in text:
|
|
||||||
if char == "\n": # newline
|
|
||||||
newlines += 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
my_glyph = font.get_glyph(ord(char))
|
|
||||||
|
|
||||||
if my_glyph is None: # Error checking: no glyph found
|
|
||||||
print("Glyph not found: {}".format(repr(char)))
|
|
||||||
else:
|
|
||||||
if newlines:
|
|
||||||
xposition = x_start # reset to left column
|
|
||||||
yposition += (
|
|
||||||
self._line_spacing_ypixels(font, line_spacing) * newlines
|
|
||||||
) # Add the newline(s)
|
|
||||||
lines += newlines
|
|
||||||
newlines = 0
|
|
||||||
if xposition == x_start:
|
|
||||||
if left is None:
|
|
||||||
left = 0
|
|
||||||
else:
|
|
||||||
left = min(left, my_glyph.dx)
|
|
||||||
xright = xposition + my_glyph.width + my_glyph.dx
|
|
||||||
xposition += my_glyph.shift_x
|
|
||||||
|
|
||||||
right = max(right, xposition, xright)
|
|
||||||
|
|
||||||
if yposition == y_start: # first line, find the Ascender height
|
|
||||||
top = min(top, -my_glyph.height - my_glyph.dy + y_offset_tight)
|
|
||||||
bottom = max(bottom, yposition - my_glyph.dy + y_offset_tight)
|
|
||||||
|
|
||||||
if left is None:
|
|
||||||
left = 0
|
|
||||||
|
|
||||||
final_box_width = right - left
|
|
||||||
|
|
||||||
final_box_height_tight = bottom - top
|
|
||||||
final_y_offset_tight = -top + y_offset_tight
|
|
||||||
|
|
||||||
final_box_height_loose = (lines - 1) * self._line_spacing_ypixels(
|
|
||||||
font, line_spacing
|
|
||||||
) + (ascender_max + descender_max)
|
|
||||||
final_y_offset_loose = ascender_max
|
|
||||||
|
|
||||||
# return (final_box_width, final_box_height, left, final_y_offset)
|
|
||||||
|
|
||||||
return (
|
|
||||||
final_box_width,
|
|
||||||
final_box_height_tight,
|
|
||||||
left,
|
|
||||||
final_y_offset_tight,
|
|
||||||
final_box_height_loose,
|
|
||||||
final_y_offset_loose,
|
|
||||||
)
|
|
||||||
|
|
||||||
# pylint: disable = too-many-branches
|
|
||||||
def _place_text(
|
|
||||||
self,
|
|
||||||
bitmap: displayio.Bitmap,
|
|
||||||
text: str,
|
|
||||||
font: FontProtocol,
|
|
||||||
xposition: int,
|
|
||||||
yposition: int,
|
|
||||||
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
|
||||||
# when copying glyph bitmaps (this is important for slanted text
|
|
||||||
# where rectangular glyph boxes overlap)
|
|
||||||
) -> Tuple[int, int, int, int]:
|
|
||||||
# pylint: disable=too-many-arguments, too-many-locals
|
|
||||||
|
|
||||||
# placeText - Writes text into a bitmap at the specified location.
|
|
||||||
#
|
|
||||||
# Note: scale is pushed up to Group level
|
|
||||||
|
|
||||||
x_start = xposition # starting x position (left margin)
|
|
||||||
y_start = yposition
|
|
||||||
|
|
||||||
left = None
|
|
||||||
right = x_start
|
|
||||||
top = bottom = y_start
|
|
||||||
line_spacing = self._line_spacing
|
|
||||||
|
|
||||||
for char in text:
|
|
||||||
if char == "\n": # newline
|
|
||||||
xposition = x_start # reset to left column
|
|
||||||
yposition = yposition + self._line_spacing_ypixels(
|
|
||||||
font, line_spacing
|
|
||||||
) # Add a newline
|
|
||||||
|
|
||||||
else:
|
|
||||||
my_glyph = font.get_glyph(ord(char))
|
|
||||||
|
|
||||||
if my_glyph is None: # Error checking: no glyph found
|
|
||||||
print("Glyph not found: {}".format(repr(char)))
|
|
||||||
else:
|
|
||||||
if xposition == x_start:
|
|
||||||
if left is None:
|
|
||||||
left = 0
|
|
||||||
else:
|
|
||||||
left = min(left, my_glyph.dx)
|
|
||||||
|
|
||||||
right = max(
|
|
||||||
right,
|
|
||||||
xposition + my_glyph.shift_x,
|
|
||||||
xposition + my_glyph.width + my_glyph.dx,
|
|
||||||
)
|
|
||||||
if yposition == y_start: # first line, find the Ascender height
|
|
||||||
top = min(top, -my_glyph.height - my_glyph.dy)
|
|
||||||
bottom = max(bottom, yposition - my_glyph.dy)
|
|
||||||
|
|
||||||
glyph_offset_x = (
|
|
||||||
my_glyph.tile_index * my_glyph.width
|
|
||||||
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
|
|
||||||
# for BDF loaded fonts, this should equal 0
|
|
||||||
|
|
||||||
y_blit_target = yposition - my_glyph.height - my_glyph.dy
|
|
||||||
|
|
||||||
# Clip glyph y-direction if outside the font ascent/descent metrics.
|
|
||||||
# Note: bitmap.blit will automatically clip the bottom of the glyph.
|
|
||||||
y_clip = 0
|
|
||||||
if y_blit_target < 0:
|
|
||||||
y_clip = -y_blit_target # clip this amount from top of bitmap
|
|
||||||
y_blit_target = 0 # draw the clipped bitmap at y=0
|
|
||||||
if self._verbose:
|
|
||||||
print(
|
|
||||||
'Warning: Glyph clipped, exceeds Ascent property: "{}"'.format(
|
|
||||||
char
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (y_blit_target + my_glyph.height) > bitmap.height:
|
|
||||||
if self._verbose:
|
|
||||||
print(
|
|
||||||
'Warning: Glyph clipped, exceeds descent property: "{}"'.format(
|
|
||||||
char
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._blit(
|
|
||||||
bitmap,
|
|
||||||
max(xposition + my_glyph.dx, 0),
|
|
||||||
y_blit_target,
|
|
||||||
my_glyph.bitmap,
|
|
||||||
x_1=glyph_offset_x,
|
|
||||||
y_1=y_clip,
|
|
||||||
x_2=glyph_offset_x + my_glyph.width,
|
|
||||||
y_2=my_glyph.height,
|
|
||||||
skip_index=skip_index, # do not copy over any 0 background pixels
|
|
||||||
)
|
|
||||||
|
|
||||||
xposition = xposition + my_glyph.shift_x
|
|
||||||
|
|
||||||
# bounding_box
|
|
||||||
return left, top, right - left, bottom - top
|
|
||||||
|
|
||||||
def _blit(
|
|
||||||
self,
|
|
||||||
bitmap: displayio.Bitmap, # target bitmap
|
|
||||||
x: int, # target x upper left corner
|
|
||||||
y: int, # target y upper left corner
|
|
||||||
source_bitmap: displayio.Bitmap, # source bitmap
|
|
||||||
x_1: int = 0, # source x start
|
|
||||||
y_1: int = 0, # source y start
|
|
||||||
x_2: int = None, # source x end
|
|
||||||
y_2: int = None, # source y end
|
|
||||||
skip_index: int = None, # palette index that will not be copied
|
|
||||||
# (for example: the background color of a glyph)
|
|
||||||
) -> None:
|
|
||||||
# pylint: disable=no-self-use, too-many-arguments
|
|
||||||
|
|
||||||
if hasattr(bitmap, "blit"): # if bitmap has a built-in blit function, call it
|
|
||||||
# this function should perform its own input checks
|
|
||||||
bitmap.blit(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
source_bitmap,
|
|
||||||
x1=x_1,
|
|
||||||
y1=y_1,
|
|
||||||
x2=x_2,
|
|
||||||
y2=y_2,
|
|
||||||
skip_index=skip_index,
|
|
||||||
)
|
|
||||||
elif hasattr(bitmaptools, "blit"):
|
|
||||||
bitmaptools.blit(
|
|
||||||
bitmap,
|
|
||||||
source_bitmap,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
x1=x_1,
|
|
||||||
y1=y_1,
|
|
||||||
x2=x_2,
|
|
||||||
y2=y_2,
|
|
||||||
skip_source_index=skip_index,
|
|
||||||
)
|
|
||||||
|
|
||||||
else: # perform pixel by pixel copy of the bitmap
|
|
||||||
# Perform input checks
|
|
||||||
|
|
||||||
if x_2 is None:
|
|
||||||
x_2 = source_bitmap.width
|
|
||||||
if y_2 is None:
|
|
||||||
y_2 = source_bitmap.height
|
|
||||||
|
|
||||||
# Rearrange so that x_1 < x_2 and y1 < y2
|
|
||||||
if x_1 > x_2:
|
|
||||||
x_1, x_2 = x_2, x_1
|
|
||||||
if y_1 > y_2:
|
|
||||||
y_1, y_2 = y_2, y_1
|
|
||||||
|
|
||||||
# Ensure that x2 and y2 are within source bitmap size
|
|
||||||
x_2 = min(x_2, source_bitmap.width)
|
|
||||||
y_2 = min(y_2, source_bitmap.height)
|
|
||||||
|
|
||||||
for y_count in range(y_2 - y_1):
|
|
||||||
for x_count in range(x_2 - x_1):
|
|
||||||
x_placement = x + x_count
|
|
||||||
y_placement = y + y_count
|
|
||||||
|
|
||||||
if (bitmap.width > x_placement >= 0) and (
|
|
||||||
bitmap.height > y_placement >= 0
|
|
||||||
): # ensure placement is within target bitmap
|
|
||||||
# get the palette index from the source bitmap
|
|
||||||
this_pixel_color = source_bitmap[
|
|
||||||
y_1
|
|
||||||
+ (
|
|
||||||
y_count * source_bitmap.width
|
|
||||||
) # Direct index into a bitmap array is speedier than [x,y] tuple
|
|
||||||
+ x_1
|
|
||||||
+ x_count
|
|
||||||
]
|
|
||||||
|
|
||||||
if (skip_index is None) or (this_pixel_color != skip_index):
|
|
||||||
bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple
|
|
||||||
y_placement * bitmap.width + x_placement
|
|
||||||
] = this_pixel_color
|
|
||||||
elif y_placement > bitmap.height:
|
|
||||||
break
|
|
||||||
|
|
||||||
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
|
||||||
if self._save_text:
|
|
||||||
self._reset_text(line_spacing=new_line_spacing, scale=self.scale)
|
|
||||||
else:
|
|
||||||
raise RuntimeError("line_spacing is immutable when save_text is False")
|
|
||||||
|
|
||||||
def _set_font(self, new_font: FontProtocol) -> None:
|
|
||||||
self._font = new_font
|
|
||||||
if self._save_text:
|
|
||||||
self._reset_text(font=new_font, scale=self.scale)
|
|
||||||
else:
|
|
||||||
raise RuntimeError("font is immutable when save_text is False")
|
|
||||||
|
|
||||||
def _set_text(self, new_text: str, scale: int) -> None:
|
|
||||||
self._reset_text(text=self._replace_tabs(new_text), scale=self.scale)
|
|
||||||
|
|
||||||
def _set_background_color(self, new_color: Optional[int]):
|
|
||||||
self._background_color = new_color
|
|
||||||
if new_color is not None:
|
|
||||||
self._palette[0] = new_color
|
|
||||||
self._palette.make_opaque(0)
|
|
||||||
else:
|
|
||||||
self._palette[0] = 0
|
|
||||||
self._palette.make_transparent(0)
|
|
||||||
|
|
||||||
def _set_label_direction(self, new_label_direction: str) -> None:
|
|
||||||
# Only make changes if new direction is different
|
|
||||||
# to prevent errors in the _reset_text() direction checks
|
|
||||||
if self._label_direction != new_label_direction:
|
|
||||||
self._prev_label_direction = self._label_direction
|
|
||||||
self._label_direction = new_label_direction
|
|
||||||
self._reset_text(text=str(self._text)) # Force a recalculation
|
|
||||||
|
|
||||||
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
|
||||||
return "LTR", "RTL", "UPD", "UPR", "DWR"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bitmap(self) -> displayio.Bitmap:
|
|
||||||
"""
|
|
||||||
The Bitmap object that the text and background are drawn into.
|
|
||||||
|
|
||||||
:rtype: displayio.Bitmap
|
|
||||||
"""
|
|
||||||
return self._bitmap
|
|
||||||
|
|
@ -1,447 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
`adafruit_display_text.label`
|
|
||||||
====================================================
|
|
||||||
|
|
||||||
Displays text labels using CircuitPython's displayio.
|
|
||||||
|
|
||||||
* Author(s): Scott Shawcroft
|
|
||||||
|
|
||||||
Implementation Notes
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
**Hardware:**
|
|
||||||
|
|
||||||
**Software and Dependencies:**
|
|
||||||
|
|
||||||
* Adafruit CircuitPython firmware for the supported boards:
|
|
||||||
https://circuitpython.org/downloads
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = "3.2.2"
|
|
||||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
|
||||||
|
|
||||||
|
|
||||||
from displayio import Bitmap, Palette, TileGrid
|
|
||||||
from adafruit_display_text import LabelBase
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
from fontio import FontProtocol
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Label(LabelBase):
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
|
|
||||||
"""A label displaying a string of text. The origin point set by ``x`` and ``y``
|
|
||||||
properties will be the left edge of the bounding box, and in the center of a M
|
|
||||||
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
|
|
||||||
it will try to have it be center-left as close as possible.
|
|
||||||
|
|
||||||
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
|
||||||
Must include a capital M for measuring character size.
|
|
||||||
:type font: ~fontio.FontProtocol
|
|
||||||
:param str text: Text to display
|
|
||||||
:param int|Tuple(int, int, int) color: Color of all text in HEX or RGB
|
|
||||||
:param int|Tuple(int, int, int)|None background_color: Color of the background, use `None`
|
|
||||||
for transparent
|
|
||||||
:param float line_spacing: Line spacing of text to display
|
|
||||||
:param bool background_tight: Set `True` only if you want background box to tightly
|
|
||||||
surround text. When set to 'True' Padding parameters will be ignored.
|
|
||||||
:param int padding_top: Additional pixels added to background bounding box at top.
|
|
||||||
This parameter could be negative indicating additional pixels subtracted from the
|
|
||||||
background bounding box.
|
|
||||||
:param int padding_bottom: Additional pixels added to background bounding box at bottom.
|
|
||||||
This parameter could be negative indicating additional pixels subtracted from the
|
|
||||||
background bounding box.
|
|
||||||
:param int padding_left: Additional pixels added to background bounding box at left.
|
|
||||||
This parameter could be negative indicating additional pixels subtracted from the
|
|
||||||
background bounding box.
|
|
||||||
:param int padding_right: Additional pixels added to background bounding box at right.
|
|
||||||
This parameter could be negative indicating additional pixels subtracted from the
|
|
||||||
background bounding box.
|
|
||||||
:param Tuple(float, float) anchor_point: Point that anchored_position moves relative to.
|
|
||||||
Tuple with decimal percentage of width and height.
|
|
||||||
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
|
||||||
:param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple
|
|
||||||
containing x,y pixel coordinates.
|
|
||||||
:param int scale: Integer value of the pixel scaling
|
|
||||||
:param bool base_alignment: when True allows to align text label to the baseline.
|
|
||||||
This is helpful when two or more labels need to be aligned to the same baseline
|
|
||||||
:param Tuple(int, str) tab_replacement: tuple with tab character replace information. When
|
|
||||||
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
|
||||||
tab character
|
|
||||||
:param str label_direction: string defining the label text orientation. There are 5
|
|
||||||
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
|
|
||||||
``TTB``-Top-To-Bottom ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``"""
|
|
||||||
|
|
||||||
def __init__(self, font: FontProtocol, **kwargs) -> None:
|
|
||||||
self._background_palette = Palette(1)
|
|
||||||
self._added_background_tilegrid = False
|
|
||||||
|
|
||||||
super().__init__(font, **kwargs)
|
|
||||||
|
|
||||||
text = self._replace_tabs(self._text)
|
|
||||||
|
|
||||||
self._width = len(text)
|
|
||||||
self._height = self._font.get_bounding_box()[1]
|
|
||||||
|
|
||||||
# Create the two-color text palette
|
|
||||||
self._palette[0] = 0
|
|
||||||
self._palette.make_transparent(0)
|
|
||||||
|
|
||||||
if text is not None:
|
|
||||||
self._reset_text(str(text))
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
def _create_background_box(self, lines: int, y_offset: int) -> TileGrid:
|
|
||||||
"""Private Class function to create a background_box
|
|
||||||
:param lines: int number of lines
|
|
||||||
:param y_offset: int y pixel bottom coordinate for the background_box"""
|
|
||||||
|
|
||||||
left = self._bounding_box[0]
|
|
||||||
if self._background_tight: # draw a tight bounding box
|
|
||||||
box_width = self._bounding_box[2]
|
|
||||||
box_height = self._bounding_box[3]
|
|
||||||
x_box_offset = 0
|
|
||||||
y_box_offset = self._bounding_box[1]
|
|
||||||
|
|
||||||
else: # draw a "loose" bounding box to include any ascenders/descenders.
|
|
||||||
ascent, descent = self._ascent, self._descent
|
|
||||||
|
|
||||||
if self._label_direction in ("DWR", "UPR"):
|
|
||||||
box_height = (
|
|
||||||
self._bounding_box[3] + self._padding_right + self._padding_left
|
|
||||||
)
|
|
||||||
x_box_offset = -self._padding_left
|
|
||||||
box_width = (
|
|
||||||
(ascent + descent)
|
|
||||||
+ int((lines - 1) * self._width * self._line_spacing)
|
|
||||||
+ self._padding_top
|
|
||||||
+ self._padding_bottom
|
|
||||||
)
|
|
||||||
elif self._label_direction == "TTB":
|
|
||||||
box_height = (
|
|
||||||
self._bounding_box[3] + self._padding_top + self._padding_bottom
|
|
||||||
)
|
|
||||||
x_box_offset = -self._padding_left
|
|
||||||
box_width = (
|
|
||||||
(ascent + descent)
|
|
||||||
+ int((lines - 1) * self._height * self._line_spacing)
|
|
||||||
+ self._padding_right
|
|
||||||
+ self._padding_left
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
box_width = (
|
|
||||||
self._bounding_box[2] + self._padding_left + self._padding_right
|
|
||||||
)
|
|
||||||
x_box_offset = -self._padding_left
|
|
||||||
box_height = (
|
|
||||||
(ascent + descent)
|
|
||||||
+ int((lines - 1) * self._height * self._line_spacing)
|
|
||||||
+ self._padding_top
|
|
||||||
+ self._padding_bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._label_direction == "DWR":
|
|
||||||
padding_to_use = self._padding_bottom
|
|
||||||
elif self._label_direction == "TTB":
|
|
||||||
padding_to_use = self._padding_top
|
|
||||||
y_offset = 0
|
|
||||||
ascent = 0
|
|
||||||
else:
|
|
||||||
padding_to_use = self._padding_top
|
|
||||||
|
|
||||||
if self._base_alignment:
|
|
||||||
y_box_offset = -ascent - padding_to_use
|
|
||||||
else:
|
|
||||||
y_box_offset = -ascent + y_offset - padding_to_use
|
|
||||||
|
|
||||||
box_width = max(0, box_width) # remove any negative values
|
|
||||||
box_height = max(0, box_height) # remove any negative values
|
|
||||||
|
|
||||||
if self._label_direction == "UPR":
|
|
||||||
movx = y_box_offset
|
|
||||||
movy = -box_height - x_box_offset
|
|
||||||
elif self._label_direction == "DWR":
|
|
||||||
movx = y_box_offset
|
|
||||||
movy = x_box_offset
|
|
||||||
elif self._label_direction == "TTB":
|
|
||||||
movx = x_box_offset
|
|
||||||
movy = y_box_offset
|
|
||||||
else:
|
|
||||||
movx = left + x_box_offset
|
|
||||||
movy = y_box_offset
|
|
||||||
|
|
||||||
background_bitmap = Bitmap(box_width, box_height, 1)
|
|
||||||
tile_grid = TileGrid(
|
|
||||||
background_bitmap,
|
|
||||||
pixel_shader=self._background_palette,
|
|
||||||
x=movx,
|
|
||||||
y=movy,
|
|
||||||
)
|
|
||||||
|
|
||||||
return tile_grid
|
|
||||||
|
|
||||||
# pylint: enable=too-many-branches
|
|
||||||
def _set_background_color(self, new_color: Optional[int]) -> None:
|
|
||||||
"""Private class function that allows updating the font box background color
|
|
||||||
|
|
||||||
:param int new_color: Color as an RGB hex number, setting to None makes it transparent
|
|
||||||
"""
|
|
||||||
|
|
||||||
if new_color is None:
|
|
||||||
self._background_palette.make_transparent(0)
|
|
||||||
if self._added_background_tilegrid:
|
|
||||||
self._local_group.pop(0)
|
|
||||||
self._added_background_tilegrid = False
|
|
||||||
else:
|
|
||||||
self._background_palette.make_opaque(0)
|
|
||||||
self._background_palette[0] = new_color
|
|
||||||
self._background_color = new_color
|
|
||||||
|
|
||||||
lines = self._text.rstrip("\n").count("\n") + 1
|
|
||||||
y_offset = self._ascent // 2
|
|
||||||
|
|
||||||
if self._bounding_box is None:
|
|
||||||
# Still in initialization
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._added_background_tilegrid: # no bitmap is in the self Group
|
|
||||||
# add bitmap if text is present and bitmap sizes > 0 pixels
|
|
||||||
if (
|
|
||||||
(len(self._text) > 0)
|
|
||||||
and (
|
|
||||||
self._bounding_box[2] + self._padding_left + self._padding_right > 0
|
|
||||||
)
|
|
||||||
and (
|
|
||||||
self._bounding_box[3] + self._padding_top + self._padding_bottom > 0
|
|
||||||
)
|
|
||||||
):
|
|
||||||
self._local_group.insert(
|
|
||||||
0, self._create_background_box(lines, y_offset)
|
|
||||||
)
|
|
||||||
self._added_background_tilegrid = True
|
|
||||||
|
|
||||||
else: # a bitmap is present in the self Group
|
|
||||||
# update bitmap if text is present and bitmap sizes > 0 pixels
|
|
||||||
if (
|
|
||||||
(len(self._text) > 0)
|
|
||||||
and (
|
|
||||||
self._bounding_box[2] + self._padding_left + self._padding_right > 0
|
|
||||||
)
|
|
||||||
and (
|
|
||||||
self._bounding_box[3] + self._padding_top + self._padding_bottom > 0
|
|
||||||
)
|
|
||||||
):
|
|
||||||
self._local_group[0] = self._create_background_box(
|
|
||||||
lines, self._y_offset
|
|
||||||
)
|
|
||||||
else: # delete the existing bitmap
|
|
||||||
self._local_group.pop(0)
|
|
||||||
self._added_background_tilegrid = False
|
|
||||||
|
|
||||||
def _update_text(self, new_text: str) -> None:
|
|
||||||
# pylint: disable=too-many-branches,too-many-statements
|
|
||||||
|
|
||||||
x = 0
|
|
||||||
y = 0
|
|
||||||
if self._added_background_tilegrid:
|
|
||||||
i = 1
|
|
||||||
else:
|
|
||||||
i = 0
|
|
||||||
tilegrid_count = i
|
|
||||||
if self._base_alignment:
|
|
||||||
self._y_offset = 0
|
|
||||||
else:
|
|
||||||
self._y_offset = self._ascent // 2
|
|
||||||
|
|
||||||
if self._label_direction == "RTL":
|
|
||||||
left = top = bottom = 0
|
|
||||||
right = None
|
|
||||||
elif self._label_direction == "LTR":
|
|
||||||
right = top = bottom = 0
|
|
||||||
left = None
|
|
||||||
else:
|
|
||||||
top = right = left = 0
|
|
||||||
bottom = 0
|
|
||||||
|
|
||||||
for character in new_text:
|
|
||||||
if character == "\n":
|
|
||||||
y += int(self._height * self._line_spacing)
|
|
||||||
x = 0
|
|
||||||
continue
|
|
||||||
glyph = self._font.get_glyph(ord(character))
|
|
||||||
if not glyph:
|
|
||||||
continue
|
|
||||||
|
|
||||||
position_x, position_y = 0, 0
|
|
||||||
|
|
||||||
if self._label_direction in ("LTR", "RTL"):
|
|
||||||
bottom = max(bottom, y - glyph.dy + self._y_offset)
|
|
||||||
if y == 0: # first line, find the Ascender height
|
|
||||||
top = min(top, -glyph.height - glyph.dy + self._y_offset)
|
|
||||||
position_y = y - glyph.height - glyph.dy + self._y_offset
|
|
||||||
|
|
||||||
if self._label_direction == "LTR":
|
|
||||||
right = max(right, x + glyph.shift_x, x + glyph.width + glyph.dx)
|
|
||||||
if x == 0:
|
|
||||||
if left is None:
|
|
||||||
left = 0
|
|
||||||
else:
|
|
||||||
left = min(left, glyph.dx)
|
|
||||||
position_x = x + glyph.dx
|
|
||||||
else:
|
|
||||||
left = max(
|
|
||||||
left, abs(x) + glyph.shift_x, abs(x) + glyph.width + glyph.dx
|
|
||||||
)
|
|
||||||
if x == 0:
|
|
||||||
if right is None:
|
|
||||||
right = 0
|
|
||||||
else:
|
|
||||||
right = max(right, glyph.dx)
|
|
||||||
position_x = x - glyph.width
|
|
||||||
|
|
||||||
elif self._label_direction == "TTB":
|
|
||||||
if x == 0:
|
|
||||||
if left is None:
|
|
||||||
left = 0
|
|
||||||
else:
|
|
||||||
left = min(left, glyph.dx)
|
|
||||||
if y == 0:
|
|
||||||
top = min(top, -glyph.dy)
|
|
||||||
|
|
||||||
bottom = max(bottom, y + glyph.height, y + glyph.height + glyph.dy)
|
|
||||||
right = max(
|
|
||||||
right, x + glyph.width + glyph.dx, x + glyph.shift_x + glyph.dx
|
|
||||||
)
|
|
||||||
position_y = y + glyph.dy
|
|
||||||
position_x = x - glyph.width // 2 + self._y_offset
|
|
||||||
|
|
||||||
elif self._label_direction == "UPR":
|
|
||||||
if x == 0:
|
|
||||||
if bottom is None:
|
|
||||||
bottom = -glyph.dx
|
|
||||||
|
|
||||||
if y == 0: # first line, find the Ascender height
|
|
||||||
bottom = min(bottom, -glyph.dy)
|
|
||||||
left = min(left, x - glyph.height + self._y_offset)
|
|
||||||
top = min(top, y - glyph.width - glyph.dx, y - glyph.shift_x)
|
|
||||||
right = max(right, x + glyph.height, x + glyph.height - glyph.dy)
|
|
||||||
position_y = y - glyph.width - glyph.dx
|
|
||||||
position_x = x - glyph.height - glyph.dy + self._y_offset
|
|
||||||
|
|
||||||
elif self._label_direction == "DWR":
|
|
||||||
if y == 0:
|
|
||||||
if top is None:
|
|
||||||
top = -glyph.dx
|
|
||||||
top = min(top, -glyph.dx)
|
|
||||||
if x == 0:
|
|
||||||
left = min(left, -glyph.dy)
|
|
||||||
left = min(left, x, x - glyph.dy - self._y_offset)
|
|
||||||
bottom = max(bottom, y + glyph.width + glyph.dx, y + glyph.shift_x)
|
|
||||||
right = max(right, x + glyph.height)
|
|
||||||
position_y = y + glyph.dx
|
|
||||||
position_x = x + glyph.dy - self._y_offset
|
|
||||||
|
|
||||||
if glyph.width > 0 and glyph.height > 0:
|
|
||||||
face = TileGrid(
|
|
||||||
glyph.bitmap,
|
|
||||||
pixel_shader=self._palette,
|
|
||||||
default_tile=glyph.tile_index,
|
|
||||||
tile_width=glyph.width,
|
|
||||||
tile_height=glyph.height,
|
|
||||||
x=position_x,
|
|
||||||
y=position_y,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._label_direction == "UPR":
|
|
||||||
face.transpose_xy = True
|
|
||||||
face.flip_x = True
|
|
||||||
if self._label_direction == "DWR":
|
|
||||||
face.transpose_xy = True
|
|
||||||
face.flip_y = True
|
|
||||||
|
|
||||||
if tilegrid_count < len(self._local_group):
|
|
||||||
self._local_group[tilegrid_count] = face
|
|
||||||
else:
|
|
||||||
self._local_group.append(face)
|
|
||||||
tilegrid_count += 1
|
|
||||||
|
|
||||||
if self._label_direction == "RTL":
|
|
||||||
x = x - glyph.shift_x
|
|
||||||
if self._label_direction == "TTB":
|
|
||||||
if glyph.height < 2:
|
|
||||||
y = y + glyph.shift_x
|
|
||||||
else:
|
|
||||||
y = y + glyph.height + 1
|
|
||||||
if self._label_direction == "UPR":
|
|
||||||
y = y - glyph.shift_x
|
|
||||||
if self._label_direction == "DWR":
|
|
||||||
y = y + glyph.shift_x
|
|
||||||
if self._label_direction == "LTR":
|
|
||||||
x = x + glyph.shift_x
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if self._label_direction == "LTR" and left is None:
|
|
||||||
left = 0
|
|
||||||
if self._label_direction == "RTL" and right is None:
|
|
||||||
right = 0
|
|
||||||
if self._label_direction == "TTB" and top is None:
|
|
||||||
top = 0
|
|
||||||
|
|
||||||
while len(self._local_group) > tilegrid_count: # i:
|
|
||||||
self._local_group.pop()
|
|
||||||
|
|
||||||
if self._label_direction == "RTL":
|
|
||||||
# pylint: disable=invalid-unary-operand-type
|
|
||||||
# type-checkers think left can be None
|
|
||||||
self._bounding_box = (-left, top, left - right, bottom - top)
|
|
||||||
if self._label_direction == "TTB":
|
|
||||||
self._bounding_box = (left, top, right - left, bottom - top)
|
|
||||||
if self._label_direction == "UPR":
|
|
||||||
self._bounding_box = (left, top, right, bottom - top)
|
|
||||||
if self._label_direction == "DWR":
|
|
||||||
self._bounding_box = (left, top, right, bottom - top)
|
|
||||||
if self._label_direction == "LTR":
|
|
||||||
self._bounding_box = (left, top, right - left, bottom - top)
|
|
||||||
|
|
||||||
self._text = new_text
|
|
||||||
|
|
||||||
if self._background_color is not None:
|
|
||||||
self._set_background_color(self._background_color)
|
|
||||||
|
|
||||||
def _reset_text(self, new_text: str) -> None:
|
|
||||||
current_anchored_position = self.anchored_position
|
|
||||||
self._update_text(str(self._replace_tabs(new_text)))
|
|
||||||
self.anchored_position = current_anchored_position
|
|
||||||
|
|
||||||
def _set_font(self, new_font: FontProtocol) -> None:
|
|
||||||
old_text = self._text
|
|
||||||
current_anchored_position = self.anchored_position
|
|
||||||
self._text = ""
|
|
||||||
self._font = new_font
|
|
||||||
self._height = self._font.get_bounding_box()[1]
|
|
||||||
self._update_text(str(old_text))
|
|
||||||
self.anchored_position = current_anchored_position
|
|
||||||
|
|
||||||
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
|
||||||
self._line_spacing = new_line_spacing
|
|
||||||
self.text = self._text # redraw the box
|
|
||||||
|
|
||||||
def _set_text(self, new_text: str, scale: int) -> None:
|
|
||||||
self._reset_text(new_text)
|
|
||||||
|
|
||||||
def _set_label_direction(self, new_label_direction: str) -> None:
|
|
||||||
self._label_direction = new_label_direction
|
|
||||||
self._update_text(str(self._text))
|
|
||||||
|
|
||||||
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
|
||||||
return "LTR", "RTL", "UPR", "DWR", "TTB"
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Tim C
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
`adafruit_display_text.outlined_label`
|
|
||||||
====================================================
|
|
||||||
|
|
||||||
Subclass of BitmapLabel that adds outline color and stroke size
|
|
||||||
functionalities.
|
|
||||||
|
|
||||||
* Author(s): Tim Cocks
|
|
||||||
|
|
||||||
Implementation Notes
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
**Hardware:**
|
|
||||||
|
|
||||||
**Software and Dependencies:**
|
|
||||||
|
|
||||||
* Adafruit CircuitPython firmware for the supported boards:
|
|
||||||
https://circuitpython.org/downloads
|
|
||||||
|
|
||||||
"""
|
|
||||||
__version__ = "3.2.2"
|
|
||||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
|
||||||
|
|
||||||
import bitmaptools
|
|
||||||
from displayio import Palette, Bitmap
|
|
||||||
from adafruit_display_text import bitmap_label
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import Optional, Tuple, Union
|
|
||||||
from fontio import FontProtocol
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OutlinedLabel(bitmap_label.Label):
|
|
||||||
"""
|
|
||||||
OutlinedLabel - A BitmapLabel subclass that includes arguments and properties for specifying
|
|
||||||
outline_size and outline_color to get drawn as a stroke around the text.
|
|
||||||
|
|
||||||
:param Union[Tuple, int] outline_color: The color of the outline stroke as RGB tuple, or hex.
|
|
||||||
:param int outline_size: The size in pixels of the outline stroke.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
font,
|
|
||||||
outline_color: Union[int, Tuple] = 0x999999,
|
|
||||||
outline_size: int = 1,
|
|
||||||
padding_top: Optional[int] = None,
|
|
||||||
padding_bottom: Optional[int] = None,
|
|
||||||
padding_left: Optional[int] = None,
|
|
||||||
padding_right: Optional[int] = None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
if padding_top is None:
|
|
||||||
padding_top = outline_size + 0
|
|
||||||
if padding_bottom is None:
|
|
||||||
padding_bottom = outline_size + 2
|
|
||||||
if padding_left is None:
|
|
||||||
padding_left = outline_size + 0
|
|
||||||
if padding_right is None:
|
|
||||||
padding_right = outline_size + 0
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
font,
|
|
||||||
padding_top=padding_top,
|
|
||||||
padding_bottom=padding_bottom,
|
|
||||||
padding_left=padding_left,
|
|
||||||
padding_right=padding_right,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
_background_color = self._palette[0]
|
|
||||||
_foreground_color = self._palette[1]
|
|
||||||
_background_is_transparent = self._palette.is_transparent(0)
|
|
||||||
self._palette = Palette(3)
|
|
||||||
self._palette[0] = _background_color
|
|
||||||
self._palette[1] = _foreground_color
|
|
||||||
self._palette[2] = outline_color
|
|
||||||
if _background_is_transparent:
|
|
||||||
self._palette.make_transparent(0)
|
|
||||||
|
|
||||||
self._outline_size = outline_size
|
|
||||||
self._stamp_source = Bitmap((outline_size * 2) + 1, (outline_size * 2) + 1, 3)
|
|
||||||
self._stamp_source.fill(2)
|
|
||||||
|
|
||||||
self._bitmap = None
|
|
||||||
|
|
||||||
self._reset_text(
|
|
||||||
font=font,
|
|
||||||
text=self._text,
|
|
||||||
line_spacing=self._line_spacing,
|
|
||||||
scale=self.scale,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _add_outline(self):
|
|
||||||
"""
|
|
||||||
Blit the outline into the labels Bitmap. We will stamp self._stamp_source for each
|
|
||||||
pixel of the foreground color but skip the foreground color when we blit.
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if hasattr(self, "_stamp_source"):
|
|
||||||
for y in range(self.bitmap.height):
|
|
||||||
for x in range(self.bitmap.width):
|
|
||||||
if self.bitmap[x, y] == 1:
|
|
||||||
try:
|
|
||||||
bitmaptools.blit(
|
|
||||||
self.bitmap,
|
|
||||||
self._stamp_source,
|
|
||||||
x - self._outline_size,
|
|
||||||
y - self._outline_size,
|
|
||||||
skip_dest_index=1,
|
|
||||||
)
|
|
||||||
except ValueError as value_error:
|
|
||||||
raise ValueError(
|
|
||||||
"Padding must be big enough to fit outline_size "
|
|
||||||
"all the way around the text. "
|
|
||||||
"Try using either larger padding sizes, or smaller outline_size."
|
|
||||||
) from value_error
|
|
||||||
|
|
||||||
def _place_text(
|
|
||||||
self,
|
|
||||||
bitmap: Bitmap,
|
|
||||||
text: str,
|
|
||||||
font: FontProtocol,
|
|
||||||
xposition: int,
|
|
||||||
yposition: int,
|
|
||||||
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
|
||||||
# when copying glyph bitmaps (this is important for slanted text
|
|
||||||
# where rectangular glyph boxes overlap)
|
|
||||||
) -> Tuple[int, int, int, int]:
|
|
||||||
"""
|
|
||||||
Copy the glpyphs that represent the value of the string into the labels Bitmap.
|
|
||||||
:param bitmap: The bitmap to place text into
|
|
||||||
:param text: The text to render
|
|
||||||
:param font: The font to render the text in
|
|
||||||
:param xposition: x location of the starting point within the bitmap
|
|
||||||
:param yposition: y location of the starting point within the bitmap
|
|
||||||
:param skip_index: Color index to skip during rendering instead of covering up
|
|
||||||
:return Tuple bounding_box: tuple with x, y, width, height values of the bitmap
|
|
||||||
"""
|
|
||||||
parent_result = super()._place_text(
|
|
||||||
bitmap, text, font, xposition, yposition, skip_index=skip_index
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_outline()
|
|
||||||
|
|
||||||
return parent_result
|
|
||||||
|
|
||||||
@property
|
|
||||||
def outline_color(self):
|
|
||||||
"""Color of the outline to draw around the text."""
|
|
||||||
return self._palette[2]
|
|
||||||
|
|
||||||
@outline_color.setter
|
|
||||||
def outline_color(self, new_outline_color):
|
|
||||||
self._palette[2] = new_outline_color
|
|
||||||
|
|
||||||
@property
|
|
||||||
def outline_size(self):
|
|
||||||
"""Stroke size of the outline to draw around the text."""
|
|
||||||
return self._outline_size
|
|
||||||
|
|
||||||
@outline_size.setter
|
|
||||||
def outline_size(self, new_outline_size):
|
|
||||||
self._outline_size = new_outline_size
|
|
||||||
|
|
||||||
self._padding_top = new_outline_size + 0
|
|
||||||
self._padding_bottom = new_outline_size + 2
|
|
||||||
self._padding_left = new_outline_size + 0
|
|
||||||
self._padding_right = new_outline_size + 0
|
|
||||||
|
|
||||||
self._stamp_source = Bitmap(
|
|
||||||
(new_outline_size * 2) + 1, (new_outline_size * 2) + 1, 3
|
|
||||||
)
|
|
||||||
self._stamp_source.fill(2)
|
|
||||||
self._reset_text(
|
|
||||||
font=self._font,
|
|
||||||
text=self._text,
|
|
||||||
line_spacing=self._line_spacing,
|
|
||||||
scale=self.scale,
|
|
||||||
)
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
`adafruit_display_text.scrolling_label`
|
|
||||||
====================================================
|
|
||||||
|
|
||||||
Displays text into a fixed-width label that scrolls leftward
|
|
||||||
if the full_text is large enough to need it.
|
|
||||||
|
|
||||||
* Author(s): Tim Cocks
|
|
||||||
|
|
||||||
Implementation Notes
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
**Hardware:**
|
|
||||||
|
|
||||||
**Software and Dependencies:**
|
|
||||||
|
|
||||||
* Adafruit CircuitPython firmware for the supported boards:
|
|
||||||
https://circuitpython.org/downloads
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = "3.2.2"
|
|
||||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
|
||||||
|
|
||||||
import adafruit_ticks
|
|
||||||
from adafruit_display_text import bitmap_label
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import Optional
|
|
||||||
from fontio import FontProtocol
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ScrollingLabel(bitmap_label.Label):
|
|
||||||
"""ScrollingLabel - A fixed-width label that will scroll to the left
|
|
||||||
in order to show the full text if it's larger than the fixed-width.
|
|
||||||
|
|
||||||
:param font: The font to use for the label.
|
|
||||||
:type: ~fontio.FontProtocol
|
|
||||||
:param int max_characters: The number of characters that sets the fixed-width. Default is 10.
|
|
||||||
:param str text: The full text to show in the label. If this is longer than
|
|
||||||
``max_characters`` then the label will scroll to show everything.
|
|
||||||
:param float animate_time: The number of seconds in between scrolling animation
|
|
||||||
frames. Default is 0.3 seconds.
|
|
||||||
:param int current_index: The index of the first visible character in the label.
|
|
||||||
Default is 0, the first character. Will increase while scrolling."""
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
font: FontProtocol,
|
|
||||||
max_characters: int = 10,
|
|
||||||
text: Optional[str] = "",
|
|
||||||
animate_time: Optional[float] = 0.3,
|
|
||||||
current_index: Optional[int] = 0,
|
|
||||||
**kwargs
|
|
||||||
) -> None:
|
|
||||||
super().__init__(font, **kwargs)
|
|
||||||
self.animate_time = animate_time
|
|
||||||
self._current_index = current_index
|
|
||||||
self._last_animate_time = -1
|
|
||||||
self.max_characters = max_characters
|
|
||||||
|
|
||||||
if text and text[-1] != " ":
|
|
||||||
text = "{} ".format(text)
|
|
||||||
self._full_text = text
|
|
||||||
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def update(self, force: bool = False) -> None:
|
|
||||||
"""Attempt to update the display. If ``animate_time`` has elapsed since
|
|
||||||
previews animation frame then move the characters over by 1 index.
|
|
||||||
Must be called in the main loop of user code.
|
|
||||||
|
|
||||||
:param bool force: whether to ignore ``animation_time`` and force the update.
|
|
||||||
Default is False.
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_now = adafruit_ticks.ticks_ms()
|
|
||||||
if force or adafruit_ticks.ticks_less(
|
|
||||||
self._last_animate_time + int(self.animate_time * 1000), _now
|
|
||||||
):
|
|
||||||
if len(self.full_text) <= self.max_characters:
|
|
||||||
if self._text != self.full_text:
|
|
||||||
super()._set_text(self.full_text, self.scale)
|
|
||||||
self._last_animate_time = _now
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.current_index + self.max_characters <= len(self.full_text):
|
|
||||||
_showing_string = self.full_text[
|
|
||||||
self.current_index : self.current_index + self.max_characters
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
_showing_string_start = self.full_text[self.current_index :]
|
|
||||||
_showing_string_end = "{}".format(
|
|
||||||
self.full_text[
|
|
||||||
: (self.current_index + self.max_characters)
|
|
||||||
% len(self.full_text)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
_showing_string = "{}{}".format(
|
|
||||||
_showing_string_start, _showing_string_end
|
|
||||||
)
|
|
||||||
super()._set_text(_showing_string, self.scale)
|
|
||||||
self.current_index += 1
|
|
||||||
self._last_animate_time = _now
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_index(self) -> int:
|
|
||||||
"""Index of the first visible character.
|
|
||||||
|
|
||||||
:return int: The current index
|
|
||||||
"""
|
|
||||||
return self._current_index
|
|
||||||
|
|
||||||
@current_index.setter
|
|
||||||
def current_index(self, new_index: int) -> None:
|
|
||||||
if self.full_text:
|
|
||||||
self._current_index = new_index % len(self.full_text)
|
|
||||||
else:
|
|
||||||
self._current_index = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_text(self) -> str:
|
|
||||||
"""The full text to be shown. If it's longer than ``max_characters`` then
|
|
||||||
scrolling will occur as needed.
|
|
||||||
|
|
||||||
:return str: The full text of this label.
|
|
||||||
"""
|
|
||||||
return self._full_text
|
|
||||||
|
|
||||||
@full_text.setter
|
|
||||||
def full_text(self, new_text: str) -> None:
|
|
||||||
if new_text and new_text[-1] != " ":
|
|
||||||
new_text = "{} ".format(new_text)
|
|
||||||
if new_text != self._full_text:
|
|
||||||
self._full_text = new_text
|
|
||||||
self.current_index = 0
|
|
||||||
self.update(True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text(self):
|
|
||||||
"""The full text to be shown. If it's longer than ``max_characters`` then
|
|
||||||
scrolling will occur as needed.
|
|
||||||
|
|
||||||
:return str: The full text of this label.
|
|
||||||
"""
|
|
||||||
return self.full_text
|
|
||||||
|
|
||||||
@text.setter
|
|
||||||
def text(self, new_text):
|
|
||||||
self.full_text = new_text
|
|
||||||
|
|
@ -1,435 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
`adafruit_display_text.text_box`
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Text graphics handling for CircuitPython, including text boxes
|
|
||||||
|
|
||||||
|
|
||||||
* Author(s): Tim Cocks
|
|
||||||
|
|
||||||
Implementation Notes
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
**Hardware:**
|
|
||||||
|
|
||||||
**Software and Dependencies:**
|
|
||||||
|
|
||||||
* Adafruit CircuitPython firmware for the supported boards:
|
|
||||||
https://circuitpython.org/downloads
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = "3.2.2"
|
|
||||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
|
||||||
|
|
||||||
import displayio
|
|
||||||
from micropython import const
|
|
||||||
|
|
||||||
from adafruit_display_text import wrap_text_to_pixels
|
|
||||||
from adafruit_display_text import bitmap_label
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
from fontio import FontProtocol
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, duplicate-code
|
|
||||||
class TextBox(bitmap_label.Label):
|
|
||||||
"""
|
|
||||||
TextBox has a constrained width and optionally height.
|
|
||||||
You set the desired size when it's initialized it
|
|
||||||
will automatically wrap text to fit it within the allotted
|
|
||||||
size.
|
|
||||||
|
|
||||||
Left, Right, and Center alignment of the text within the
|
|
||||||
box are supported.
|
|
||||||
|
|
||||||
:param font: The font to use for the TextBox.
|
|
||||||
:param width: The width of the TextBox in pixels.
|
|
||||||
:param height: The height of the TextBox in pixels.
|
|
||||||
:param align: How to align the text within the box,
|
|
||||||
valid values are ``ALIGN_LEFT``, ``ALIGN_CENTER``, ``ALIGN_RIGHT``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
ALIGN_LEFT = const(0)
|
|
||||||
ALIGN_CENTER = const(1)
|
|
||||||
ALIGN_RIGHT = const(2)
|
|
||||||
|
|
||||||
DYNAMIC_HEIGHT = const(-1)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, font: FontProtocol, width: int, height: int, align=ALIGN_LEFT, **kwargs
|
|
||||||
) -> None:
|
|
||||||
self._bitmap = None
|
|
||||||
self._tilegrid = None
|
|
||||||
self._prev_label_direction = None
|
|
||||||
self._width = width
|
|
||||||
|
|
||||||
if height != TextBox.DYNAMIC_HEIGHT:
|
|
||||||
self._height = height
|
|
||||||
self.dynamic_height = False
|
|
||||||
else:
|
|
||||||
self.dynamic_height = True
|
|
||||||
|
|
||||||
if align not in (TextBox.ALIGN_LEFT, TextBox.ALIGN_CENTER, TextBox.ALIGN_RIGHT):
|
|
||||||
raise ValueError(
|
|
||||||
"Align must be one of: ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT"
|
|
||||||
)
|
|
||||||
self._align = align
|
|
||||||
|
|
||||||
self._padding_left = kwargs.get("padding_left", 0)
|
|
||||||
self._padding_right = kwargs.get("padding_right", 0)
|
|
||||||
|
|
||||||
self.lines = wrap_text_to_pixels(
|
|
||||||
kwargs.get("text", ""),
|
|
||||||
self._width - self._padding_left - self._padding_right,
|
|
||||||
font,
|
|
||||||
)
|
|
||||||
|
|
||||||
super(bitmap_label.Label, self).__init__(font, **kwargs)
|
|
||||||
|
|
||||||
print(f"before reset: {self._text}")
|
|
||||||
|
|
||||||
self._text = "\n".join(self.lines)
|
|
||||||
self._text = self._replace_tabs(self._text)
|
|
||||||
self._original_text = self._text
|
|
||||||
|
|
||||||
# call the text updater with all the arguments.
|
|
||||||
self._reset_text(
|
|
||||||
font=font,
|
|
||||||
text=self._text,
|
|
||||||
line_spacing=self._line_spacing,
|
|
||||||
scale=self.scale,
|
|
||||||
)
|
|
||||||
print(f"after reset: {self._text}")
|
|
||||||
|
|
||||||
def _place_text(
|
|
||||||
self,
|
|
||||||
bitmap: displayio.Bitmap,
|
|
||||||
text: str,
|
|
||||||
font: FontProtocol,
|
|
||||||
xposition: int,
|
|
||||||
yposition: int,
|
|
||||||
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
|
||||||
# when copying glyph bitmaps (this is important for slanted text
|
|
||||||
# where rectangular glyph boxes overlap)
|
|
||||||
) -> Tuple[int, int, int, int]:
|
|
||||||
# pylint: disable=too-many-arguments, too-many-locals, too-many-statements, too-many-branches
|
|
||||||
|
|
||||||
# placeText - Writes text into a bitmap at the specified location.
|
|
||||||
#
|
|
||||||
# Note: scale is pushed up to Group level
|
|
||||||
original_xposition = xposition
|
|
||||||
cur_line_index = 0
|
|
||||||
cur_line_width = self._text_bounding_box(self.lines[0], self.font)[0]
|
|
||||||
|
|
||||||
if self.align == self.ALIGN_LEFT:
|
|
||||||
x_start = original_xposition # starting x position (left margin)
|
|
||||||
if self.align == self.ALIGN_CENTER:
|
|
||||||
unused_space = self._width - cur_line_width
|
|
||||||
x_start = original_xposition + unused_space // 2
|
|
||||||
if self.align == self.ALIGN_RIGHT:
|
|
||||||
unused_space = self._width - cur_line_width
|
|
||||||
x_start = original_xposition + unused_space - self._padding_right
|
|
||||||
|
|
||||||
xposition = x_start # pylint: disable=used-before-assignment
|
|
||||||
|
|
||||||
y_start = yposition
|
|
||||||
# print(f"start loc {x_start}, {y_start}")
|
|
||||||
|
|
||||||
left = None
|
|
||||||
right = x_start
|
|
||||||
top = bottom = y_start
|
|
||||||
line_spacing = self._line_spacing
|
|
||||||
|
|
||||||
# print(f"cur_line width: {cur_line_width}")
|
|
||||||
for char in text:
|
|
||||||
if char == "\n": # newline
|
|
||||||
cur_line_index += 1
|
|
||||||
cur_line_width = self._text_bounding_box(
|
|
||||||
self.lines[cur_line_index], self.font
|
|
||||||
)[0]
|
|
||||||
# print(f"cur_line width: {cur_line_width}")
|
|
||||||
if self.align == self.ALIGN_LEFT:
|
|
||||||
x_start = original_xposition # starting x position (left margin)
|
|
||||||
if self.align == self.ALIGN_CENTER:
|
|
||||||
unused_space = self._width - cur_line_width
|
|
||||||
x_start = original_xposition + unused_space // 2
|
|
||||||
if self.align == self.ALIGN_RIGHT:
|
|
||||||
unused_space = self._width - cur_line_width
|
|
||||||
x_start = original_xposition + unused_space - self._padding_right
|
|
||||||
xposition = x_start
|
|
||||||
|
|
||||||
yposition = yposition + self._line_spacing_ypixels(
|
|
||||||
font, line_spacing
|
|
||||||
) # Add a newline
|
|
||||||
|
|
||||||
else:
|
|
||||||
my_glyph = font.get_glyph(ord(char))
|
|
||||||
|
|
||||||
if my_glyph is None: # Error checking: no glyph found
|
|
||||||
print("Glyph not found: {}".format(repr(char)))
|
|
||||||
else:
|
|
||||||
if xposition == x_start:
|
|
||||||
if left is None:
|
|
||||||
left = 0
|
|
||||||
else:
|
|
||||||
left = min(left, my_glyph.dx)
|
|
||||||
|
|
||||||
right = max(
|
|
||||||
right,
|
|
||||||
xposition + my_glyph.shift_x,
|
|
||||||
xposition + my_glyph.width + my_glyph.dx,
|
|
||||||
)
|
|
||||||
if yposition == y_start: # first line, find the Ascender height
|
|
||||||
top = min(top, -my_glyph.height - my_glyph.dy)
|
|
||||||
bottom = max(bottom, yposition - my_glyph.dy)
|
|
||||||
|
|
||||||
glyph_offset_x = (
|
|
||||||
my_glyph.tile_index * my_glyph.width
|
|
||||||
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
|
|
||||||
# for BDF loaded fonts, this should equal 0
|
|
||||||
|
|
||||||
y_blit_target = yposition - my_glyph.height - my_glyph.dy
|
|
||||||
|
|
||||||
# Clip glyph y-direction if outside the font ascent/descent metrics.
|
|
||||||
# Note: bitmap.blit will automatically clip the bottom of the glyph.
|
|
||||||
y_clip = 0
|
|
||||||
if y_blit_target < 0:
|
|
||||||
y_clip = -y_blit_target # clip this amount from top of bitmap
|
|
||||||
y_blit_target = 0 # draw the clipped bitmap at y=0
|
|
||||||
if self._verbose:
|
|
||||||
print(
|
|
||||||
'Warning: Glyph clipped, exceeds Ascent property: "{}"'.format(
|
|
||||||
char
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (y_blit_target + my_glyph.height) > bitmap.height:
|
|
||||||
if self._verbose:
|
|
||||||
print(
|
|
||||||
'Warning: Glyph clipped, exceeds descent property: "{}"'.format(
|
|
||||||
char
|
|
||||||
)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
self._blit(
|
|
||||||
bitmap,
|
|
||||||
max(xposition + my_glyph.dx, 0),
|
|
||||||
y_blit_target,
|
|
||||||
my_glyph.bitmap,
|
|
||||||
x_1=glyph_offset_x,
|
|
||||||
y_1=y_clip,
|
|
||||||
x_2=glyph_offset_x + my_glyph.width,
|
|
||||||
y_2=my_glyph.height,
|
|
||||||
skip_index=skip_index, # do not copy over any 0 background pixels
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
# ignore index out of bounds error
|
|
||||||
break
|
|
||||||
|
|
||||||
xposition = xposition + my_glyph.shift_x
|
|
||||||
|
|
||||||
# bounding_box
|
|
||||||
return left, top, right - left, bottom - top
|
|
||||||
|
|
||||||
def _reset_text(
|
|
||||||
self,
|
|
||||||
font: Optional[FontProtocol] = None,
|
|
||||||
text: Optional[str] = None,
|
|
||||||
line_spacing: Optional[float] = None,
|
|
||||||
scale: Optional[int] = None,
|
|
||||||
) -> None:
|
|
||||||
# pylint: disable=too-many-branches, too-many-statements, too-many-locals
|
|
||||||
|
|
||||||
# Store all the instance variables
|
|
||||||
if font is not None:
|
|
||||||
self._font = font
|
|
||||||
if line_spacing is not None:
|
|
||||||
self._line_spacing = line_spacing
|
|
||||||
|
|
||||||
# if text is not provided as a parameter (text is None), use the previous value.
|
|
||||||
if text is None:
|
|
||||||
text = self._text
|
|
||||||
|
|
||||||
self._text = self._replace_tabs(text)
|
|
||||||
print(f"inside reset_text text: {text}")
|
|
||||||
|
|
||||||
# Check for empty string
|
|
||||||
if (text == "") or (
|
|
||||||
text is None
|
|
||||||
): # If empty string, just create a zero-sized bounding box and that's it.
|
|
||||||
self._bounding_box = (
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0, # zero width with text == ""
|
|
||||||
0, # zero height with text == ""
|
|
||||||
)
|
|
||||||
# Clear out any items in the self._local_group Group, in case this is an
|
|
||||||
# update to the bitmap_label
|
|
||||||
for _ in self._local_group:
|
|
||||||
self._local_group.pop(0)
|
|
||||||
|
|
||||||
# Free the bitmap and tilegrid since they are removed
|
|
||||||
self._bitmap = None
|
|
||||||
self._tilegrid = None
|
|
||||||
|
|
||||||
else: # The text string is not empty, so create the Bitmap and TileGrid and
|
|
||||||
# append to the self Group
|
|
||||||
|
|
||||||
# Calculate the text bounding box
|
|
||||||
|
|
||||||
# Calculate both "tight" and "loose" bounding box dimensions to match label for
|
|
||||||
# anchor_position calculations
|
|
||||||
(
|
|
||||||
box_x,
|
|
||||||
tight_box_y,
|
|
||||||
x_offset,
|
|
||||||
tight_y_offset,
|
|
||||||
loose_box_y,
|
|
||||||
loose_y_offset,
|
|
||||||
) = self._text_bounding_box(
|
|
||||||
text,
|
|
||||||
self._font,
|
|
||||||
) # calculate the box size for a tight and loose backgrounds
|
|
||||||
|
|
||||||
if self._background_tight:
|
|
||||||
box_y = tight_box_y
|
|
||||||
y_offset = tight_y_offset
|
|
||||||
self._padding_left = 0
|
|
||||||
self._padding_right = 0
|
|
||||||
self._padding_top = 0
|
|
||||||
self._padding_bottom = 0
|
|
||||||
|
|
||||||
else: # calculate the box size for a loose background
|
|
||||||
box_y = loose_box_y
|
|
||||||
y_offset = loose_y_offset
|
|
||||||
|
|
||||||
# Calculate the background size including padding
|
|
||||||
tight_box_x = box_x
|
|
||||||
box_x = box_x + self._padding_left + self._padding_right
|
|
||||||
box_y = box_y + self._padding_top + self._padding_bottom
|
|
||||||
|
|
||||||
if self.dynamic_height:
|
|
||||||
print(f"dynamic height, box_y: {box_y}")
|
|
||||||
self._height = box_y
|
|
||||||
|
|
||||||
# Create the Bitmap unless it can be reused
|
|
||||||
new_bitmap = None
|
|
||||||
if (
|
|
||||||
self._bitmap is None
|
|
||||||
or self._bitmap.width != self._width
|
|
||||||
or self._bitmap.height != self._height
|
|
||||||
):
|
|
||||||
new_bitmap = displayio.Bitmap(
|
|
||||||
self._width, self._height, len(self._palette)
|
|
||||||
)
|
|
||||||
self._bitmap = new_bitmap
|
|
||||||
else:
|
|
||||||
self._bitmap.fill(0)
|
|
||||||
|
|
||||||
# Place the text into the Bitmap
|
|
||||||
self._place_text(
|
|
||||||
self._bitmap,
|
|
||||||
text,
|
|
||||||
self._font,
|
|
||||||
self._padding_left - x_offset,
|
|
||||||
self._padding_top + y_offset,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._base_alignment:
|
|
||||||
label_position_yoffset = 0
|
|
||||||
else:
|
|
||||||
label_position_yoffset = self._ascent // 2
|
|
||||||
|
|
||||||
# Create the TileGrid if not created bitmap unchanged
|
|
||||||
if self._tilegrid is None or new_bitmap:
|
|
||||||
self._tilegrid = displayio.TileGrid(
|
|
||||||
self._bitmap,
|
|
||||||
pixel_shader=self._palette,
|
|
||||||
width=1,
|
|
||||||
height=1,
|
|
||||||
tile_width=self._width,
|
|
||||||
tile_height=self._height,
|
|
||||||
default_tile=0,
|
|
||||||
x=-self._padding_left + x_offset,
|
|
||||||
y=label_position_yoffset - y_offset - self._padding_top,
|
|
||||||
)
|
|
||||||
# Clear out any items in the local_group Group, in case this is an update to
|
|
||||||
# the bitmap_label
|
|
||||||
for _ in self._local_group:
|
|
||||||
self._local_group.pop(0)
|
|
||||||
self._local_group.append(
|
|
||||||
self._tilegrid
|
|
||||||
) # add the bitmap's tilegrid to the group
|
|
||||||
|
|
||||||
self._bounding_box = (
|
|
||||||
self._tilegrid.x + self._padding_left,
|
|
||||||
self._tilegrid.y + self._padding_top,
|
|
||||||
tight_box_x,
|
|
||||||
tight_box_y,
|
|
||||||
)
|
|
||||||
print(f"end of reset_text bounding box: {self._bounding_box}")
|
|
||||||
|
|
||||||
if (
|
|
||||||
scale is not None
|
|
||||||
): # Scale will be defined in local_group (Note: self should have scale=1)
|
|
||||||
self.scale = scale # call the setter
|
|
||||||
|
|
||||||
# set the anchored_position with setter after bitmap is created, sets the
|
|
||||||
# x,y positions of the label
|
|
||||||
self.anchored_position = self._anchored_position
|
|
||||||
|
|
||||||
@property
|
|
||||||
def height(self) -> int:
|
|
||||||
"""The height of the label determined from the bounding box."""
|
|
||||||
return self._height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def width(self) -> int:
|
|
||||||
"""The width of the label determined from the bounding box."""
|
|
||||||
return self._width
|
|
||||||
|
|
||||||
@width.setter
|
|
||||||
def width(self, width: int) -> None:
|
|
||||||
self._width = width
|
|
||||||
self.text = self._text
|
|
||||||
|
|
||||||
@height.setter
|
|
||||||
def height(self, height: int) -> None:
|
|
||||||
if height != TextBox.DYNAMIC_HEIGHT:
|
|
||||||
self._height = height
|
|
||||||
self.dynamic_height = False
|
|
||||||
else:
|
|
||||||
self.dynamic_height = True
|
|
||||||
self.text = self._text
|
|
||||||
|
|
||||||
@bitmap_label.Label.text.setter
|
|
||||||
def text(self, text: str) -> None:
|
|
||||||
self.lines = wrap_text_to_pixels(
|
|
||||||
text, self._width - self._padding_left - self._padding_right, self.font
|
|
||||||
)
|
|
||||||
self._text = self._replace_tabs(text)
|
|
||||||
self._original_text = self._text
|
|
||||||
self._text = "\n".join(self.lines)
|
|
||||||
|
|
||||||
self._set_text(self._text, self.scale)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def align(self):
|
|
||||||
"""Alignment of the text within the TextBox"""
|
|
||||||
return self._align
|
|
||||||
|
|
||||||
@align.setter
|
|
||||||
def align(self, align: int) -> None:
|
|
||||||
if align not in (TextBox.ALIGN_LEFT, TextBox.ALIGN_CENTER, TextBox.ALIGN_RIGHT):
|
|
||||||
raise ValueError(
|
|
||||||
"Align must be one of: ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT"
|
|
||||||
)
|
|
||||||
self._align = align
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import board
|
|
||||||
import displayio
|
|
||||||
import vectorio
|
|
||||||
import terminalio
|
|
||||||
from adafruit_display_text import label
|
|
||||||
|
|
||||||
group = displayio.Group()
|
|
||||||
board.DISPLAY.root_group = group
|
|
||||||
|
|
||||||
text = "Hello world"
|
|
||||||
text_area = label.Label(terminalio.FONT, text=text)
|
|
||||||
text_area.x = 10
|
|
||||||
text_area.y = 10
|
|
||||||
group.append(text_area)
|
|
||||||
|
|
||||||
palette = displayio.Palette(1)
|
|
||||||
palette[0] = 0xff00ff
|
|
||||||
|
|
||||||
circle = vectorio.Circle(pixel_shader=palette, radius=5, x=5, y=5)
|
|
||||||
group.append(circle)
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#meowbit
|
|
||||||
|
|
||||||
import keypad
|
|
||||||
import board
|
|
||||||
|
|
||||||
km = keypad.KeyMatrix(
|
|
||||||
row_pins = (board.P0, board.P1, board.P2, board.P3),
|
|
||||||
column_pins = (board.P4, board.P6, board.P8, board.P9) )
|
|
||||||
|
|
||||||
while True:
|
|
||||||
event = km.events.get()
|
|
||||||
if event:
|
|
||||||
print(event.key_number, event.released)
|
|
||||||
|
|
@ -1,327 +0,0 @@
|
||||||
# TO USE KEYPAD TO MANIPULATE DATA:
|
|
||||||
#
|
|
||||||
# A button: run/halt
|
|
||||||
# B button: when halted, toggles address/data entry
|
|
||||||
# Right button: when halted, single-steps
|
|
||||||
#
|
|
||||||
# Address entry: press the two digits for the address. It is entered immediately (there's no "enter" key)
|
|
||||||
# Data entry: likewise. After you press the second digit, it will automatically go to the next address.
|
|
||||||
|
|
||||||
|
|
||||||
import time
|
|
||||||
import board
|
|
||||||
import displayio
|
|
||||||
import vectorio
|
|
||||||
import terminalio # for font
|
|
||||||
from adafruit_display_text import label
|
|
||||||
import keypad
|
|
||||||
from digitalio import DigitalInOut, Direction, Pull
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TwoDigitHexInput:
|
|
||||||
def __init__(self):
|
|
||||||
self.digits = [0x0, 0x0]
|
|
||||||
self.currentDigit = 0
|
|
||||||
self.value = 0
|
|
||||||
|
|
||||||
def input(self, d):
|
|
||||||
self.digits[self.currentDigit] = d
|
|
||||||
self.value = (self.digits[0] * 16) + self.digits[1]
|
|
||||||
print("INPUT", self.digits)
|
|
||||||
self.currentDigit = 0 if self.currentDigit else 1
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.__init__()
|
|
||||||
print(self.digits)
|
|
||||||
|
|
||||||
|
|
||||||
class CPU:
|
|
||||||
def __init__(self):
|
|
||||||
self.running = False
|
|
||||||
self.IP = 254
|
|
||||||
self.acc = 0
|
|
||||||
self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False }
|
|
||||||
self.instruction = { 'opcode': False, 'operand': False }
|
|
||||||
self.memory = False
|
|
||||||
|
|
||||||
|
|
||||||
def load_memory(self, bytes):
|
|
||||||
self.memory = bytes + bytearray(256 - len(bytes))
|
|
||||||
print(type(self.memory))
|
|
||||||
print('mem 254', self.memory[254])
|
|
||||||
# print(self.memory)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.running = True
|
|
||||||
|
|
||||||
def step(self):
|
|
||||||
if self.IP >= 256:
|
|
||||||
self.IP = 0
|
|
||||||
print("IP:", self.IP)
|
|
||||||
self.instruction['opcode'] = self.memory[self.IP]
|
|
||||||
self.IP = self.IP+1
|
|
||||||
self.instruction['operand'] = self.memory[self.IP]
|
|
||||||
self.IP = self.IP+1
|
|
||||||
self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand'])
|
|
||||||
|
|
||||||
print("instr:", self.instruction['opcode'], self.instruction['operand'])
|
|
||||||
print("mnem:", self.nums2mnems[self.instruction['opcode']])
|
|
||||||
print("acc:", self.acc)
|
|
||||||
print("running:", self.running)
|
|
||||||
print()
|
|
||||||
# self.print_screen()
|
|
||||||
print("byte 26 (keyboard):", self.memory[26])
|
|
||||||
print()
|
|
||||||
|
|
||||||
def hlt(self, operand):
|
|
||||||
self.running = False
|
|
||||||
|
|
||||||
def nop(self, operand):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def lda_lit(self, operand):
|
|
||||||
self.acc = operand
|
|
||||||
|
|
||||||
def lda_mem(self, operand):
|
|
||||||
self.acc = memory[operand]
|
|
||||||
|
|
||||||
def sta_lit(self, operand):
|
|
||||||
memory[operand] = self.acc
|
|
||||||
|
|
||||||
def sta_mem(self, operand):
|
|
||||||
memory[memory[operand]] = self.acc
|
|
||||||
|
|
||||||
def add_lit(self, operand):
|
|
||||||
self.acc = self.acc + operand
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def add_mem(self, operand):
|
|
||||||
self.acc = self.acc + self.memory[operand]
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def sub_lit(self, operand):
|
|
||||||
self.acc = self.acc - operand
|
|
||||||
if self.acc < 0:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def sub_mem(self, operand):
|
|
||||||
self.acc = self.acc - self.memory[operand]
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def jmp_lit(self, operand):
|
|
||||||
self.IP = operand
|
|
||||||
|
|
||||||
def jmp_mem(self, operand):
|
|
||||||
self.IP = memory[operand]
|
|
||||||
|
|
||||||
def ske(self, operand):
|
|
||||||
if self.flags['Eq']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skz(self, operand):
|
|
||||||
if self.flags['Z']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skn(self, operand):
|
|
||||||
if self.flags['N']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skc(self, operand):
|
|
||||||
if self.flags['C']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def cst(self, operand):
|
|
||||||
self.flags['C'] = True
|
|
||||||
|
|
||||||
def ccl(self, operand):
|
|
||||||
self.flags['C'] = False
|
|
||||||
|
|
||||||
nums2mnems = {
|
|
||||||
0: hlt,
|
|
||||||
1: nop,
|
|
||||||
2: lda_lit,
|
|
||||||
3: sta_lit,
|
|
||||||
4: add_lit,
|
|
||||||
5: sub_lit,
|
|
||||||
6: jmp_lit,
|
|
||||||
7: ske,
|
|
||||||
8: skz,
|
|
||||||
9: skn,
|
|
||||||
10: skc,
|
|
||||||
11: cst,
|
|
||||||
12: ccl,
|
|
||||||
16: hlt,
|
|
||||||
17: nop,
|
|
||||||
18: lda_mem,
|
|
||||||
19: sta_mem,
|
|
||||||
20: add_mem,
|
|
||||||
21: sub_mem,
|
|
||||||
22: jmp_mem,
|
|
||||||
23: ske,
|
|
||||||
24: skz,
|
|
||||||
25: skn,
|
|
||||||
26: skc,
|
|
||||||
27: cst,
|
|
||||||
28: ccl,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### MEOWBIT-SPECIFIC STUFF ###
|
|
||||||
|
|
||||||
# to list board features: print(board.__dir__)
|
|
||||||
|
|
||||||
btna = DigitalInOut(board.BTNA)
|
|
||||||
btna.direction = Direction.INPUT
|
|
||||||
btna.pull = Pull.UP # down doesn't work
|
|
||||||
|
|
||||||
btnb = DigitalInOut(board.BTNB)
|
|
||||||
btnb.direction = Direction.INPUT
|
|
||||||
btnb.pull = Pull.UP # down doesn't work
|
|
||||||
|
|
||||||
btnr = DigitalInOut(board.RIGHT)
|
|
||||||
btnr.direction = Direction.INPUT
|
|
||||||
btnr.pull = Pull.UP # down doesn't work
|
|
||||||
|
|
||||||
km = keypad.KeyMatrix(
|
|
||||||
row_pins = (board.P0, board.P1, board.P2, board.P3),
|
|
||||||
column_pins = (board.P4, board.P6, board.P8, board.P9) )
|
|
||||||
|
|
||||||
# This is global because that way you can update the text by just altering text_area.text
|
|
||||||
displayGroup = displayio.Group()
|
|
||||||
board.DISPLAY.root_group = displayGroup
|
|
||||||
text_area = label.Label(terminalio.FONT, text="")
|
|
||||||
text_area.x = 10
|
|
||||||
text_area.y = 10
|
|
||||||
displayGroup.append(text_area)
|
|
||||||
|
|
||||||
palette = displayio.Palette(1)
|
|
||||||
palette[0] = 0xff00ff
|
|
||||||
|
|
||||||
|
|
||||||
class Monitor:
|
|
||||||
def __init__(self, cpu):
|
|
||||||
self.cpu = cpu
|
|
||||||
self.monitorMode = 'addressEntry' # or dataEntry
|
|
||||||
self.monitorAddressInput = TwoDigitHexInput()
|
|
||||||
self.monitorDataInput = TwoDigitHexInput()
|
|
||||||
|
|
||||||
def handleKeys(self):
|
|
||||||
keypad_event = km.events.get()
|
|
||||||
keyPressed = True if (keypad_event and keypad_event.released) else False
|
|
||||||
key = keypad_event.key_number if keyPressed else False
|
|
||||||
|
|
||||||
if self.cpu.running:
|
|
||||||
if btna.value == False:
|
|
||||||
print("HALT PRESSED")
|
|
||||||
self.cpu.running = False
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
# km.events.clear() # don't track keypresses from during the run
|
|
||||||
|
|
||||||
if keyPressed:
|
|
||||||
self.cpu.memory[26] = key
|
|
||||||
|
|
||||||
elif not self.cpu.running:
|
|
||||||
if btna.value == False:
|
|
||||||
self.cpu.running = True
|
|
||||||
print("\nSTARTING")
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if btnb.value == False:
|
|
||||||
self.monitorMode = 'addressEntry' if self.monitorMode != 'addressEntry' else 'dataEntry'
|
|
||||||
print("\nENTERING", self.monitorMode, "MODE")
|
|
||||||
self.monitorDataInput.currentDigit = 0
|
|
||||||
self.monitorAddressInput.currentDigit = 0
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if btnr.value == False:
|
|
||||||
print("\nSINGLE STEP FROM MONITOR ADDR")
|
|
||||||
# self.IP = self.monitorAddressInput.value
|
|
||||||
self.cpu.step()
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if keypad_event and keypad_event.released:
|
|
||||||
if self.monitorMode == 'addressEntry':
|
|
||||||
self.monitorAddressInput.input(keypad_event.key_number)
|
|
||||||
self.cpu.IP = self.monitorAddressInput.value
|
|
||||||
print("MA", self.IP)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.monitorDataInput.input(keypad_event.key_number)
|
|
||||||
self.cpu.memory[self.IP] = self.monitorDataInput.value
|
|
||||||
print("MD", self.monitorDataInput.value)
|
|
||||||
if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so go to the next addresss
|
|
||||||
self.cpu.IP = (self.cpu.IP + 1) % 256
|
|
||||||
print("ADVANCING")
|
|
||||||
print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n")
|
|
||||||
|
|
||||||
def printMonitor(self):
|
|
||||||
text = "IP " + str(self.cpu.IP) + "\tDATA " + str(self.cpu.memory[self.cpu.IP]) + "\tACC " + str(self.cpu.acc) + "\nRunning: " + str(self.cpu.running)
|
|
||||||
text_area.text = text
|
|
||||||
|
|
||||||
|
|
||||||
def printScreen(self):
|
|
||||||
for i in range(5):
|
|
||||||
for j in range(5):
|
|
||||||
memory_index = (i * 5) + j
|
|
||||||
if self.cpu.memory[memory_index] > 0:
|
|
||||||
print("#", end=" ")
|
|
||||||
circle = vectorio.Circle(pixel_shader=palette, radius=8, x=(10 + (j * 20)), y=(40 + (i * 20)))
|
|
||||||
displayGroup.append(circle)
|
|
||||||
else:
|
|
||||||
print("_", end=" ")
|
|
||||||
print()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.cpu.start()
|
|
||||||
t = time.time()
|
|
||||||
while (time.time() - t) < 30:
|
|
||||||
self.handleKeys()
|
|
||||||
if self.cpu.running:
|
|
||||||
self.cpu.step()
|
|
||||||
self.printMonitor()
|
|
||||||
# self.printScreen()
|
|
||||||
time.sleep(0.5)
|
|
||||||
print("timeout")
|
|
||||||
print(self.cpu.memory)
|
|
||||||
|
|
||||||
|
|
||||||
cpu = CPU()
|
|
||||||
monitor = Monitor(cpu)
|
|
||||||
|
|
||||||
prog = '04 FF 04 01 14 01 00 00 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01'
|
|
||||||
#prog = '00'
|
|
||||||
program_bytes = bytearray.fromhex(prog.replace(" ", ""))
|
|
||||||
# Add jmp at addr 254:
|
|
||||||
program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600')
|
|
||||||
cpu.load_memory(program_with_jump)
|
|
||||||
|
|
||||||
monitor.run()
|
|
||||||
|
|
@ -1,353 +0,0 @@
|
||||||
# TO USE KEYPAD TO MANIPULATE DATA:
|
|
||||||
#
|
|
||||||
# A button: run/halt
|
|
||||||
# B button: when halted, toggles address/data entry
|
|
||||||
# Right button: when halted, single-steps
|
|
||||||
#
|
|
||||||
# Address entry: press the two digits for the address. It is entered immediately (there's no "enter" key)
|
|
||||||
# Data entry: likewise. After you press the second digit, it will automatically go to the next address.
|
|
||||||
|
|
||||||
|
|
||||||
import time
|
|
||||||
import board
|
|
||||||
import keypad
|
|
||||||
import digitalio
|
|
||||||
from tm1637_display import TM1637Display
|
|
||||||
import board
|
|
||||||
import busio # for led matrix
|
|
||||||
from adafruit_ht16k33 import matrix
|
|
||||||
|
|
||||||
class TwoDigitHexInput:
|
|
||||||
def __init__(self):
|
|
||||||
self.digits = [0x0, 0x0]
|
|
||||||
self.currentDigit = 0
|
|
||||||
self.value = 0
|
|
||||||
|
|
||||||
def input(self, d):
|
|
||||||
self.digits[self.currentDigit] = d
|
|
||||||
self.value = (self.digits[0] * 16) + self.digits[1]
|
|
||||||
print("INPUT", self.digits, "current digit: " + str(self.currentDigit), "value: " + str(self.value))
|
|
||||||
self.currentDigit = 0 if self.currentDigit else 1
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.__init__()
|
|
||||||
print(self.digits)
|
|
||||||
|
|
||||||
def set(self, n):
|
|
||||||
self.value = n
|
|
||||||
self.digits[0] = n >> 4
|
|
||||||
self.digits[1] = n & 0xF
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CPU:
|
|
||||||
def __init__(self):
|
|
||||||
self.running = False
|
|
||||||
self.IP = 254
|
|
||||||
self.acc = 0
|
|
||||||
self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False }
|
|
||||||
self.instruction = { 'opcode': False, 'operand': False }
|
|
||||||
self.memory = False
|
|
||||||
|
|
||||||
|
|
||||||
def load_memory(self, bytes):
|
|
||||||
self.memory = bytes + bytearray(256 - len(bytes))
|
|
||||||
# print(self.memory)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.running = True
|
|
||||||
|
|
||||||
def step(self):
|
|
||||||
if self.IP >= 255: # TODO CHECK
|
|
||||||
self.IP = 0
|
|
||||||
print("IP:", toHex(self.IP))
|
|
||||||
self.instruction['opcode'] = self.memory[self.IP]
|
|
||||||
self.IP = self.IP+1
|
|
||||||
self.instruction['operand'] = self.memory[self.IP]
|
|
||||||
self.IP = self.IP+1
|
|
||||||
self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand'])
|
|
||||||
|
|
||||||
print("instr:", toHex(self.instruction['opcode']), toHex(self.instruction['operand']))
|
|
||||||
print("mnem:", self.nums2mnems[self.instruction['opcode']])
|
|
||||||
print("acc:", self.acc, "N:", self.flags['N'])
|
|
||||||
print("running:", self.running)
|
|
||||||
print()
|
|
||||||
# self.print_screen()
|
|
||||||
print("byte 26 (keyboard):", self.memory[26])
|
|
||||||
print()
|
|
||||||
|
|
||||||
def hlt(self, operand):
|
|
||||||
self.running = False
|
|
||||||
|
|
||||||
def nop(self, operand):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def lda_lit(self, operand):
|
|
||||||
self.acc = operand
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def lda_mem(self, operand):
|
|
||||||
self.acc = self.memory[operand]
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def sta_lit(self, operand):
|
|
||||||
self.memory[operand] = self.acc
|
|
||||||
|
|
||||||
def sta_mem(self, operand):
|
|
||||||
self.memory[self.memory[operand]] = self.acc
|
|
||||||
|
|
||||||
def add_lit(self, operand):
|
|
||||||
self.acc = self.acc + operand
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def add_mem(self, operand):
|
|
||||||
self.acc = self.acc + self.memory[operand]
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def sub_lit(self, operand):
|
|
||||||
self.acc = self.acc - operand
|
|
||||||
if self.acc < 0:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def sub_mem(self, operand):
|
|
||||||
self.acc = self.acc - self.memory[operand]
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def jmp_lit(self, operand):
|
|
||||||
self.IP = operand
|
|
||||||
|
|
||||||
def jmp_mem(self, operand):
|
|
||||||
self.IP = self.memory[operand]
|
|
||||||
|
|
||||||
def ske(self, operand): # FIXME
|
|
||||||
# if self.flags['Eq']:
|
|
||||||
# self.IP += 2
|
|
||||||
if self.acc == operand:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skz(self, operand):
|
|
||||||
if self.flags['Z']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skn(self, operand):
|
|
||||||
if self.flags['N']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skc(self, operand):
|
|
||||||
if self.flags['C']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def cst(self, operand):
|
|
||||||
self.flags['C'] = True
|
|
||||||
|
|
||||||
def ccl(self, operand):
|
|
||||||
self.flags['C'] = False
|
|
||||||
|
|
||||||
nums2mnems = {
|
|
||||||
0: hlt, # x0
|
|
||||||
1: nop, # x1
|
|
||||||
2: lda_lit, # 02
|
|
||||||
3: sta_lit, # 03
|
|
||||||
4: add_lit, # 04
|
|
||||||
5: sub_lit, # 05
|
|
||||||
6: jmp_lit, # 06
|
|
||||||
7: ske, # x7
|
|
||||||
8: skz, # x8
|
|
||||||
9: skn, # x9
|
|
||||||
10: skc, # A
|
|
||||||
11: cst, # B
|
|
||||||
12: ccl, # C
|
|
||||||
16: hlt, #
|
|
||||||
17: nop, #
|
|
||||||
18: lda_mem, # 12
|
|
||||||
19: sta_mem, # 13
|
|
||||||
20: add_mem, # 14
|
|
||||||
21: sub_mem, # 15
|
|
||||||
22: jmp_mem, # 16
|
|
||||||
23: ske,
|
|
||||||
24: skz,
|
|
||||||
25: skn,
|
|
||||||
26: skc,
|
|
||||||
27: cst,
|
|
||||||
28: ccl,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### PI PICO SPECIFIC STUFF ###
|
|
||||||
|
|
||||||
# to list board features: print(dir(board))
|
|
||||||
|
|
||||||
display_1 = TM1637Display(board.GP0, board.GP1, length=4)
|
|
||||||
display_2 = TM1637Display(board.GP2, board.GP3, length=4)
|
|
||||||
|
|
||||||
i2c = busio.I2C(board.GP17, board.GP16) # scl, sda
|
|
||||||
matrix = matrix.Matrix8x8(i2c)
|
|
||||||
matrix.brightness = 1
|
|
||||||
matrix.blink_rate = 0
|
|
||||||
|
|
||||||
keymatrix = keypad.KeyMatrix(
|
|
||||||
row_pins = (board.GP5, board.GP6, board.GP7, board.GP8),
|
|
||||||
column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) )
|
|
||||||
|
|
||||||
keymap = {
|
|
||||||
15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt",
|
|
||||||
10:"4", 11:"5", 12:"6", 13:"7", 14:"step",
|
|
||||||
5:"8", 6:"9", 7:"A", 8:"B", 9:"addr",
|
|
||||||
0:"C", 1:"D", 2:"E", 3:"F", 4:"data" }
|
|
||||||
|
|
||||||
numericKeys = [ "0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F" ]
|
|
||||||
|
|
||||||
def toHex(n):
|
|
||||||
return "%0.2X" % n
|
|
||||||
|
|
||||||
class Monitor:
|
|
||||||
def __init__(self, cpu):
|
|
||||||
self.cpu = cpu
|
|
||||||
self.monitorMode = 'addressEntry' # or dataEntry
|
|
||||||
self.monitorAddressInput = TwoDigitHexInput()
|
|
||||||
self.monitorDataInput = TwoDigitHexInput()
|
|
||||||
|
|
||||||
# In data entry mode, when a full byte is keyed in,
|
|
||||||
# the next keypress advances to the next address and continues entering data there.
|
|
||||||
# This variable tracks whether it's time to do that or not.
|
|
||||||
self.advanceDataEntryNextPress = False
|
|
||||||
|
|
||||||
def handleKeys(self):
|
|
||||||
keypad_event = keymatrix.events.get()
|
|
||||||
keyPressed = True if (keypad_event and keypad_event.released ) else False
|
|
||||||
key = keymap[keypad_event.key_number] if keyPressed else False
|
|
||||||
numericKeyPressed = True if (keyPressed and (key in numericKeys)) else False
|
|
||||||
|
|
||||||
|
|
||||||
if self.cpu.running:
|
|
||||||
if key == "runhalt":
|
|
||||||
print("HALT PRESSED")
|
|
||||||
self.cpu.running = False
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
# km.events.clear() # don't track keypresses from during the run
|
|
||||||
|
|
||||||
if numericKeyPressed:
|
|
||||||
self.cpu.memory[26] = int(key, 16)
|
|
||||||
|
|
||||||
elif not self.cpu.running:
|
|
||||||
if key == "runhalt":
|
|
||||||
self.cpu.running = True
|
|
||||||
print("\nSTARTING")
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if key == "addr":
|
|
||||||
self.monitorMode = 'addressEntry'
|
|
||||||
print("\nENTERING", self.monitorMode, "MODE")
|
|
||||||
self.monitorAddressInput.currentDigit = 0
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
if key == "data":
|
|
||||||
self.monitorMode = 'dataEntry'
|
|
||||||
print("\nENTERING", self.monitorMode, "MODE")
|
|
||||||
self.monitorDataInput.clear()
|
|
||||||
self.advanceDataEntryNextPress = False
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if key == "step":
|
|
||||||
print("\nSINGLE STEP FROM MONITOR ADDR")
|
|
||||||
# self.IP = self.monitorAddressInput.value
|
|
||||||
self.cpu.step()
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if numericKeyPressed:
|
|
||||||
if self.monitorMode == 'addressEntry':
|
|
||||||
self.monitorAddressInput.input(int(key, 16))
|
|
||||||
self.cpu.IP = self.monitorAddressInput.value
|
|
||||||
print("MA", self.cpu.IP)
|
|
||||||
|
|
||||||
if self.monitorMode == 'dataEntry':
|
|
||||||
if self.advanceDataEntryNextPress:
|
|
||||||
print("ADVANCING")
|
|
||||||
self.cpu.IP = (self.cpu.IP + 1) % 256
|
|
||||||
# self.monitorDataInput.clear() # reset .currentDigit
|
|
||||||
self.monitorDataInput.set(self.cpu.memory[self.cpu.IP])
|
|
||||||
self.advanceDataEntryNextPress = False
|
|
||||||
self.monitorDataInput.input(int(key, 16))
|
|
||||||
self.cpu.memory[self.cpu.IP] = self.monitorDataInput.value
|
|
||||||
print("MD", self.monitorDataInput.value)
|
|
||||||
if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so next keypress is for the next address
|
|
||||||
self.advanceDataEntryNextPress = True
|
|
||||||
|
|
||||||
print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def displayScreen(self):
|
|
||||||
for x in range(8):
|
|
||||||
for y in range(8):
|
|
||||||
matrix[x, y] = self.cpu.memory[x + (8*y)]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
#self.cpu.start()
|
|
||||||
t = time.time()
|
|
||||||
while (time.time() - t) < 120: # TODO: add a time delta or sth maybe so this doesn't just burn cycles
|
|
||||||
self.handleKeys()
|
|
||||||
display_1.print(toHex(self.cpu.IP) + toHex(self.cpu.memory[self.cpu.IP]))
|
|
||||||
# display_1.print(toHex(self.monitorAddressInput.value) + toHex(self.cpu.memory[self.cpu.IP]))
|
|
||||||
# display_2.print(toHex(self.cpu.IP) + toHex(self.cpu.acc))
|
|
||||||
display_2.print(toHex(self.cpu.acc))
|
|
||||||
self.displayScreen()
|
|
||||||
if self.cpu.running:
|
|
||||||
self.cpu.step()
|
|
||||||
# time.sleep(0.5) # TODO ?
|
|
||||||
print("timeout")
|
|
||||||
print(self.cpu.memory)
|
|
||||||
|
|
||||||
|
|
||||||
cpu = CPU()
|
|
||||||
monitor = Monitor(cpu)
|
|
||||||
|
|
||||||
# preamble = '00 ' * 64
|
|
||||||
# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 12 f0 05 41 08 00 06 40 00 00' # STRIPES
|
|
||||||
# offset = 64
|
|
||||||
# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 05 08 09 00 04 09 03 f0 07 41 06' + toHex(offset) + '00 00'
|
|
||||||
#prog = '00'
|
|
||||||
# program_bytes = bytearray.fromhex(prog.replace(" ", ""))
|
|
||||||
|
|
||||||
# Add jmp at addr 254:
|
|
||||||
#program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600') # jump to addr 00
|
|
||||||
# program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0640') # jump to addr 0x40 (dec 64)
|
|
||||||
|
|
||||||
with open('test-multiply2.bin', 'rb') as file:
|
|
||||||
program_bytes = bytearray(file.read())
|
|
||||||
|
|
||||||
cpu.load_memory(program_bytes)
|
|
||||||
|
|
||||||
monitor.run()
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import time
|
|
||||||
import board
|
|
||||||
import neopixel
|
|
||||||
|
|
||||||
pixels = neopixel.NeoPixel(board.A3, 5*5, brightness=0.5, auto_write=True)
|
|
||||||
|
|
||||||
for i in range(50):
|
|
||||||
pixels.fill((50, 0, 0)) # red
|
|
||||||
time.sleep(0.5)
|
|
||||||
pixels.fill((0,0,50)) # blue
|
|
||||||
time.sleep(0.5)
|
|
||||||
print(i)
|
|
||||||
Binary file not shown.
|
|
@ -1,181 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2016 Damien P. George
|
|
||||||
# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries
|
|
||||||
# SPDX-FileCopyrightText: 2019 Carter Nelson
|
|
||||||
# SPDX-FileCopyrightText: 2019 Roy Hooper
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
`neopixel` - NeoPixel strip driver
|
|
||||||
====================================================
|
|
||||||
|
|
||||||
* Author(s): Damien P. George, Scott Shawcroft, Carter Nelson, Rose Hooper
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import board
|
|
||||||
import digitalio
|
|
||||||
from neopixel_write import neopixel_write
|
|
||||||
|
|
||||||
import adafruit_pixelbuf
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Used only for typing
|
|
||||||
from typing import Optional, Type
|
|
||||||
from types import TracebackType
|
|
||||||
import microcontroller
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = "6.3.15"
|
|
||||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.git"
|
|
||||||
|
|
||||||
|
|
||||||
# Pixel color order constants
|
|
||||||
RGB = "RGB"
|
|
||||||
"""Red Green Blue"""
|
|
||||||
GRB = "GRB"
|
|
||||||
"""Green Red Blue"""
|
|
||||||
RGBW = "RGBW"
|
|
||||||
"""Red Green Blue White"""
|
|
||||||
GRBW = "GRBW"
|
|
||||||
"""Green Red Blue White"""
|
|
||||||
|
|
||||||
|
|
||||||
class NeoPixel(adafruit_pixelbuf.PixelBuf):
|
|
||||||
"""
|
|
||||||
A sequence of neopixels.
|
|
||||||
|
|
||||||
:param ~microcontroller.Pin pin: The pin to output neopixel data on.
|
|
||||||
:param int n: The number of neopixels in the chain
|
|
||||||
:param int bpp: Bytes per pixel. 3 for RGB and 4 for RGBW pixels.
|
|
||||||
:param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full
|
|
||||||
brightness
|
|
||||||
:param bool auto_write: True if the neopixels should immediately change when set. If False,
|
|
||||||
`show` must be called explicitly.
|
|
||||||
:param str pixel_order: Set the pixel color channel order. The default is GRB if bpp is set
|
|
||||||
to 3, otherwise GRBW is used as the default.
|
|
||||||
|
|
||||||
Example for Circuit Playground Express:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import neopixel
|
|
||||||
from board import *
|
|
||||||
|
|
||||||
RED = 0x100000 # (0x10, 0, 0) also works
|
|
||||||
|
|
||||||
pixels = neopixel.NeoPixel(NEOPIXEL, 10)
|
|
||||||
for i in range(len(pixels)):
|
|
||||||
pixels[i] = RED
|
|
||||||
|
|
||||||
Example for Circuit Playground Express setting every other pixel red using a slice:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import neopixel
|
|
||||||
from board import *
|
|
||||||
import time
|
|
||||||
|
|
||||||
RED = 0x100000 # (0x10, 0, 0) also works
|
|
||||||
|
|
||||||
# Using ``with`` ensures pixels are cleared after we're done.
|
|
||||||
with neopixel.NeoPixel(NEOPIXEL, 10) as pixels:
|
|
||||||
pixels[::2] = [RED] * (len(pixels) // 2)
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
.. py:method:: NeoPixel.show()
|
|
||||||
|
|
||||||
Shows the new colors on the pixels themselves if they haven't already
|
|
||||||
been autowritten.
|
|
||||||
|
|
||||||
The colors may or may not be showing after this function returns because
|
|
||||||
it may be done asynchronously.
|
|
||||||
|
|
||||||
.. py:method:: NeoPixel.fill(color)
|
|
||||||
|
|
||||||
Colors all pixels the given ***color***.
|
|
||||||
|
|
||||||
.. py:attribute:: brightness
|
|
||||||
|
|
||||||
Overall brightness of the pixel (0 to 1.0)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
pin: microcontroller.Pin,
|
|
||||||
n: int,
|
|
||||||
*,
|
|
||||||
bpp: int = 3,
|
|
||||||
brightness: float = 1.0,
|
|
||||||
auto_write: bool = True,
|
|
||||||
pixel_order: str = None
|
|
||||||
):
|
|
||||||
if not pixel_order:
|
|
||||||
pixel_order = GRB if bpp == 3 else GRBW
|
|
||||||
elif isinstance(pixel_order, tuple):
|
|
||||||
order_list = [RGBW[order] for order in pixel_order]
|
|
||||||
pixel_order = "".join(order_list)
|
|
||||||
|
|
||||||
self._power = None
|
|
||||||
if (
|
|
||||||
sys.implementation.version[0] >= 7
|
|
||||||
and getattr(board, "NEOPIXEL", None) == pin
|
|
||||||
):
|
|
||||||
power = getattr(board, "NEOPIXEL_POWER_INVERTED", None)
|
|
||||||
polarity = power is None
|
|
||||||
if not power:
|
|
||||||
power = getattr(board, "NEOPIXEL_POWER", None)
|
|
||||||
if power:
|
|
||||||
try:
|
|
||||||
self._power = digitalio.DigitalInOut(power)
|
|
||||||
self._power.switch_to_output(value=polarity)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
n, brightness=brightness, byteorder=pixel_order, auto_write=auto_write
|
|
||||||
)
|
|
||||||
|
|
||||||
self.pin = digitalio.DigitalInOut(pin)
|
|
||||||
self.pin.direction = digitalio.Direction.OUTPUT
|
|
||||||
|
|
||||||
def deinit(self) -> None:
|
|
||||||
"""Blank out the NeoPixels and release the pin."""
|
|
||||||
self.fill(0)
|
|
||||||
self.show()
|
|
||||||
self.pin.deinit()
|
|
||||||
if self._power:
|
|
||||||
self._power.deinit()
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(
|
|
||||||
self,
|
|
||||||
exception_type: Optional[Type[BaseException]],
|
|
||||||
exception_value: Optional[BaseException],
|
|
||||||
traceback: Optional[TracebackType],
|
|
||||||
):
|
|
||||||
self.deinit()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "[" + ", ".join([str(x) for x in self]) + "]"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def n(self) -> int:
|
|
||||||
"""
|
|
||||||
The number of neopixels in the chain (read-only)
|
|
||||||
"""
|
|
||||||
return len(self)
|
|
||||||
|
|
||||||
def write(self) -> None:
|
|
||||||
""".. deprecated: 1.0.0
|
|
||||||
|
|
||||||
Use ``show`` instead. It matches Micro:Bit and Arduino APIs."""
|
|
||||||
self.show()
|
|
||||||
|
|
||||||
def _transmit(self, buffer: bytearray) -> None:
|
|
||||||
neopixel_write(self.pin, buffer)
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
# Open the binary file in read-binary mode
|
|
||||||
with open('test-multiply.bin', 'rb') as file:
|
|
||||||
# Read the entire file contents
|
|
||||||
binary_data = file.read()
|
|
||||||
|
|
||||||
# Convert the binary data to a string of hex bytes
|
|
||||||
hex_string = binary_data.hex()
|
|
||||||
|
|
||||||
# Print the hex string
|
|
||||||
print(hex_string)
|
|
||||||
|
|
||||||
|
|
||||||
# Open the binary file in read-binary mode
|
|
||||||
with open('test-multiply.bin', 'rb') as file:
|
|
||||||
# Read the entire file contents into a bytearray
|
|
||||||
byte_data = bytearray(file.read())
|
|
||||||
|
|
||||||
# Print the bytearray
|
|
||||||
print(byte_data)
|
|
||||||
|
|
||||||
Binary file not shown.
|
|
@ -1,10 +0,0 @@
|
||||||
import board
|
|
||||||
import busio
|
|
||||||
from adafruit_ht16k33 import matrix
|
|
||||||
i2c = busio.I2C(board.GP17, board.GP16) # scl, sda
|
|
||||||
|
|
||||||
matrix = matrix.Matrix8x8(i2c)
|
|
||||||
matrix.fill(0) # Clear the matrix.
|
|
||||||
matrix[0, 0] = 1
|
|
||||||
matrix.brightness = 1
|
|
||||||
matrix.blink_rate = 2
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
"""Example for pi pico. Blinking LED"""
|
|
||||||
|
|
||||||
import board
|
|
||||||
import digitalio
|
|
||||||
import time
|
|
||||||
|
|
||||||
led = digitalio.DigitalInOut(board.LED)
|
|
||||||
led.direction = digitalio.Direction.OUTPUT
|
|
||||||
|
|
||||||
led.value = True
|
|
||||||
|
|
||||||
for i in range(50):
|
|
||||||
led.value = not led.value
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
"""Example for pi pico. Button-controlled LED.
|
|
||||||
|
|
||||||
Wiring: switch to ground pin on pico, and to pin 18.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import board
|
|
||||||
import digitalio
|
|
||||||
|
|
||||||
led = digitalio.DigitalInOut(board.LED)
|
|
||||||
led.direction = digitalio.Direction.OUTPUT
|
|
||||||
|
|
||||||
switch = digitalio.DigitalInOut(board.GP18)
|
|
||||||
switch.direction = digitalio.Direction.INPUT
|
|
||||||
switch.pull = digitalio.Pull.UP
|
|
||||||
|
|
||||||
while True:
|
|
||||||
led.value = not switch.value
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
# pi pico
|
|
||||||
|
|
||||||
import keypad
|
|
||||||
import board
|
|
||||||
|
|
||||||
keymatrix = keypad.KeyMatrix(
|
|
||||||
row_pins = (board.GP5, board.GP6, board.GP7, board.GP8),
|
|
||||||
column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) )
|
|
||||||
|
|
||||||
keymap = {
|
|
||||||
15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt",
|
|
||||||
10:"4", 11:"5", 12:"6", 13:"7", 14:"step",
|
|
||||||
5:"8", 6:"9", 7:"A", 8:"B", 9:"addrdata",
|
|
||||||
0:"C", 1:"D", 2:"E", 3:"F", 4:"NA" }
|
|
||||||
|
|
||||||
while True:
|
|
||||||
event = keymatrix.events.get()
|
|
||||||
if event:
|
|
||||||
print(event.key_number, event.released, keymap[event.key_number])
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import board
|
|
||||||
import keypad
|
|
||||||
|
|
||||||
keymatrix = keypad.KeyMatrix(
|
|
||||||
row_pins = (board.GP5, board.GP6, board.GP7, board.GP8),
|
|
||||||
column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) )
|
|
||||||
|
|
||||||
keymap = {
|
|
||||||
15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt",
|
|
||||||
10:"4", 11:"5", 12:"6", 13:"7", 14:"step",
|
|
||||||
5:"8", 6:"9", 7:"A", 8:"B", 9:"addr",
|
|
||||||
0:"C", 1:"D", 2:"E", 3:"F", 4:"data" }
|
|
||||||
|
|
||||||
while True:
|
|
||||||
keypad_event = keymatrix.events.get()
|
|
||||||
keyPressed = True if (keypad_event and keypad_event.released ) else False
|
|
||||||
key = keymap[keypad_event.key_number] if keyPressed else False
|
|
||||||
if key:
|
|
||||||
print(key)
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
"""Example for pi pico. Blinking LED"""
|
|
||||||
|
|
||||||
import board
|
|
||||||
import digitalio
|
|
||||||
import time
|
|
||||||
from tm1637_display import TM1637Display
|
|
||||||
|
|
||||||
display_1 = TM1637Display(board.GP0, board.GP1, length=4)
|
|
||||||
display_1.print("1234")
|
|
||||||
|
|
||||||
display_2 = TM1637Display(board.GP2, board.GP3, length=4)
|
|
||||||
display_2.print("ABCD")
|
|
||||||
|
|
||||||
print("end")
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2013-2024, Kim Davies and contributors.
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
3. Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
||||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
||||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
||||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
||||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
164
python/cpu.py
164
python/cpu.py
|
|
@ -1,164 +0,0 @@
|
||||||
import time
|
|
||||||
|
|
||||||
class CPU:
|
|
||||||
def __init__(self):
|
|
||||||
self.running = False
|
|
||||||
self.IP = 254
|
|
||||||
self.acc = 0
|
|
||||||
self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False }
|
|
||||||
self.instruction = { 'opcode': False, 'operand': False }
|
|
||||||
self.memory = False
|
|
||||||
|
|
||||||
|
|
||||||
def load_memory(self, bytes):
|
|
||||||
self.memory = bytes + bytearray(256 - len(bytes))
|
|
||||||
# print(self.memory)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.running = True
|
|
||||||
|
|
||||||
def step(self):
|
|
||||||
if self.IP >= 255: # TODO CHECK
|
|
||||||
self.IP = 0
|
|
||||||
print("IP:", toHex(self.IP))
|
|
||||||
self.instruction['opcode'] = self.memory[self.IP]
|
|
||||||
self.IP = self.IP+1
|
|
||||||
self.instruction['operand'] = self.memory[self.IP]
|
|
||||||
self.IP = self.IP+1
|
|
||||||
self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand'])
|
|
||||||
|
|
||||||
print("instr:", toHex(self.instruction['opcode']), toHex(self.instruction['operand']))
|
|
||||||
print("mnem:", self.nums2mnems[self.instruction['opcode']])
|
|
||||||
print("acc:", self.acc, "N:", self.flags['N'])
|
|
||||||
print("running:", self.running)
|
|
||||||
print()
|
|
||||||
# self.print_screen()
|
|
||||||
print("byte 26 (keyboard):", self.memory[26])
|
|
||||||
print()
|
|
||||||
|
|
||||||
def hlt(self, operand):
|
|
||||||
self.running = False
|
|
||||||
|
|
||||||
def nop(self, operand):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def lda_lit(self, operand):
|
|
||||||
self.acc = operand
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def lda_mem(self, operand):
|
|
||||||
self.acc = self.memory[operand]
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def sta_lit(self, operand):
|
|
||||||
self.memory[operand] = self.acc
|
|
||||||
|
|
||||||
def sta_mem(self, operand):
|
|
||||||
self.memory[self.memory[operand]] = self.acc
|
|
||||||
|
|
||||||
def add_lit(self, operand):
|
|
||||||
self.acc = self.acc + operand
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def add_mem(self, operand):
|
|
||||||
self.acc = self.acc + self.memory[operand]
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def sub_lit(self, operand):
|
|
||||||
self.acc = self.acc - operand
|
|
||||||
if self.acc < 0:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def sub_mem(self, operand):
|
|
||||||
self.acc = self.acc - self.memory[operand]
|
|
||||||
if self.acc > 255:
|
|
||||||
self.acc = self.acc % 256
|
|
||||||
self.flags['C'] = True
|
|
||||||
else:
|
|
||||||
self.flags['C'] = False
|
|
||||||
self.flags['Z'] = True if self.acc == 0 else False
|
|
||||||
self.flags['Eq'] = True if self.acc == operand else False
|
|
||||||
self.flags['N'] = True if self.acc > 127 else False
|
|
||||||
|
|
||||||
def jmp_lit(self, operand):
|
|
||||||
self.IP = operand
|
|
||||||
|
|
||||||
def jmp_mem(self, operand):
|
|
||||||
self.IP = self.memory[operand]
|
|
||||||
|
|
||||||
def ske(self, operand): # FIXME
|
|
||||||
# if self.flags['Eq']:
|
|
||||||
# self.IP += 2
|
|
||||||
if self.acc == operand:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skz(self, operand):
|
|
||||||
if self.flags['Z']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skn(self, operand):
|
|
||||||
if self.flags['N']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def skc(self, operand):
|
|
||||||
if self.flags['C']:
|
|
||||||
self.IP += 2
|
|
||||||
|
|
||||||
def cst(self, operand):
|
|
||||||
self.flags['C'] = True
|
|
||||||
|
|
||||||
def ccl(self, operand):
|
|
||||||
self.flags['C'] = False
|
|
||||||
|
|
||||||
nums2mnems = {
|
|
||||||
0: hlt, # x0
|
|
||||||
1: nop, # x1
|
|
||||||
2: lda_lit, # 02
|
|
||||||
3: sta_lit, # 03
|
|
||||||
4: add_lit, # 04
|
|
||||||
5: sub_lit, # 05
|
|
||||||
6: jmp_lit, # 06
|
|
||||||
7: ske, # x7
|
|
||||||
8: skz, # x8
|
|
||||||
9: skn, # x9
|
|
||||||
10: skc, # A
|
|
||||||
11: cst, # B
|
|
||||||
12: ccl, # C
|
|
||||||
16: hlt, #
|
|
||||||
17: nop, #
|
|
||||||
18: lda_mem, # 12
|
|
||||||
19: sta_mem, # 13
|
|
||||||
20: add_mem, # 14
|
|
||||||
21: sub_mem, # 15
|
|
||||||
22: jmp_mem, # 16
|
|
||||||
23: ske,
|
|
||||||
24: skz,
|
|
||||||
25: skn,
|
|
||||||
26: skc,
|
|
||||||
27: cst,
|
|
||||||
28: ccl,
|
|
||||||
}
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
numericKeys = [ "0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F" ]
|
|
||||||
|
|
||||||
def toHex(n):
|
|
||||||
return "%0.2X" % n
|
|
||||||
|
|
||||||
class Monitor:
|
|
||||||
def __init__(self, cpu):
|
|
||||||
self.cpu = cpu
|
|
||||||
self.monitorMode = 'addressEntry' # or dataEntry
|
|
||||||
self.monitorAddressInput = TwoDigitHexInput()
|
|
||||||
self.monitorDataInput = TwoDigitHexInput()
|
|
||||||
|
|
||||||
# In data entry mode, when a full byte is keyed in,
|
|
||||||
# the next keypress advances to the next address and continues entering data there.
|
|
||||||
# This variable tracks whether it's time to do that or not.
|
|
||||||
self.advanceDataEntryNextPress = False
|
|
||||||
|
|
||||||
def handleKeys(self):
|
|
||||||
keypad_event = keymatrix.events.get()
|
|
||||||
keyPressed = True if (keypad_event and keypad_event.released ) else False
|
|
||||||
key = keymap[keypad_event.key_number] if keyPressed else False
|
|
||||||
numericKeyPressed = True if (keyPressed and (key in numericKeys)) else False
|
|
||||||
|
|
||||||
|
|
||||||
if self.cpu.running:
|
|
||||||
if key == "runhalt":
|
|
||||||
print("HALT PRESSED")
|
|
||||||
self.cpu.running = False
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
# km.events.clear() # don't track keypresses from during the run
|
|
||||||
|
|
||||||
if numericKeyPressed:
|
|
||||||
self.cpu.memory[26] = int(key, 16)
|
|
||||||
|
|
||||||
elif not self.cpu.running:
|
|
||||||
if key == "runhalt":
|
|
||||||
self.cpu.running = True
|
|
||||||
print("\nSTARTING")
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if key == "addr":
|
|
||||||
self.monitorMode = 'addressEntry'
|
|
||||||
print("\nENTERING", self.monitorMode, "MODE")
|
|
||||||
self.monitorAddressInput.currentDigit = 0
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
if key == "data":
|
|
||||||
self.monitorMode = 'dataEntry'
|
|
||||||
print("\nENTERING", self.monitorMode, "MODE")
|
|
||||||
self.monitorDataInput.clear()
|
|
||||||
self.advanceDataEntryNextPress = False
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if key == "step":
|
|
||||||
print("\nSINGLE STEP FROM MONITOR ADDR")
|
|
||||||
# self.IP = self.monitorAddressInput.value
|
|
||||||
self.cpu.step()
|
|
||||||
time.sleep(0.5) # lazy debounce
|
|
||||||
|
|
||||||
if numericKeyPressed:
|
|
||||||
if self.monitorMode == 'addressEntry':
|
|
||||||
self.monitorAddressInput.input(int(key, 16))
|
|
||||||
self.cpu.IP = self.monitorAddressInput.value
|
|
||||||
print("MA", self.cpu.IP)
|
|
||||||
|
|
||||||
if self.monitorMode == 'dataEntry':
|
|
||||||
if self.advanceDataEntryNextPress:
|
|
||||||
print("ADVANCING")
|
|
||||||
self.cpu.IP = (self.cpu.IP + 1) % 256
|
|
||||||
# self.monitorDataInput.clear() # reset .currentDigit
|
|
||||||
self.monitorDataInput.set(self.cpu.memory[self.cpu.IP])
|
|
||||||
self.advanceDataEntryNextPress = False
|
|
||||||
self.monitorDataInput.input(int(key, 16))
|
|
||||||
self.cpu.memory[self.cpu.IP] = self.monitorDataInput.value
|
|
||||||
print("MD", self.monitorDataInput.value)
|
|
||||||
if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so next keypress is for the next address
|
|
||||||
self.advanceDataEntryNextPress = True
|
|
||||||
|
|
||||||
print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def displayScreen(self):
|
|
||||||
for x in range(8):
|
|
||||||
for y in range(8):
|
|
||||||
matrix[x, y] = self.cpu.memory[x + (8*y)]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
#self.cpu.start()
|
|
||||||
t = time.time()
|
|
||||||
while (time.time() - t) < 120: # TODO: add a time delta or sth maybe so this doesn't just burn cycles
|
|
||||||
self.handleKeys()
|
|
||||||
display_1.print(toHex(self.cpu.IP) + toHex(self.cpu.memory[self.cpu.IP]))
|
|
||||||
# display_1.print(toHex(self.monitorAddressInput.value) + toHex(self.cpu.memory[self.cpu.IP]))
|
|
||||||
# display_2.print(toHex(self.cpu.IP) + toHex(self.cpu.acc))
|
|
||||||
display_2.print(toHex(self.cpu.acc))
|
|
||||||
self.displayScreen()
|
|
||||||
if self.cpu.running:
|
|
||||||
self.cpu.step()
|
|
||||||
# time.sleep(0.5) # TODO ?
|
|
||||||
print("timeout")
|
|
||||||
print(self.cpu.memory)
|
|
||||||
|
|
||||||
|
|
||||||
cpu = CPU()
|
|
||||||
monitor = Monitor(cpu)
|
|
||||||
|
|
||||||
# preamble = '00 ' * 64
|
|
||||||
# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 12 f0 05 41 08 00 06 40 00 00' # STRIPES
|
|
||||||
# offset = 64
|
|
||||||
# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 05 08 09 00 04 09 03 f0 07 41 06' + toHex(offset) + '00 00'
|
|
||||||
#prog = '00'
|
|
||||||
# program_bytes = bytearray.fromhex(prog.replace(" ", ""))
|
|
||||||
|
|
||||||
# Add jmp at addr 254:
|
|
||||||
#program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600') # jump to addr 00
|
|
||||||
# program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0640') # jump to addr 0x40 (dec 64)
|
|
||||||
|
|
||||||
with open('test-multiply2.bin', 'rb') as file:
|
|
||||||
program_bytes = bytearray(file.read())
|
|
||||||
|
|
||||||
cpu.load_memory(program_bytes)
|
|
||||||
|
|
||||||
monitor.run()
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import cpu
|
|
||||||
|
|
||||||
c = cpu.CPU()
|
|
||||||
|
|
||||||
print(c)
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Cardiograph computers
|
||||||
|
|
||||||
|
The Cardiographs are a pair of imaginary computers,
|
||||||
|
designed as educational toys.
|
||||||
|
|
||||||
|
Inspired by the CARDIAC paper computer,
|
||||||
|
they are intended to be simple enough to build as
|
||||||
|
hand-operated paper models.
|
||||||
|
|
||||||
|
Their design is guided by two additional criteria:
|
||||||
|
|
||||||
|
1. They should be capable of producing interesting graphical output
|
||||||
|
2. They should accurately model the functioning of a real computer
|
||||||
|
(by operating on binary data, for example)
|
||||||
|
|
||||||
|
## The two computers
|
||||||
|
|
||||||
|
The two Cardiograph computers are:
|
||||||
|
|
||||||
|
1. the _Cardiograph Mark I_ (CG) is a mainframe machine
|
||||||
|
2. the _Micro Cardiograph_ (µCG) is a microprocessor trainer
|
||||||
|
(a miniaturized descendent of the mainframe)
|
||||||
|
|
||||||
|
They use the same instruction set and have very similar CPUs. (TODO: is that true?)
|
||||||
|
|
||||||
|
The main difference is in their peripheral hardware:
|
||||||
|
the Mark I is designed for batch processing and supports punched-card input,
|
||||||
|
while the MicroCardiograph is designed to be used interactively.
|
||||||
|
|
||||||
|
## Simulator
|
||||||
|
|
||||||
|
_[Micro ElectroCardiograph (µECG)](micro/readme-micro.md)_ is a simulator for the Micro Cardiograph.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Specification for the Cardiograph Architecture](docs/architecture-specification.md)
|
||||||
|
- [Design for the mainframe computer](docs/mainframe-design.md)
|
||||||
|
- [Design for the micro computer](docs/micro-design.md)
|
||||||
|
- [Assembly Language](docs/assembly-language.md)
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Node.js
|
||||||
|
|
||||||
|
|
||||||
|
### Quick examples
|
||||||
|
|
||||||
|
Assemble and run:
|
||||||
|
```./assembler.js -i <source.asm> | ./cardiograph.js```
|
||||||
|
|
||||||
|
Assemble to a file:
|
||||||
|
```./assembler.js -i <source.asm> -o <machinecode.out>```
|
||||||
|
|
||||||
|
Run from a file:
|
||||||
|
```./cardiograph.js -i <machinecode.out>```
|
||||||
|
|
||||||
|
|
||||||
|
### Assembler: assembler.js
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: ./assembler.js [-a] -i <input-file> [-o <output-file>]
|
||||||
|
|
||||||
|
-a, --annotate Output code with debugging annotations
|
||||||
|
-i, --in <file> Assembly-language input
|
||||||
|
-o, --out <file> Machine-code output
|
||||||
|
```
|
||||||
|
|
||||||
|
- If an output file is not provided, the output is printed to stdout
|
||||||
|
|
||||||
|
- If the `annotate` flag is not set, the machine code is returned as a string of space-separated decimal numbers
|
||||||
|
|
||||||
|
|
||||||
|
### Simulator: cardiograph.js
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: ./cardiograph.js [-i <file>]
|
||||||
|
|
||||||
|
-i, --in <file> Machine-code input
|
||||||
|
```
|
||||||
|
|
||||||
|
- If an input file is not provided, the input is read from stdin
|
||||||
Loading…
Reference in New Issue